| name | new-processing-method |
| description | Add a new xmris processing/accessor function end-to-end following the project's architectural rules. Use when adding a new `.xmr` method or transform (e.g. a new apodization, phasing, Fourier, baseline, or fitting operation), or when asked to "add a processing function/method" to xmris. |
Add a new xmris processing method
Scaffold a new library function and wire it into the .xmr accessor following the project's strict architecture, then prove it with a notebook test.
0. Read the rules first
Read docs/contributing/ai_context.md (the "8 Commandments" + code templates) before writing anything. All of it applies. The steps below are the workflow; that file is the source of truth for how each piece is written.
1. Place the transform
Add the pure math to the right domain module under src/xmris/ — processing/ (fid.py, fourier.py, phasing.py, baseline.py), vendor/ (hardware sanitization), or fitting/. Match the surrounding file's style.
Rules that are easy to get wrong here:
- Functional purity — never mutate in place; always return a new
xr.DataArray/xr.Dataset.
- No magic strings — import
ATTRS/DIMS/COORDS/VARS from xmris.core.config; never hardcode "time", "reference_frequency", etc. inside the package.
- Domain contract — decide it up front (see
docs/explanation/domains.md): only meaningful in one domain and consumed there (e.g. phasing, baseline) → @ensures_domain(DOMAIN) (funnel: result stays there); same physics seen from either domain (e.g. apodization, zero-fill) → @computes_in(DOMAIN) (representation restored, even when the length changes); converter/primitive/fitting → no domain decorator, transforms stay explicit. Route any conversion through to_spectrum/to_fid/to_ppm/to_hz — never inline fft/ifft.
- Dim defaults (the biconditional) — a
dim argument defaults to the config constant (dim: str = DIMS.time), EXCEPT it defaults to None iff the function is domain-decorated with a multi-label domain (SPECTRAL_DIMS) — the decorator resolves Hz vs ppm. Enforced by TestDomainDimRule.
- Validation — guard required attrs with
@requires_attrs(...) (free-function style: first arg is the DataArray); validate dims with _check_dims(da, dim, "func_name").
- Coordinates — build new coords with
as_variable(TERM, dim, data) + .assign_coords(...), don't hand-mutate .attrs.
- Lineage — append only quantitative parameters to
.attrs (e.g. phase_p0=15.0); no boolean/string flags.
- Docstring — NumPy convention, fully-typed signature (feeds the quartodoc API docs).
2. Extend the vocabulary if needed
If the function needs a dim/coord/attr not already in src/xmris/core/config.py, add the XmrisTerm there (with unit/long_name) and explicitly tell the user every new ATTRS/DIMS/COORDS/VARS term you introduced so it can be tracked.
3. Register on the accessor
Expose the function via the .xmr namespace on XmrisAccessor (or XmrisDatasetAccessor) in src/xmris/core/accessor.py, and add it to the public API in src/xmris/__init__.py (__all__) if it's user-facing.
4. Write the notebook test (this is how math is tested here)
Math/science is tested via MyST .md notebooks in docs/notebooks/<area>/ (basics/pipeline/fitting/vendor/visualization), not test_*.py. Create or extend the relevant notebook with:
- Markdown explaining the math/physics (use rich MyST: LaTeX, mermaid, dropdowns where useful).
- A cell generating synthetic, noisy
xarray data.
- A cell applying the new function and plotting the result.
- Assertion cells (
assert / np.testing.assert_allclose) proving both the numeric result and that dims/coords/attrs were preserved — each tagged # %% tags=["remove-cell"] so nbmake runs them but the rendered docs hide them.
Use a testonly_-prefixed file for pure test notebooks that shouldn't render.
5. Verify
Run the affected notebook and the architecture tests:
uv run test-gen # regenerate .ipynb from the .md notebooks
uv run pytest tests/test_core.py -n0 --no-cov # architecture invariants
uv run ruff format . && uv run ruff check . --fix
uv run mypy src/xmris # fix clear type errors
Then uv run test for the full pipeline (regenerates notebooks + runs everything) before wrapping up.
6. Report
Summarize the new function, any new config.py terms (call these out explicitly), and where its notebook test lives.