Tied Parameters Break in Compound Models Due to Name Mangling
Description
When using astropy.modeling, it’s possible to tie parameters within a custom model using lambda functions, e.g. param2.tied = lambda m: 0.5 * m.param1. This works as expected when the model is standalone.
However, if the same model is added to another (e.g., forming a compound model with model1 + model2), the parameter names are internally renamed (e.g., param1 → model_0_param1), and the original tie function no longer works, because it refers to attributes (m.param1) that no longer exist on the compound model.
Expected behavior
In complex models with many parameters it would be great to be able to add and remove components without every time adjusting the links. Is there a way to easily link (tie) parameters keeping the possibility to change the compound model later? Is the lambda approach the only one possible?
How to Reproduce
import numpy as np
from astropy.modeling import Fittable1DModel, Parameter, models, fitting
# --- Custom model with two tied amplitudes ---
class TwoGaussians(Fittable1DModel):
amp1 = Parameter(default=1.0)
amp2 = Parameter(default=0.5)
mean1 = Parameter(default=0.0)
mean2 = Parameter(default=2.0)
stddev1 = Parameter(default=1.0)
stddev2 = Parameter(default=1.0)
def evaluate(self, x, amp1, amp2, mean1, mean2, stddev1, stddev2):
g1 = amp1 * np.exp(-0.5 * ((x - mean1) / stddev1)**2)
g2 = amp2 * np.exp(-0.5 * ((x - mean2) / stddev2)**2)
return g1 + g2
# Create data
x = np.linspace(-5, 5, 100)
true_model = TwoGaussians(amp1=2.0, amp2=1.0, mean1=0.0, mean2=2.0, stddev1=0.5, stddev2=0.5)
y = true_model(x) + 0.1 * np.random.normal(size=len(x))
# --- First: tied parameters, works! ---
model = TwoGaussians()
model.amp2.tied = lambda m: 0.5 * m.amp1 # Tie works in standalone model
fitter = fitting.LevMarLSQFitter()
fit1 = fitter(model, x, y)
print("Standalone model fit:")
print(f"amp1 = {fit1.amp1.value:.3f}, amp2 = {fit1.amp2.value:.3f} (should be half of amp1)")
# --- Now: Add a constant model, tie breaks ---
compound = model + models.Const1D(amplitude=0.0)
# # Need to reset the tied parameter in the compound model to make it work!
# compound.amp2_0.tied = lambda m: 0.5 * m.amp1_0
try:
fit2 = fitter(compound, x, y)
print("\nCompound model fit:")
print(f"amp1 = {fit2.amp1_0.value:.3f}, amp2 = {fit2.amp2_0.value:.3f}")
except Exception as e:
print("\nCompound model fit failed with error:")
raise e
Standalone model fit:
amp1 = 1.986, amp2 = 0.993 (should be half of amp1)
Compound model fit failed with error:
Attribute "amp1" not found
Versions
No response
@astropy/modeling , can you please advise? Thanks! 🙏
CompoundModel overrides __getitem__ to allow lookup of submodels by name, so it's possible to give each model a unique name and then adjust the lambda to resolve the desired attribute(s) by name from a closure where the model is detected to be within a CompoundModel.
For example, a minimally-adjusted version of @pier-astro 's example above is:
import numpy as np
from astropy.modeling import CompoundModel, Fittable1DModel, Parameter, models, fitting
# --- Custom model with two tied amplitudes ---
class TwoGaussians(Fittable1DModel):
amp1 = Parameter(default=1.0)
amp2 = Parameter(default=0.5)
mean1 = Parameter(default=0.0)
mean2 = Parameter(default=2.0)
stddev1 = Parameter(default=1.0)
stddev2 = Parameter(default=1.0)
def evaluate(self, x, amp1, amp2, mean1, mean2, stddev1, stddev2):
g1 = amp1 * np.exp(-0.5 * ((x - mean1) / stddev1)**2)
g2 = amp2 * np.exp(-0.5 * ((x - mean2) / stddev2)**2)
return g1 + g2
# Create data
x = np.linspace(-5, 5, 100)
true_model = TwoGaussians(amp1=2.0, amp2=1.0, mean1=0.0, mean2=2.0, stddev1=0.5, stddev2=0.5)
y = true_model(x) + 0.1 * np.random.normal(size=len(x))
# Tie now resolved by name when submodel in CompoundModel instance
model = TwoGaussians(name="FirstModel")
model.amp2.tied = lambda m, name=model.name: 0.5 * (m[name].amp1 if isinstance(m, CompoundModel) else m.amp1)
fitter = fitting.LevMarLSQFitter()
fit1 = fitter(model, x, y)
print("Standalone model fit:")
print(f"amp1 = {fit1.amp1.value:.3f}, amp2 = {fit1.amp2.value:.3f} (should be half of amp1)")
# Model-specific references now preserved in compound
compound = model + models.Const1D(amplitude=0.0)
try:
fit2 = fitter(compound, x, y)
print("\nCompound model fit:")
print(f"amp1 = {fit2.amp1_0.value:.3f}, amp2 = {fit2.amp2_0.value:.3f}")
except Exception as e:
print("\nCompound model fit failed with error:")
raise e
Note the default value argument to the lambda to capture the relevant model name.
This now gives the expected output, e.g...
Standalone model fit:
amp1 = 2.054, amp2 = 1.027 (should be half of amp1)
Compound model fit:
amp1 = 2.050, amp2 = 1.025
The if-else embedded in the lambda is somewhat unaesthetic, but this approach could be expanded to a decorator or some equivalent and applied to standalone functions to automatically handle whether to lookup attrs directly or via model name from the compound.