Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
63462bb
prototype ptychodus_store and update agent module
stevehenke Jun 30, 2026
3798ca6
add visualization support and switch to httpx everywhere
stevehenke Jul 6, 2026
2632992
Merge branch 'main' into webservice
stevehenke Jul 18, 2026
12ba623
update cxi file readers/writers
stevehenke Jul 20, 2026
40eb6af
prototype web ui
stevehenke Jul 22, 2026
0d000aa
add support for multiple sets of diffraction patterns
stevehenke Jul 31, 2026
1432f69
remove detector width/height settings and use loaded dataset shape di…
stevehenke Jul 31, 2026
9774ccc
ensure that detector extent and bad pixels shape cannot mismatch
stevehenke Jul 31, 2026
cfd48ee
require DiffractionMetadata.detector_extent
stevehenke Jul 31, 2026
bc49e93
testing fixes
stevehenke Jul 31, 2026
c2ab7e0
enqueue product creation
stevehenke Aug 1, 2026
1fefa3b
deduplicate product item models
stevehenke Aug 1, 2026
9b3a3ca
make detector settings per-dataset
stevehenke Aug 3, 2026
883162e
fix handling of product geometry updates
stevehenke Aug 3, 2026
66bb612
visualization tools respect the selected complex component
stevehenke Aug 4, 2026
7ec8607
trim begin/end of position sequence; fix position transformations
stevehenke Aug 4, 2026
6580e00
mirror the position builder restructure into probe and object
stevehenke Aug 5, 2026
495b87b
update diffraction views to become consistent with the product views
stevehenke Aug 5, 2026
5fccdf1
auto-select product in processing view whenever repository is not empty
stevehenke Aug 5, 2026
59f5676
add polarization and tilt angle to metadata
stevehenke Aug 5, 2026
8601c38
display metadata values in wizard page before apply
stevehenke Aug 5, 2026
7b682f9
promote fluorescence enhancement to top-level view
stevehenke Aug 5, 2026
963d804
only acquire gpu contexts in subprocesses (in-progress)
stevehenke Aug 7, 2026
1539f78
standardize pluginchooser combo box wiring
stevehenke Aug 7, 2026
eb92433
migrate rst to md
stevehenke Aug 7, 2026
3b25ae3
clean up ptychopinn subprocess interface
stevehenke Aug 10, 2026
e642092
replace min/max sliders with DecimalRangeSlider and reuse ImageView f…
stevehenke Aug 10, 2026
4a33a58
add PtychoFM reconstructor backend
haskels Aug 11, 2026
078cbb3
style: reformat docs code blocks per ruff format
haskels Aug 11, 2026
584b8de
testing fixes
stevehenke Aug 11, 2026
cc1ea26
make web ui consistent with qt ui
stevehenke Aug 11, 2026
aa3f077
pre-release fixes
stevehenke Aug 11, 2026
1f01e69
fix formatting
stevehenke Aug 12, 2026
8d9eab1
Merge remote-tracking branch 'origin/webservice' into haskels/ptycho-…
haskels Aug 12, 2026
e433b85
ptycho_fm: export training data as paired HDF5 for ptycho_vit
haskels Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
97 changes: 97 additions & 0 deletions .claude/skills/add-core/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: add-core
description: Scaffold a new *Core subsystem (settings + parameters + observers + api) and wire it through ModelCore, ViewCore, and ControllerCore. Use when the user says "add a new analysis/processing feature", "add a new model component", or is introducing a subsystem that needs its own settings group and GUI panel.
---

# add-core

Every ptychodus subsystem follows the same shape: a `*Core` class in `src/ptychodus/model/<feature>/` owns its `SettingsRegistry` group, typed parameters, sub-components, and API; `model/core.py::ModelCore` composes it; and the view + controller layers mirror the layout. This skill encodes that shape.

## Layer boundaries (do not cross)

- `src/ptychodus/api/` — pure domain, no dependencies on model/view/controller.
- `src/ptychodus/model/` — logic; depends on `api/` only.
- `src/ptychodus/view/` — PyQt5 widgets; no logic.
- `src/ptychodus/controller/` — mediates view ↔ model.

Run the `check-layers` skill after wiring to confirm you haven't accidentally introduced a model→view import.

## Steps

### 1. Settings

Create `src/ptychodus/model/<feature>/settings.py`. Follow the pattern in [src/ptychodus/model/analysis/settings.py](../../src/ptychodus/model/analysis/settings.py):

```python
from ptychodus.api.observer import Observable, Observer
from ptychodus.api.settings import SettingsRegistry


class <Feature>Settings(Observable, Observer):
def __init__(self, registry: SettingsRegistry) -> None:
super().__init__()
self._group = registry.create_group('<Feature>')
self._group.add_observer(self)

# Typed parameters:
self.num_iterations = self._group.create_integer_parameter(
'NumberOfIterations', 1000, minimum=1
)
# create_real_parameter, create_boolean_parameter, create_string_parameter,
# create_path_parameter — see api/parametric.py for the full menu.

def _update(self, observable: Observable) -> None:
if observable is self._group:
self.notify_observers()
```

Preserve unit suffixes on physical quantities (e.g. `probe_energy_eV`, `pixel_width_m`) — CLAUDE.md notes these are accepted via `# noqa: N815` and reviewers expect them.

### 2. Core

Create `src/ptychodus/model/<feature>/core.py`:

```python
class <Feature>Core:
def __init__(
self,
settings_registry: SettingsRegistry,
# ... other dependencies from ModelCore (repositories, other Cores)
) -> None:
self.settings = <Feature>Settings(settings_registry)
# ... construct sub-components, wire observers
```

For an example that composes many sub-components + `VisualizationEngine`s, see [src/ptychodus/model/analysis/core.py](../../src/ptychodus/model/analysis/core.py).

### 3. Compose in ModelCore

In [src/ptychodus/model/core.py](../../src/ptychodus/model/core.py), import your `<Feature>Core` and construct it in `ModelCore.__init__` in the correct dependency order — after everything it needs, before anything that needs it. `ModelCore` is the single composition root; do not construct components anywhere else.

### 4. View

Create `src/ptychodus/view/<feature>/` mirroring the model layout. PyQt5 widgets only — no logic, no imports from `model/` or `controller/`.

### 5. Controller

Create `src/ptychodus/controller/<feature>/` with the `*ViewController` that bridges the view widgets to `<Feature>Core`.

### 6. Wire into ViewCore and ControllerCore

`ViewCore` and `ControllerCore` (in `src/ptychodus/view/core.py` and `src/ptychodus/controller/core.py`) are composition roots. Register the new panel in both. **The navigation toolbar order in `ViewCore` is the source of truth for the left/right stacked-panel indexes** (per CLAUDE.md) — pick the toolbar position deliberately and ensure `ControllerCore` uses the matching stacked-widget index.

## PluginChooser binding for settings-driven selection

If your feature has a "current selected implementation" (e.g. current file reader), bind a `PluginChooser` to a `StringParameter` in your settings:

```python
self.file_reader_chooser.synchronize_with_parameter(self.settings.file_type)
```

`synchronize_with_parameter` in [src/ptychodus/api/plugins.py](../../src/ptychodus/api/plugins.py) sets up the two-way binding: parameter value ↔ current plugin.

## Verify

- `check-layers` — no boundary violations.
- `pre-push` — full CI gate green.
- Launch the GUI (`uv run ptychodus`) and confirm the new panel shows up in the correct toolbar position.
103 changes: 103 additions & 0 deletions .claude/skills/add-nav-icon/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
name: add-nav-icon
description: Add a navigation-bar icon to ptychodus in both the PyQt GUI and the ptychodus_store web UI. Covers SVG placement, Qt resource compilation with the manual typing fix that pyrcc5 clobbers, ViewCore wiring, nav.ts wiring, and the tsc rebuild. Use when the user says "add a nav icon", "add a toolbar icon", "wire up an icon for the new X panel", or is following up on add-core / a new panel.
---

# add-nav-icon

Ptychodus has two navigation surfaces that share icon assets: the PyQt5 toolbar in `ViewCore` and the browser nav bar in `ptychodus_store`. SVGs live once in [`src/ptychodus_store/ui/icons/`](../../src/ptychodus_store/ui/icons/); the PyQt side pulls them in via a Qt resource file compiled to `resources.py`, and the web UI pulls them straight from the FastAPI static mount. This skill wires a new icon through both.

## Step 1 — Add the SVG

Drop the SVG into [`src/ptychodus_store/ui/icons/`](../../src/ptychodus_store/ui/icons/). Do not put icons anywhere else — the `.qrc` file uses a relative path (`../../ptychodus_store/ui/icons/…`) to reach exactly this directory.

- Kebab-case filename (e.g. `my-icon.svg`).
- `fill="currentColor"` so CSS themes work.
- Font Awesome 7.1.0 (CC BY 4.0) is the house style. See [`icons/Font-Awesome-LICENSE.txt`](../../src/ptychodus_store/ui/icons/Font-Awesome-LICENSE.txt) for attribution and [`icons/README.md`](../../src/ptychodus_store/ui/icons/README.md) for the update procedure.

## Step 2 — Register in the Qt resource file

Add one line to [`src/ptychodus/view/resources.qrc`](../../src/ptychodus/view/resources.qrc), keeping the list alphabetized by `alias`:

```xml
<file alias="my-feature">../../ptychodus_store/ui/icons/my-icon.svg</file>
```

The `alias` is what code references as `:/icons/my-feature`. Convention: alias matches the panel concept, not the filename (e.g. `atom.svg` is aliased `fluorescence`).

## Step 3 — Regenerate `resources.py` and reapply the typing fix

`resources.py` is auto-generated binary. Regenerate from inside the venv:

```sh
cd src/ptychodus/view
uv run pyrcc5 -o resources.py resources.qrc
```

Fresh `pyrcc5` output writes the two module functions **unannotated and camelCase**, which fails both `mypy` (missing return type) and `ruff` (`N802`, function name should be lowercase). Reapply the manual typing fix — patch the tail of the file so the two functions look like this:

```python
def qInitResources() -> None: # noqa: N802
QtCore.qRegisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)


def qCleanupResources() -> None: # noqa: N802
QtCore.qUnregisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)


qInitResources()
```

Two edits per function: append `-> None` to the signature and `# noqa: N802` to the same line. Leave the `WARNING! All changes made in this file will be lost!` header — the noqa comments and return annotations are the only manual patches, and this pattern must survive every regeneration. (The import site at [`view/core.py:30`](../../src/ptychodus/view/core.py#L30) uses `from . import resources # noqa` for the same reason — it's imported for side effects.)

## Step 4 — Wire it into `ViewCore`

Add a call to `self.navigation.add_panel(...)` in [`src/ptychodus/view/core.py`](../../src/ptychodus/view/core.py) `ViewCore.__init__`. Order matters — **the sequence of `add_panel` calls is the source of truth for the left/right stacked-panel indexes** (per CLAUDE.md). Mirror the fluorescence pattern at [`view/core.py:275-280`](../../src/ptychodus/view/core.py#L275-L280):

```python
self.my_feature_view = MyFeatureView()
self.my_feature_image_view = MyFeatureImageView()
self.my_feature_action = self.navigation.add_panel(
QIcon(':/icons/my-feature'),
'My Feature',
left=self.my_feature_view,
right=self.my_feature_image_view,
)
```

If the new panel belongs nested under a parent (Products or Processing today), add its action to the existing `add_subview_group` call at [`view/core.py:336-352`](../../src/ptychodus/view/core.py#L336-L352). Otherwise it renders as a top-level toolbar button.

`ControllerCore` uses stacked-widget indexes matching `ViewCore`'s `add_panel` order — inserting a panel in the middle shifts every downstream index. Prefer appending, or run the app afterward and confirm nothing underneath shifted.

## Step 5 — Wire it into the web UI

Add one entry to the `NAV` array in [`src/ptychodus_store/ui/src/nav.ts`](../../src/ptychodus_store/ui/src/nav.ts):

```typescript
{ route: 'my-feature', label: 'My Label', icon: 'my-icon.svg' },
```

- `route` — page identifier used by the front-end router.
- `label` — tooltip / text under the button.
- `icon` — **filename only**; the render code prefixes `/ui/icons/`.

No backend router change is needed. FastAPI already serves `src/ptychodus_store/ui/` at `/ui/` via the `StaticFiles` mount in [`src/ptychodus_store/app.py`](../../src/ptychodus_store/app.py), so a new SVG in `ui/icons/` is automatically reachable at `/ui/icons/my-icon.svg`.

## Step 6 — Rebuild the TypeScript

```sh
cd src/ptychodus_store/ui
tsc
```

No bundler, no framework — plain `tsc` compiles `src/**/*.ts` into `dist/` (see `tsconfig.json`). Restart the store server so the fresh compiled JS is loaded; `tsc --watch` is fine for iteration.

## Verify

- **PyQt:** `uv run ptychodus` — the new icon appears at the correct toolbar position (top-level or nested), highlights on click, and swaps in the associated left/right panels.
- **Web UI:** run the `store-dev` skill, load the browser UI, confirm the icon renders in the nav bar (network tab should show a 200 for `/ui/icons/my-icon.svg`).
- **CI gate:** run the `pre-push` skill. If `pyrcc5` wiped the typing fix, `ruff` will fail with `N802` on `qInitResources` / `qCleanupResources` — reapply Step 3 and re-run.
69 changes: 69 additions & 0 deletions .claude/skills/add-plugin/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: add-plugin
description: Scaffold a new file-format plugin (diffraction / probe / probe-position / object / product / fluorescence reader or writer) under src/ptychodus/plugins/. Use when the user says "add a plugin for X format", "add a reader/writer for Y", or is integrating a new beamline data format.
---

# add-plugin

Plugins are auto-discovered by `PluginRegistry.load_plugins()` in [src/ptychodus/api/plugins.py](../../src/ptychodus/api/plugins.py). Every module under `src/ptychodus/plugins/` that defines a module-level `register_plugins(registry)` function is imported and registered at startup; modules whose imports fail are logged and skipped (this is how optional-dependency plugins silently disable themselves).

## Steps

1. **Identify the abstract interface.** Pick the right one from `ptychodus.api`:

| You want to read/write... | Interface | Registry chooser |
| --- | --- | --- |
| Raw diffraction patterns | `DiffractionFileReader/Writer` (`api/diffraction.py`) | `diffraction_file_readers` / `_writers` |
| Probe positions | `ProbePositionFileReader/Writer` (`api/probe_positions.py`) | `probe_position_file_readers` / `_writers` |
| Probes | `ProbeFileReader/Writer` (`api/probe.py`) | `probe_file_readers` / `_writers` |
| Objects | `ObjectFileReader/Writer` (`api/object.py`) | `object_file_readers` / `_writers` |
| A full Product (probe + positions + object bundled) | `ProductFileReader/Writer` (`api/product.py`) | see step 4 |
| Fluorescence / bad pixels | `Fluorescence*` / `BadPixelsFileReader` | corresponding choosers |

2. **Create the module** at `src/ptychodus/plugins/<beamline_or_format>_<kind>_file.py`. Follow the naming convention already in use — e.g. `csv_probe_file.py`, `aps33id_velociprobe/`, `aps19id_isn_diffraction_file.py`. Beamline plugins may be a subpackage if they need more than one file.

3. **Implement Reader and/or Writer** by subclassing the interface and providing the `read(path) -> ...` / `write(path, data) -> None` method. Keep the imports minimal — put any optional dependency import at module top so a `ModuleNotFoundError` cleanly disables the plugin.

4. **Register.** Add `register_plugins(registry: PluginRegistry)` at module level.

- **For a product reader**, prefer `register_product_file_reader_with_adapters` so the same reader is also reachable as a probe/probe-position/object reader without extra boilerplate:

```python
def register_plugins(registry: PluginRegistry) -> None:
registry.register_product_file_reader_with_adapters(
MyProductReader(),
display_name='My Format (*.h5)',
simple_name='MyFormat',
)
```

- **For everything else**, register on the appropriate chooser with both `display_name` (human-readable, includes the glob) and `simple_name` (short token used by the settings string):

```python
def register_plugins(registry: PluginRegistry) -> None:
registry.probe_file_readers.register_plugin(
MyProbeReader(),
simple_name='MyFormat',
display_name='My Format Files (*.myext)',
)
```

`simple_name` defaults to a stripped alphanumeric form of `display_name`; provide it explicitly if you want a stable settings token.

## Reference examples

Read one of these before writing new code — pick the closest match:

- Simple single-file reader+writer pair: [src/ptychodus/plugins/csv_probe_file.py](../../src/ptychodus/plugins/csv_probe_file.py)
- HDF5-backed diffraction: [src/ptychodus/plugins/h5_diffraction_file.py](../../src/ptychodus/plugins/h5_diffraction_file.py)
- Product reader with adapters: [src/ptychodus/plugins/cxi_file.py](../../src/ptychodus/plugins/cxi_file.py), [src/ptychodus/plugins/h5_product_file.py](../../src/ptychodus/plugins/h5_product_file.py)
- Beamline subpackage: [src/ptychodus/plugins/aps33id_velociprobe/](../../src/ptychodus/plugins/aps33id_velociprobe/)
- Optional-dep plugin (fails cleanly if the dep is missing): [src/ptychodus/plugins/lcls_file_readers.py](../../src/ptychodus/plugins/lcls_file_readers.py)

## Index preservation

Diffraction patterns and probe positions are paired by **integer scan index**, not array order (see CLAUDE.md "Index-based pattern/position association"). A new diffraction reader must set `DiffractionArray.get_indexes()`; a new probe-position reader must set `ProbePosition.index`. A round-trip test that preserves indexes is worth writing.

## Verify

After creating the plugin, run the `check-plugin-load` skill to confirm it registers cleanly, then run `pre-push` before committing.
Loading
Loading