Problem
Processing functions force users into manual domain juggling:
da.xmr.to_hz().xmr.autophase() (Hz vs ppm)
da.xmr.to_spectrum().xmr.autophase().xmr.to_fid() (time vs frequency)
Solution
Three decorator tiers, named by agency level:
@enforces_domain(DIMS.frequency) # actively FFTs if needed, restores after
@resolves_spectral_dim # auto-detects Hz/ppm, injects dim=None
@requires_attrs(ATTRS.reference_frequency) # checks existence, raises if missing
def autophase(da, dim=None):
| Decorator |
Verb |
Agency |
Cost |
@requires_attrs(...) |
gate |
Raises if missing |
Zero |
@resolves_spectral_dim |
resolve |
Introspects dims, fills dim=None |
Zero |
@enforces_domain(DIMS.x) |
transform |
FFT round-trip if wrong domain |
O(N log N) |
Checklist
Problem
Processing functions force users into manual domain juggling:
da.xmr.to_hz().xmr.autophase()(Hz vs ppm)da.xmr.to_spectrum().xmr.autophase().xmr.to_fid()(time vs frequency)Solution
Three decorator tiers, named by agency level:
@requires_attrs(...)@resolves_spectral_dimdim=None@enforces_domain(DIMS.x)Checklist
config.py: addSPECTRAL_DIMS = frozenset({DIMS.frequency, DIMS.chemical_shift})utils.py: add_resolve_spectral_dim()helpervalidation.py: add@resolves_spectral_dimdecoratorvalidation.py: add@enforces_domain()decoratorto_ppm()/to_hz(): no-op guard, always recompute, nodimparamdim: str = DIMS.frequency→dim: str | None = None