``CompoundModel.n_left_params` counts parameter values, not parameters, so array-valued parameters and model sets break `evaluate()` and every `parallel_fit_dask` fit · Issue #20554 · astropy/astropy · GitHub
Skip to content

`CompoundModel.n_left_params counts parameter values, not parameters, so array-valued parameters and model sets break evaluate() and every parallel_fit_dask fit #20554

Open

Description

@nabobalis

Description

In my glorious attempt to make the lives of my users a nightmare, I have come up with fixed compound models for them to use as starting points for their spectral fitting.

This means having parameters with array and I hit this bug.

parallel_fit_dask documents that a model's parameters may be arrays shaped like the iterating axes, so each spectrum gets its own start. With a compound model such as Const1D + Gaussian1D, every fit raises TypeError: Const1D.evaluate() takes 2 positional arguments but 5 were given and every parameter comes back NaN. The same error hits any compound model with array-valued parameters or a model set (n_models > 1) whenever evaluate(*inputs, *parameters) is called, which is what the fitters do.

Expected behavior

n_left_params should be the number of parameters of the left model, len(self.left.param_names), regardless of the shape of their values.

Suggested patch:

--- a/astropy/modeling/core.py
+++ b/astropy/modeling/core.py
@@ CompoundModel.__init__ @@
-        self.n_left_params = len(self.left.parameters)
+        self.n_left_params = len(self.left.param_names)

How to Reproduce

LLM code example warning

import tempfile
from pathlib import Path

import numpy as np

from astropy.modeling import models
from astropy.modeling.fitting import TRFLSQFitter, parallel_fit_dask

scalar = models.Const1D(1.0) + models.Gaussian1D(1.0, 0.0, 1.0)
array = models.Const1D(np.ones((3, 4))) + models.Gaussian1D(np.ones((3, 4)), np.zeros((3, 4)), np.ones((3, 4)))
print("scalar parameters: n_left_params =", scalar.n_left_params)
print("(3, 4) parameters: n_left_params =", array.n_left_params, "for", len(array.left.param_names), "parameter")

# __call__ works; evaluate(*inputs, *parameters), which the fitters use, does not.
x = np.linspace(-1, 1, 5)
print("array(x).shape =", array(x[:, None, None]).shape)
try:
    array.evaluate(x[:, None, None], *(getattr(array, name).value for name in array.param_names))
except TypeError as error:
    print("array.evaluate(...) ->", error)

# Model sets are affected too.
model_set = models.Const1D([1.0, 2.0], n_models=2) + models.Gaussian1D([1.0, 1.0], [0.0, 0.0], [1.0, 1.0], n_models=2)
try:
    model_set.evaluate(x[:, None], *(getattr(model_set, name).value for name in model_set.param_names))
except TypeError as error:
    print("model_set.evaluate(...) ->", error)

# parallel_fit_dask with one start per pixel: every fit raises and comes back NaN.
rng = np.random.default_rng(0)
xs = np.linspace(-1, 1, 50)
data = 2 + 5 * np.exp(-0.5 * ((xs - rng.normal(0, 0.05, (3, 4))[..., None]) / 0.2) ** 2) + rng.normal(0, 0.1, (3, 4, 50))
start = models.Const1D(np.ones((3, 4))) + models.Gaussian1D(np.full((3, 4), 4.0), np.zeros((3, 4)), np.full((3, 4), 0.3))
with tempfile.TemporaryDirectory() as path:
    fitted = parallel_fit_dask(model=start, fitter=TRFLSQFitter(), data=data, world=(xs,), fitting_axes=2,
                               scheduler="single-threaded", diagnostics="error", diagnostics_path=path)
    log = sorted(Path(path).rglob("error.log"))[0].read_text().strip().splitlines()[-1]
print(f"parallel_fit_dask: {np.isnan(fitted.mean_1.value).sum()} of 12 fits NaN; error.log ends with: {log}")

# The proposed fix, applied by hand to this instance:
start.n_left_params = len(start.left.param_names)
fitted = parallel_fit_dask(model=start, fitter=TRFLSQFitter(), data=data, world=(xs,), fitting_axes=2, scheduler="single-threaded")
print(f"with n_left_params = len(left.param_names): {np.isnan(fitted.mean_1.value).sum()} of 12 fits NaN")

Output:

scalar parameters: n_left_params = 1
(3, 4) parameters: n_left_params = 12 for 1 parameter
array(x).shape = (5, 3, 4)
array.evaluate(...) -> Const1D.evaluate() takes 2 positional arguments but 5 were given
model_set.evaluate(...) -> Const1D.evaluate() takes 2 positional arguments but 3 were given
parallel_fit_dask: 12 of 12 fits NaN; error.log ends with: TypeError: Const1D.evaluate() takes 2 positional arguments but 5 were given
with n_left_params = len(left.param_names): 0 of 12 fits NaN

Versions

platform.platform() = 'Linux-7.2.9-cachyos1.fc44.x86_64-x86_64-with-glibc2.43'
platform.python_version() = '3.14.5'

astropy              8.1.0.dev684+g2b5b20b6d  (also reproduced on 8.0.1)
numpy                2.4.6
scipy                1.17.0
pyerfa               2.0.1.5
dask                 2026.1.2

Activity

  1. changed the title [-]CompoundModel.n_left_params counts parameter values, not parameters, so array-valued parameters and model sets break evaluate() and every parallel_fit_dask fit[/-] [+]``CompoundModel.n_left_params` counts parameter values, not parameters, so array-valued parameters and model sets break `evaluate()` and every `parallel_fit_dask` fit[/+] on Oct 6, 2026
  2. added a commit that references this issue on Oct 15, 2026
    d8474a1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions