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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
This file is maintained using the shared changelog workflow in
[`docs/ai/changelog.md`](docs/ai/changelog.md).

## [Unreleased]

### Changed

- **Breaking: IRF convolution is now a quadrature-weighted kernel-matrix operator** (`conv_matrix_operator` / `conv_matrix_apply` in `utils/arrays.py`), replacing the sampled-1D-kernel convolution (`my_conv` + per-theta kernel support) on both the mcp and compiled (GIR) evaluation paths. Consequences:
- **Non-uniform time axes are convolved correctly.** The axis enters through the time-difference matrix and trapezoid quadrature weights, so measured fine-around-t0 + coarse-tail delay axes no longer produce silently biased fits (the old sample-index convolution deviated by up to ~7% of the trace maximum on example 21's stepped axis). Uniform axes agree with the previous path within its documented kernel-truncation tolerance.
- **Exact edge handling for any kernel width**: kernel mass beyond the time window contributes through the edge samples (edge-value padding semantics), computed analytically per kernel via edge-mass companions (`functions/time.py::CONV_EDGE_MASS`, cancellation-safe erfc/exp/clip tail forms). No support truncation at any width or axis spacing; a kernel without a registered companion is rejected at model validation (mcp) and scheduling (GIR).
- **Kernel parameters must be strictly positive**: widths/timescales with a non-positive initial value or lower bound — or varying without an explicit lower bound — are rejected at model load with a clear error (previously a fit could propose width = 0 and fail mid-fit). The edge-mass companions additionally reject nonpositive/non-finite parameter values at evaluation time, as a backstop for expression-driven kernel parameters (`boxCONV` with width 0 would otherwise silently become the identity operator). Example/test model YAMLs updated from `0` to `1.0E-6` lower bounds.
- **Non-monotonic time axes are rejected** at model construction with a clear error (previously accepted and silently misconvolved).
- **A kernel far narrower than the local step now degrades to identity** instead of raising a zero-sum-kernel error (the dt=0 diagonal keeps every row sum positive).
- **Static array shapes per evaluation**: the theta-independent operator — interior-only `(n_t, n_t)` deduplicated dt matrix, true trapezoid weights, edge-mass abscissae — is precomputed once per axis (`ScheduledPlan2D.conv_operator`, numeric kernel ids only; cached on the mcp `Component`), removing the theta-dependent kernel-shape blocker for the JAX track and the SciPy convolution utilities from the lowered conv path (the Gaussian edge masses still use `scipy.special.erfc`, which has a `jax.scipy.special` equivalent).
- **API removals**: `conv_kernel_support`, `Component.create_t_kernel`, and the `*_kernel_width` registry helpers are gone; kernel functions must be elementwise in their first argument and ship a `CONV_EDGE_MASS` companion. Conv components keep the model time axis instead of a private kernel-support axis.
- Example 21's data was regenerated through the new operator (the old operator's artifact was baked into the CSVs).

### Fixed

- Function-name discovery (`config/functions.py`) now only accepts functions defined in the `functions/` modules themselves. Imported helpers (`erf`, `erfc`, `wofz`, `Callable`) no longer leak into `all_functions()` / `time_functions()`, so a malformed YAML component naming one of them fails function-name validation upfront instead of erroring mid-evaluation.

### Removed

- **Breaking: `voigtCONV` and `lorentzCONV` convolution kernels.** Neither has a physical basis as a *time-domain* instrument response (Voigt and Lorentzian are energy-domain lineshape profiles, which the energy layer provides); no example used them. Voigt additionally has no closed-form CDF, which blocked the exact analytic edge handling above, and its removal retires `scipy.special.wofz` from `functions/time.py`. Remaining kernels: `gaussCONV`, `expSymCONV`, `expDecayCONV`, `expRiseCONV`, `boxCONV`. Model YAMLs referencing the removed kernels fail validation with an unknown-function error.

## [0.10.2] - 2026-07-10

### Added
Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
## Fitting

- [ ] **Enable 1D time-trace fitting (post-SbS kinetics)**: wire standalone `TIME_1D` dynamics models (already evaluable on the mcp path per `docs/design/supported_models.md`, but connected to no fit method) to a fit entry point, so parameter-vs-time traces extracted from SbS results can be fit to `functions/time.py` dynamics (`expFun` sums, IRF convolution) inside the package instead of in external scripts. Frame it as the diagnostic/initialization rung before `fit_2d`, not a statistical equivalent (per-slice correlations and unpropagated SbS uncertainties make two-step inferior). Design direction: promote SbS results into a time-axis `File` — the promoted object inherits limits/CI/MCMC/save/export machinery, and trace fits land in a `SavedFitSlot` like every other fit type (no parallel fitting pipeline). Requires `File` to support time as the primary axis, which today it assumes is energy. Weighting traces by per-slice stderr ties into the `sigma_type` expansion item below.
- [ ] **Kernel-matrix convolution (non-uniform time axes)**: replace the 1D-kernel convolution (`my_conv` + per-theta `conv_kernel_support`) with a quadrature-weighted kernel-matrix operator on both the mcp and GIR paths (one branch — parity tests couple them). Fixes silently wrong IRF convolution on non-uniform time axes (measured 2026-07-10 on example 21's 0.5→2.0 step axis: deviations up to ~7% of trace max vs the exact continuous convolution, worst just past the step change; the shipped example round-trips only because its data was generated through the same operator) and removes the theta-dependent kernel-shape jit blocker for the JAX track. One shared helper serves standalone 1D time traces and dynamics traces inside 2D models (single mcp conv site in `Model._combine_component`, plus two GIR sites), so it also enables the 1D time-trace fitting item above on measured delay axes. Requires regenerating example 21's data and one golden-value update. Plan: [docs/design/kernel-matrix-convolution.md](docs/design/kernel-matrix-convolution.md). Sequencing: after `fix-conv-kernels` merges, before the JAX backend item below.


## Noise and simulation

Expand Down
48 changes: 39 additions & 9 deletions docs/ai/add-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This guide covers both:

- Format: `<function_name> <module>`
- `function_name`: the Python function name
(e.g. `GLP`, `expFun`, `pGauss`, `lorentzCONV`)
(e.g. `GLP`, `expFun`, `pGauss`, `gaussCONV`)
- `module`: one of `energy`, `time`, `profile`

## Module mapping
Expand Down Expand Up @@ -53,8 +53,22 @@ Read the source file and find the function. Verify:
- [ ] `#\n` before function definition
- [ ] No underscores in function name (enforced by guard test)
- [ ] Profile functions start with `p` prefix
- [ ] Convolution kernels end with `CONV` suffix and have a companion
`<name>_kernel_width()` function returning an int
- [ ] Convolution kernels end with `CONV` suffix and are elementwise in
their first argument (they are evaluated on the 2D dt matrix of
the kernel-matrix convolution operator)
- [ ] Convolution kernels have a private edge-mass companion
`_<name>_edge_mass(dt_left, dt_right, *params)` registered in
`CONV_EDGE_MASS` (in `functions/time.py`). It must return the
exact analytic integrals of the kernel body — *including its
normalization* — over `(-inf, t[0]]` and `[t[-1], inf)`, using
cancellation-safe tail forms (erfc / direct exp / clip, never
`G(inf) - G(x)` CDF differences). Companions must validate their
parameters via `_validate_kernel_par` (strictly positive, finite)
— they are the runtime backstop for expression-driven kernel
parameters that bypass model-load bound checks. A kernel without
a companion fails model validation (mcp) and scheduling (GIR).
Kernels whose body has no closed-form antiderivative do not fit
this contract (this is why `voigtCONV` was removed).

Note: these functions do NOT use `*` for keyword-only args because the
framework calls them via `self.fct(x, **parameters)`.
Expand All @@ -73,7 +87,12 @@ Registration notes:
- Background functions are manually listed in
`src/trspecfit/config/functions.py::background_functions()`.
- Convolution kernels are discovered dynamically by their `CONV` suffix in
`config/functions.py`; they do not require a separate manual registry there.
`config/functions.py` (no manual registry there), but additionally
require two manual entries: the `CONV_EDGE_MASS` companion registry in
`functions/time.py` (mcp path; missing entry fails model validation)
and, for GIR support, `CONV_EDGE_MASS_DISPATCH` in `eval_2d.py`
alongside `CONV_KERNEL_DISPATCH` (missing entry fails scheduling).
A completeness test asserts kernels and companions stay in lockstep.

Module-specific guidance:

Expand Down Expand Up @@ -140,12 +159,17 @@ Convolution kernels:
- [ ] Add the kernel to the convolution enum/name mapping in
`src/trspecfit/graph_ir.py`.
- [ ] Add runtime dispatch in `src/trspecfit/eval_2d.py`
(`CONV_KERNEL_DISPATCH`).
- [ ] Verify kernel-width handling still works with the new kernel.
(`CONV_KERNEL_DISPATCH` **and** `CONV_EDGE_MASS_DISPATCH` — the
edge-mass companion, keyed by the same enum; scheduling fails
without it).
- [ ] Verify the kernel is elementwise in its first argument (it is
evaluated on the deduplicated dt values; no shape assumptions, no
normalization requirements — the operator row-normalizes; the
edge-mass companion must match the body's normalization though).
- [ ] Verify unsupported kernels still fall back cleanly to MCP.
- [ ] If lowering fails, recommend a kernel signature and behavior compatible
with the current compiled path (pure kernel function, explicit parameter
list, companion `<name>_kernel_width(...)`, no hidden state).
with the current compiled path (pure elementwise kernel function,
explicit parameter list, no hidden state).

Profile functions:

Expand Down Expand Up @@ -245,9 +269,15 @@ test_half_max_at_half_width -- FWHM check using independent property, NOT formul
test_zero_for_negative_x -- (causal kernels only: expDecayCONV)
test_zero_for_positive_x -- (anti-causal kernels only: expRiseCONV)
test_decays_monotonically -- monotonic decay away from peak
test_kernel_width_positive -- companion kernel_width() > 0
```

Additionally, register the kernel in `_KERNEL_TEST_PARAMS` in
`tests/test_functions_convolution.py`: the parametrized
`TestConvEdgeMassCompanions` suite iterates `CONV_EDGE_MASS` and checks
every companion against `scipy.integrate.quad` of the kernel body, plus
registry/dispatch completeness — verify the new kernel is picked up
(the sync test fails until the params entry exists).

**Profile functions**:

```text
Expand Down
Loading