Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ Thumbs.db
tests/

# Secret keys or env files (if not required in build)
.env
# Exclude .env in root, but allow frontend/.env.react
/.env
.secrets
1 change: 1 addition & 0 deletions .env.react
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_WEBSOCKET_URL=ws://ochoa.als.dhcp.lbl.gov:8001/simImages
10 changes: 0 additions & 10 deletions AP-XPS.code-workspace

This file was deleted.

114 changes: 114 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

ArroyoXPS is a real-time streaming data analysis service for X-ray Photoelectron Spectroscopy (XPS) at the Advanced Light Source (ALS) beamline. It ingests ZMQ messages from LabVIEW instruments, processes detector frames (peak fitting, FFT analysis), and publishes results over WebSocket to a React frontend and to a Tiled data server.

## Commands

### Python Backend

```bash
# Install with dev dependencies (uses uv or pip)
pip install ".[dev]"

# Run tests
python -m pytest

# Run a single test file
python -m pytest src/_tests/test_processor.py -v

# Linting (pre-commit runs flake8, black, isort)
pre-commit run --all-files
```

Max line length is 115 (configured in `.flake8`).

### Frontend

```bash
cd frontend
npm install
npm start # dev server at http://localhost:3000
npm run build
```

### Running Locally (Docker)

```bash
# One-time setup
docker network create mle_net
cp .env.example .env # then set TILED_SINGLE_USER_API_KEY

# Start all services
docker-compose up -d

# With LabVIEW simulator instead of real hardware
docker-compose -f docker-compose-simulator.yaml up -d
```

Services: Frontend at `:8080`, Tiled at `:8000`, Jaeger at `:16686`, Prometheus at `:9090`, Grafana at `:3000`.

### Running Without Docker

```bash
# Start the LabVIEW frame simulator
python -m tr_ap_xps.simulator
```

## Architecture

### Data Flow

```
LabVIEW (ZMQ PUB) → XPSLabviewZMQListener → XPSOperator → XPSProcessor
XPSWSResultPublisher (WebSocket)
TiledPublisher (Tiled server)
```

**Message lifecycle:**
1. LabVIEW sends three message types over ZMQ: `start` (scan metadata), `event` (detector frame), `stop`
2. `labview.py` parses raw ZMQ messages into Pydantic models (`XPSStart`, `XPSRawEvent`, `XPSStop`)
3. `XPSOperator` (`pipeline/xps_operator.py`) orchestrates the processing pipeline using the Arroyopy framework
4. `XPSProcessor` (`pipeline/xps_processor.py`) does the computation: frame integration, rolling mean/std, peak fitting, FFT
5. Results (`XPSResult`) are published to WebSocket clients and Tiled

### Key Components

- **`src/tr_ap_xps/`** — main package
- `schemas.py` — all Pydantic message models; LabVIEW JSON field names mapped via aliases
- `labview.py` — ZMQ listener; handles BigEndian binary frame buffers from LabVIEW
- `websockets.py` — WebSocket publisher; uses msgpack binary protocol for efficiency
- `tiled.py` — Tiled server integration for data persistence
- `config.py` — Dynaconf configuration (env vars prefixed `DYNACONF_`, override via `.secrets.yaml`)
- `pipeline/xps_operator.py` — Arroyopy `Operator` subclass; async `process()` entry point
- `pipeline/xps_processor.py` — core XPS computation (horizontal integration, rolling stats)
- `pipeline/peak_fitting.py` — Bayesian blocks peak detection + Astropy Gaussian fitting
- `pipeline/fft.py` — vertical FFT + inverse FFT filtering with configurable repeat factors
- `simulator/` — LabVIEW simulators for local development

- **`frontend/src/`** — React 18 frontend
- WebSocket connection managed via custom hooks
- Plotly.js for heatmaps and scatter plots
- msgpack decoding of binary WebSocket frames

### Framework: Arroyopy

The backend is built on [Arroyopy](https://github.com/als-computing/arroyo), an async ZMQ pub/sub framework. Key base classes:
- `Operator` — processes incoming messages, calls `process()` for each
- `Publisher` — sends results to an output sink

### Observability

- OpenTelemetry tracing via `@traced` decorators; Jaeger collects traces
- Prometheus metrics endpoint (configured in `config/prometheus.yml`)
- Grafana dashboards pre-configured in `config/grafana-dashboard.json`

## Serialization Notes

- LabVIEW frames arrive as BigEndian binary buffers; `DATATYPE_MAP` in `labview.py` handles type conversion
- WebSocket results use msgpack (binary) for efficiency, not JSON
- Zarr format used for Tiled data storage
2 changes: 1 addition & 1 deletion Dockerfile_frontend
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ WORKDIR /frontend
COPY ./frontend/package*.json /frontend/
RUN npm ci
COPY ./frontend /frontend/
COPY .env /frontend/.env
COPY ./frontend/.env.react /frontend/.env
RUN npm run build

FROM nginx:1.26-alpine
Expand Down
16 changes: 13 additions & 3 deletions Dockerfile_processor
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
FROM python:3.11
FROM python:3.13

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Copy dependency files first for better caching
COPY pyproject.toml /app/

# Copy the rest of the application
COPY . /app
# RUN pip install ./arroyo
RUN pip install --no-cache-dir --upgrade .

# Install dependencies
RUN uv pip install --system --no-cache .



ENTRYPOINT ["arroyo", "run"]
1 change: 1 addition & 0 deletions block_configs/timepix_processor_block.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ blocks:
class: tr_ap_xps.pipeline.xps_operator.build_xps_operator
kwargs:
build_heatmaps: false
otlp_endpoint: "http://jaeger:4317"

listeners:
- class: tr_ap_xps.timepix.xps_timepix_listener_factory
Expand Down
141 changes: 141 additions & 0 deletions config/grafana-dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{
"title": "Arroyopy Metrics",
"dashboard": {
"title": "Arroyopy Metrics",
"tags": ["arroyopy", "telemetry"],
"timezone": "browser",
"schemaVersion": 16,
"version": 0,
"refresh": "5s",
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"panels": [
{
"id": 1,
"title": "Messages Per Second",
"type": "graph",
"targets": [
{
"expr": "rate(arroyopy_processing_seconds_count[1m])",
"legendFormat": "rate",
"refId": "A"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"yaxes": [
{
"label": "messages/sec",
"show": true
},
{
"show": true
}
]
},
{
"id": 2,
"title": "Average Processing Time",
"type": "graph",
"targets": [
{
"expr": "arroyopy_avg_processing_seconds",
"legendFormat": "avg time",
"refId": "A"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"yaxes": [
{
"label": "seconds",
"show": true
},
{
"show": true
}
]
},
{
"id": 3,
"title": "Total Messages Processed",
"type": "graph",
"targets": [
{
"expr": "arroyopy_processing_seconds_count",
"legendFormat": "total",
"refId": "A"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"yaxes": [
{
"label": "total messages",
"show": true
},
{
"show": true
}
]
},
{
"id": 4,
"title": "Processing Time Percentiles",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.50, rate(arroyopy_processing_seconds_bucket[5m]))",
"legendFormat": "p50",
"refId": "A"
},
{
"expr": "histogram_quantile(0.95, rate(arroyopy_processing_seconds_bucket[5m]))",
"legendFormat": "p95",
"refId": "B"
},
{
"expr": "histogram_quantile(0.99, rate(arroyopy_processing_seconds_bucket[5m]))",
"legendFormat": "p99",
"refId": "C"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"yaxes": [
{
"label": "seconds",
"show": true
},
{
"show": true
}
]
}
]
},
"folderId": 0,
"overwrite": true
}
12 changes: 12 additions & 0 deletions config/grafana-dashboards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: 1

providers:
- name: 'Arroyopy Dashboards'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
15 changes: 15 additions & 0 deletions config/grafana-datasources.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Grafana datasource provisioning configuration

apiVersion: 1

datasources:
# Prometheus datasource
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
jsonData:
timeInterval: "5s"
queryTimeout: "60s"
27 changes: 27 additions & 0 deletions config/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Prometheus configuration for Arroyopy metrics

global:
scrape_interval: 15s # How frequently to scrape targets
evaluation_interval: 15s # How frequently to evaluate rules

# Scrape configurations
scrape_configs:
# Arroyopy application metrics
- job_name: 'arroyopy'
static_configs:
- targets: ['host.docker.internal:8000'] # Default metrics port
labels:
service: 'arroyopy'
environment: 'development'

# Scrape more frequently for development
scrape_interval: 5s

# Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']

# Note: For Linux, you may need to use the host's IP address instead of host.docker.internal
# Find your IP with: ip addr show docker0 | grep inet
# Then replace host.docker.internal with that IP (usually 172.17.0.1)
20 changes: 20 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
dev_processor:
# dcc -f docker-compose-dev.yml run --entrypoint /bin/bash dev_processor -i
build:
context: .
dockerfile: Dockerfile_processor
command: "block_configs/timepix_processor_block.yaml"
restart: unless-stopped

volumes:
- .:/app:rw,z
ports:
- "8001:8001"
networks:
mle_net:

networks:
mle_net:
name: mle_net
driver: bridge
Loading
Loading