Skip to content

Make models declare their inputs, so that least_squares() and evaluate() can check x. - #9

Merged
roytsmart merged 2 commits into
mainfrom
feature/model-inputs
Oct 20, 2026
Merged

roytsmart merged 2 commits into
mainfrom
feature/model-inputs

Conversation

@roytsmart

@roytsmart roytsmart commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Models now name their inputs, the columns of x, the way they name their parameters:

@ndoptimize.model(parameters=("amplitude", "center", "width"), inputs=("lower", "upper"))
def gaussian_binned(x, parameters, f, jacobian):
    ...

inputs is required, so this is a breaking change, meant for 0.2.0.

Why

A compiled model doesn't check its indices. least_squares() checks the guess against the model's parameters, but it had nothing to check x against, so a model that reads more columns than x has read past the end of each row with no error. Fitting gaussian_binned to one column of bin centers, for example, took the next point as the upper edge of each bin, and read past the end of the array at the last one.

named-arrays needs the count too (sun-data/named-arrays#271). Along an axis where the inputs of a function array are bin edges, na.ModelFitFunctionArray gives the model the lower and upper edge of each bin. With model.inputs, it can reject a one-input model like gaussian there, instead of silently fitting it to the lower edges.

Changes

  • ndoptimize.model(parameters, *, inputs, cache=True): inputs is a sequence of unique names, checked like parameters. Model.inputs holds them, and the repr shows them. inputs and cache are keyword-only, so the inputs can't be swapped with the parameters, which are also a sequence of names.
  • The names of the parameters and of the inputs must be a sequence of strings, such as a tuple. A set, whose order changes from one process to the next, now raises a TypeError, as do bytes and names that aren't strings. A Model made directly checks its names too, not only one made with model().
  • least_squares() and evaluate() raise a ValueError naming the model's inputs when the last axis of x doesn't match them, before any compiled code runs. Calling a model directly checks it too. One Model method holds the checks of x and of the parameters for all three, and the error from calling a model names its inputs and parameters.
  • The common models declare their inputs: gaussian, gaussian_linear and two_gaussians_linear take ("x",), and gaussian_binned takes ("lower", "upper"). Their docstrings give the shape of x as (num_points, 1) or (num_points, 2), instead of reading "the first component of x".
  • The README and the documentation show inputs in the model example, and say that x has one column per input.

Migrating from 0.1

  • Add inputs= to every @ndoptimize.model(...), with one name for each column of x the model reads. A model that read only the first column of a wider x now raises an error until the extra columns are dropped.
  • Pass cache by keyword.
  • Give the names as a tuple or a list of strings, not a set.

Tests

  • A bare string, bytes, a set, a name that isn't a string, None or a repeated name, in parameters or in inputs, raises, both through model() and when a Model is made directly. Passing inputs by position raises. model.inputs and the repr hold the names.
  • x with the wrong number of columns raises in Model.__call__, least_squares() and evaluate(), including gaussian_binned given a single column in all three.
  • The inputs of each common model.

82 tests pass with and without JIT, and every changed line is covered. ruff, black and pyright 1.1.411 are clean, and the documentation builds without warnings, with the model examples showing their inputs.

The named-arrays side, which checks the number of inputs it gives a model and points a one-input model given bin edges to a binned one, passes its tests against this branch. It will go into #271 once this is released.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9

…uate()` can check `x`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.65%. Comparing base (9d65a38) to head (6f9ef4a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main       #9   +/-   ##
=======================================
  Coverage   99.65%   99.65%           
=======================================
  Files           8        8           
  Lines         861      881   +20     
=======================================
+ Hits          858      878   +20     
  Misses          3        3           
Flag Coverage Δ
unittests 99.65% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…d, and check `x` and the parameters in one `Model` method.

- The names of the parameters and inputs must be a sequence of strings,
  so a set, whose order changes from one process to the next, bytes or
  `None` raise a `TypeError`. `Model.__post_init__` checks them too, so a
  model made directly is checked like one made with `model()`.
- `inputs` and `cache` are keyword-only, so the inputs cannot be mistaken
  for the parameters, and 0.1 code which passed `cache` by position gets
  a clear error.
- `Model._check_shapes()` checks the last axes of `x` and of the
  parameters against the model for `Model.__call__`, `least_squares()`
  and `evaluate()`, instead of three copies. The error from calling a
  model names its inputs and parameters.
- `test_model_shapes` calls `gaussian_binned` with one column, and
  `test_model_invalid` makes each invalid model both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
@roytsmart
roytsmart merged commit b887763 into main Oct 20, 2026
18 checks passed
@roytsmart
roytsmart deleted the feature/model-inputs branch October 20, 2026 23:33
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