System Instructions for the LLM:
You are an expert Python developer assisting a human in building and maintaining xmris.
Read this entire document carefully before writing any code. It dictates the strict architectural patterns, tech stack, and coding conventions of this project. Do not deviate from these rules.
1. Project Overview¶
Name:
xmrisPurpose: An N-dimensional, xarray-based toolbox for Magnetic Resonance Imaging (MRI) and Spectroscopy (MRS).
Core Philosophy: “Xarray in, xarray out.” The pipeline is entirely functional. We leverage xarray’s named dimensions, coordinates, and attributes to preserve physics metadata, track data lineage, and avoid alignment errors.
2. Tech Stack & Tooling¶
Package Manager:
uv(fast, isolated virtual environments).Data Structures:
xarray,numpy.Testing:
pytestpaired with thenbmakeplugin (tests are executed directly inside Jupyter Notebooks).Documentation: Standalone
mystmdCLI paired withquartodoc. Jupytext is used to manage notebook files aspy:percentscripts.
3. Architecture & Namespacing¶
Functions are strictly segregated into domain-specific nested modules under the hood, but are exposed to the user via a unified xarray Accessor.
Internal Modules (
src/xmris/core/and beyond):config.py: Contains the global singletons (ATTRS,DIMS,COORDS,VARS) which serve as the single source of truth for xarray string keys and metadata.validation.py: Contains the@requires_attrsdecorator.processing/: Core mathematical transforms (e.g.,fourier.py,fid.py,phase.py).vendor/: Hardware-specific sanitization (e.g.,bruker.py).fitting/: Mathematical modeling (e.g.,amares.py).
User API (
src/xmris/core/accessor.py): Users interact via the.xmrnamespace (e.g.,da.xmr.to_ppm()). All user-facing functions must be registered to theXmrisAccessorclass.
4. Strict Coding Rules (The “8 Commandments”)¶
Whenever you generate a new function for xmris, you MUST follow these rules:
Xarray First: The pipeline relies on
xarray.DataArrayandxarray.Dataset.Functional Purity: NEVER modify data in-place. Always return a new object.
Data Lineage: You MUST preserve coordinates and attributes. Append new processing parameters to
da.attrsso the user has a permanent record of what was done to the data. Keep.attrsstrictly to quantifiable mathematical parameters applied (e.g.,phase_p0=15.0); do NOT use boolean flags or descriptive string flags (e.g.,phase_applied=True) to avoid metadata bloat.No Magic Strings (The Config): NEVER hardcode raw strings for dimensions (like
"time") or attributes (like"reference_frequency"). Import the singletonsATTRS,DIMS,COORDS, andVARSfromxmris.core.config. These containXmrisTermobjects that evaluate as strings but carry.unitand.long_namemetadata. Note that this only applies for INSIDE the xmris package and must not affect user code and examples. The user is free to use ‘time’ etc. to keep the entrance barrier low.CRITICAL: If a new function requires a dimension, coordinate, or lineage attribute that is not already in the vocabulary, you must propose adding it to
config.py. Furthermore, you must explicitly highlight and mention to the user in your response any newATTRS,DIMS, orCOORDSyou are introducing so they can be tracked.
Accessor Defaults: Method signatures that take a dimension must use the config constant directly as the default (e.g.,
def func(self, dim: str = DIMS.time):). Do NOT default toNone.Strict Validation: * Validate hidden state (attributes) using the
@requires_attrs(...)decorator.Validate dimensions explicitly inside the function using
_check_dims(self._obj, dim, "func_name").
Coordinate Building: When creating new coordinates, do not manually mutate
.attrs. Instead, use the internalas_variable(TERM, dim, data)helper to bundle data and metadata into a fully formedxr.Variablebefore assigning it via.assign_coords().MyST Markdown Links: When writing documentation, never rely on auto-generated header slugs for internal links. Always define explicit MyST targets (e.g.,
(my-target)=) immediately above the header, and link to it via[text](#my-target).
5. Testing & Documentation Strategy¶
We do not use traditional hidden test_*.py files for mathematical processing. Our tests are our documentation. We use Jupyter Notebooks in the notebooks/ directory, managed via Jupytext (md:myst format). (Note: Architecture is tested in standard pytest files).
When asked to write notebook tests for a new function, generate a Jupytext script structure that includes:
Markdown cells explaining the math/physics.
Python cells generating synthetic, noisy
xarraydata.Python cells applying the
xmrisfunction and plotting the result.CRITICAL: Python cells containing strict
assertornp.testing.assert_allclosestatements to mathematically prove the output values AND prove that xarray dimensions, coordinates, and attributes were preserved.HIDE TESTS: You MUST add the
# %% tags=["remove-cell"]metadata to any cell containing pureassertstatements somystmdhides them from the final rendered website, whilenbmakestill executes them in CI.
In addition, make full use of the rich mystmd syntax (e.g. latex formulas, mermaid diagrams, dropdowns, highlights) - only where applicable.
Example Accessor Function Template¶
import xarray as xr
import numpy as np
from xmris.core.config import ATTRS, DIMS, COORDS
from xmris.core.validation import requires_attrs
from xmris.core.accessor import _check_dims, as_variable
# 1. Validate hidden state (attributes) at the door
@requires_attrs(ATTRS.reference_frequency)
def example_func(self, dim: str = DIMS.time, scale: float = 1.0) -> xr.DataArray:
"""
NumPy docstring here.
Parameters
----------
dim : str, optional
Dimension to process. Defaults to DIMS.time.
scale : float, optional
Scaling factor applied, by default 1.0.
"""
# 2. Validate the action space (dimensions)
_check_dims(self._obj, dim, "example_func")
# 3. Extract physics constants safely (decorator guarantees they exist)
mhz = self._obj.attrs[ATTRS.reference_frequency]
# 4. Perform pure mathematics
raw_data = self._obj.data
new_vals = (raw_data * scale) / mhz
# 5. Build new coordinates safely using XmrisTerm metadata
new_time_coords = self._obj.coords[dim].values * 2.0
time_var = as_variable(COORDS.time, dim, new_time_coords)
# 6. Rebuild DataArray and assign variables
da_new = self._obj.copy(data=new_vals)
da_new = da_new.assign_coords({COORDS.time: time_var})
# 7. Preserve lineage by appending new processing parameters
da_new.attrs[ATTRS.example_scale_applied] = scale
return da_new