Skip to content

fix conv kernel support truncation... - #34

Merged
InfinityMonkeyAtWork merged 36 commits into
mainfrom
fix-conv-kernels
Jul 11, 2026
Merged

fix conv kernel support truncation...#34
InfinityMonkeyAtWork merged 36 commits into
mainfrom
fix-conv-kernels

Conversation

@InfinityMonkeyAtWork

Copy link
Copy Markdown
Owner

... by rebuilding from current parameter values

The kernel time axis was built once at model construction from the
initial width (±4·SD_init for gaussCONV) and never rebuilt, silently
truncating the kernel and biasing the fitted width once it grew past
its init. Both eval paths now rebuild the support per evaluation via a
shared conv_kernel_support() helper (symmetric, odd-length, guarded):
the mcp path in Component.value, the GIR path in the eval_2d conv step
and the schedule_2d precompute. The frozen conv_support_* plan arrays
and kernel_time node snapshots are removed.

Adds a deterministic regression test (fit init 16x below truth,
verified to fail on the old code), a grown-width GIR/mcp parity test,
and support-builder unit tests. Drops the "init conv widths generously"
workaround guidance from docs and example 04. Benchmarked: no
performance change on examples 01/04.

…ter values

  The kernel time axis was built once at model construction from the
  initial width (±4·SD_init for gaussCONV) and never rebuilt, silently
  truncating the kernel and biasing the fitted width once it grew past
  its init. Both eval paths now rebuild the support per evaluation via a
  shared conv_kernel_support() helper (symmetric, odd-length, guarded):
  the mcp path in Component.value, the GIR path in the eval_2d conv step
  and the schedule_2d precompute. The frozen conv_support_* plan arrays
  and kernel_time node snapshots are removed.

  Adds a deterministic regression test (fit init 16x below truth,
  verified to fail on the old code), a grown-width GIR/mcp parity test,
  and support-builder unit tests. Drops the "init conv widths generously"
  workaround guidance from docs and example 04. Benchmarked: no
  performance change on examples 01/04.
  The MonoExpPosIRF gaussCONV SD (5e-2) was ~10x below the shared test
  time step, so the kernel was numerically a delta and chi² was bit-flat
  in SD: the F4 roundtrip fit landed wherever optimizer round-off left
  it, which differs across scipy/lmfit versions and machines (failed on
  min-versions CI only, deterministically). The old frozen-support code
  had masked this with a spurious gradient from its asymmetric kernel.

  Raise MonoExpPosIRF SD to 0.4 (~0.8*dt): identifiable (exact recovery
  from 0.5x/2x starts, r(SD,A)=0.47) without burying the expFun dynamics
  (peak keeps 77%). Keep the sub-sample variant as MonoExpPosIRFNarrow
  for kernel-support tests that need a far-below-truth fit init.
The SNR plot title called get_snr() before the data_clean auto-simulate
guard, so plot_comparison without a prior simulate_1d/2d raised
ValueError instead of simulating first. Hoist the auto-simulate above
the title construction (code-review-2026-07, FAIL 1/4).
The ignore_zeros propagation loop rolls the previous non-zero sign
through zeros, but all-zero input has no sign to propagate, so
'while sz.any()' never terminated. Skip propagation when asign has no
non-zero entries; the result is correctly all-zeros
(code-review-2026-07, FAIL 2/4).
my_conv computed x_arr[1] - x_arr[0] with no length guard, so a
single-element x raised a bare IndexError from the hot path. Guard with
a ValueError naming the precondition; benchmark unchanged
(code-review-2026-07, FAIL 3/4).
y_norm=1 normalized each trace by its own range, which is zero for a
constant trace, silently plotting all-NaN data. Map zero-range traces
to baseline 0 (a constant trace has no amplitude to normalize)
(code-review-2026-07, FAIL 4/4).
Model.create_value_2d(t_ind=[start, stop]) passed the loop index to
create_value_1d, which expects an absolute index into self.time. Any
partial-range evaluation of a time-dependent model therefore computed
the dynamics for t[0:stop-start] instead of t[start:stop]. Pass
t_start + ti and add a regression test comparing a partial-range
evaluation against the matching rows of the full evaluation.
…_limits

These File methods assigned index axes to self.energy/self.time as a
side effect when axes were missing. Since File(data=...) always creates
index axes itself, data without axes means the object was corrupted by
direct attribute assignment - raise a clear ValueError instead of
silently persisting fabricated axes from an inspection or setup call.
describe on a File with no data at all still warns and returns, since
an empty File is a normal lifecycle state.
The kernel step size comes from time[1] - time[0], which raised a bare
IndexError in the schedule_2d conv precompute (and would again in
evaluate_2d) for a 1-point axis. Validate once at scheduling when conv
steps are present. The mcp-layer guard in create_t_kernel already
caught this at model construction but blamed an undefined time axis;
give the too-short case its own message. Tests cover both layers.
Par.value printed a warning and returned -1.0 when t_vary was set but
no t_model was attached, silently poisoning every spectrum evaluated
from the corrupted parameter. Raise a RuntimeError naming the parameter
instead, matching the adjacent inconsistent-state guards.
NaN/Inf in the data surfaced as lmfit's generic error blaming "input
data or the output of your objective/model function", leaving the user
to figure out which. Check the fit-window slice once in fit_wrapper -
the choke point for all fit entry points - and raise a message with
the non-finite count and a pointer to set_fit_limits(). Data outside
the e_lim/t_lim window never reaches the residual and stays legal.
Factor the residual_fun window slicing into a shared helper, and add
test_fit_validation.py closing the check-17 gaps: NaN/Inf at the
public fit level and single-element energy/time axes through the
pipeline.
One themed pass over the silent-mode violations (review checks 3, 12):

- _load_config: any error other than a missing file was swallowed with
  a print gated on show_output, so a broken config silently fell back
  to defaults; raise ValueError instead.
- fit_wrapper MCMC: the emcee banner and progress bar ran
  unconditionally, and with show_output=0, save_output=0 the walker and
  corner figures reached plt.show() and were left open; gate the prints
  on show_output and close unsaved figures in silent mode.
- fit_2d / fit_slice_by_slice: time_display and display(params) ran
  whenever stages >= 1; gate on show_output like fit_baseline and
  fit_spectrum.
- define_baseline / set_fit_limits: plotted on their show_plot=True
  default without consulting Project.show_output; now suppressed in
  silent mode.
- eval_expr_program: push scalar constants and trace-row views instead
  of allocating per-instruction arrays; broadcast constant-only results
- profile sample/expr evaluation: write into preallocated buffers
  instead of broadcast_to().copy() and np.repeat temporaries
- profiled ops: vectorize over the aux axis instead of a per-aux Python
  loop (~4.5x faster on a profiled 2D model)
- my_conv: pad y directly (the padded x grid was built and discarded)
  and normalize the kernel instead of the padded signal
- parse notebook calls parenthesis-matched instead of per cell: the
  multi-call cell in example 04 replayed add_time_dependence with the
  wrong target_parameter (GLP_01_A instead of GLP_01_x0_pLinear_01_m)
- replay add_par_profile calls (before dynamics) and load
  data/aux_axis.csv; the profile example previously compiled with
  plan.n_aux == 0, never reaching the profiled-op path
- filter replayed calls to the benchmarked "2D" model and list
  attached profiles in the preamble
The aux vectorization only wins when profiled params enter the energy
function linearly (amplitude-only profiles keep the transcendentals at
(n_time, 1, n_energy)); with a profiled position it materializes full
(n_time, n_aux, n_energy) temporaries and measures ~60% slower on the
real example 04 workload (39 -> 64 ms/call), exposed by the fixed
benchmark harness. Param sources are still resolved once outside the
loop.
- compare-mode coverage for all lowerable energy shapes and all 7 IRF
  kernels; residual parity for the six non-exp dynamics functions
- new fixtures: profile_pGauss, chained-conv MonoExpPosDoubleIRF,
  pinned_gauss_offset + profile_pExpDecayFixed (constant profiled op)
- pipeline parity for multi-substep single-cycle dynamics
  (BiExpSharedT0) and chained convolution (pins n_conv_steps == 2)
- TIME_1D standalone dynamics: fit_model_gir falls back to MCP and
  matches at the residual level (can_lower_1d rejects the domain)
- constant profiled op folds into cached_result at plan build
  (compile-time branch previously reached by no fixture)
With show_output=0 and save_output=0 the figures were still built and
closed on every MCMC run; the silent-mode test now also asserts
corner.corner is never called (fails on the old code).
@InfinityMonkeyAtWork
InfinityMonkeyAtWork merged commit c462d70 into main Jul 11, 2026
4 checks passed
@InfinityMonkeyAtWork
InfinityMonkeyAtWork deleted the fix-conv-kernels branch July 11, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant