Holding a Parameter Fixed¶
Synthetic example
The fixture below is a single seeded peak with one parameter's "known"
value baked in by construction — chosen to demonstrate the vary=False
mechanism, not a sweep proving fixed-parameter fits behave well across
real independently-measured calibration scenarios.
Quick example¶
You already know a parameter's value from an independent measurement — a
background level read off a blank/dark scan, or an instrument-broadening
\(\sigma\) from a calibration standard — and want to hold it fixed instead of
asking this same noisy dataset to estimate it too. Set vary=False on that
Parameter, with no expr.
def synthesize_data() -> tuple[np.ndarray, np.ndarray]:
"""Synthesize a Gaussian peak on top of ``TRUE_BACKGROUND`` (with noise)."""
rng = np.random.default_rng(11)
x = np.linspace(-3, 3, 120)
peak = 2.0 * np.exp(-0.5 * ((x - 0.5) / 0.7) ** 2)
noise = rng.normal(0, 0.05, len(x))
y = peak + TRUE_BACKGROUND + noise
return x, y
x, y = synthesize_data()
TRUE_BACKGROUND stands in for a level measured independently of this
dataset — known exactly, not something to estimate from the same noisy
points. Build two graphs against the same data: one pins bg.c at that
known value, the other leaves it free to compare against.
def build_graph(*, fix_background: bool) -> FitGraph:
"""One Gaussian peak + one constant background node.
When ``fix_background`` is True, ``bg.c`` is constructed with
``value=TRUE_BACKGROUND, vary=False`` — pinned to the known value and
excluded from the optimizer's free set for the whole fit. When False, it
starts from the same value but is left ``vary=True`` (the default) and
must be estimated from this noisy data like every other free parameter.
"""
return FitGraph(
nodes=[
ModelNodeSpec(
id="peak",
model_type=ModelType.GAUSSIAN,
parameters={
"amplitude": Parameter(value=1.5),
"center": Parameter(value=0.0),
"sigma": Parameter(value=0.5, min=1e-3),
},
),
ModelNodeSpec(
id="bg",
model_type=ModelType.CONSTANT,
parameters={
"c": Parameter(value=TRUE_BACKGROUND, vary=not fix_background),
},
),
],
)
Both graphs are fit against the identical (x, y) — the only difference
between the two calls below is bg.c's vary flag.
def fit_both(x: np.ndarray, y: np.ndarray) -> dict[str, FitResult]:
"""Fit the same noisy data with ``bg.c`` fixed, then again with it free."""
data = MeasurementData(x=x.tolist(), y=y.tolist())
return {
"fixed": fit(build_graph(fix_background=True), data),
"free": fit(build_graph(fix_background=False), data),
}
results = fit_both(x, y)
fixed_result, free_result = results["fixed"], results["free"]
Two facts are guaranteed by the mechanism itself, not just this one seeded
example: fixing bg.c removes it from the free-parameter vector, so
dof = n_points - n_free is exactly one higher in the fixed run, and its
stderr is None there since a fixed parameter was never part of the
optimization the covariance matrix describes.
def compare(fixed_result: FitResult, free_result: FitResult) -> None:
"""Report dof, per-parameter stderr, and the fixed parameter's absent stderr.
Two facts are *guaranteed* and asserted below: fixing ``bg.c`` removes it
from the free-parameter vector, so ``dof = n_points - n_free`` is exactly
one higher in the fixed model; and ``ParameterResult.stderr`` for ``bg.c``
is ``None`` in the fixed run, since a fixed parameter was never part of
the optimization the covariance matrix describes. The peak parameters'
stderr being *tighter* in the fixed run (printed below, not hard-asserted
beyond a non-strict bound) is the expected direction for a correctly-known
fixed value — removing a genuinely-correlated nuisance parameter from the
free set cannot increase the remaining parameters' Fisher information —
but the exact margin is specific to this one seeded example, not a
general accuracy claim.
"""
print(f"{'':10s} {'dof':>5s} {'amplitude stderr':>18s} {'sigma stderr':>14s}")
for name, result in (("fixed", fixed_result), ("free", free_result)):
amp_stderr = result.parameters["peak.amplitude"].stderr
sigma_stderr = result.parameters["peak.sigma"].stderr
assert amp_stderr is not None
assert sigma_stderr is not None
print(f"{name:10s} {result.dof:5d} {amp_stderr:18.5f} {sigma_stderr:14.5f}")
bg_fixed_stderr = fixed_result.parameters["bg.c"].stderr
bg_free_stderr = free_result.parameters["bg.c"].stderr
print(f"\nbg.c stderr — fixed: {bg_fixed_stderr!r}, free: {bg_free_stderr!r}")
assert fixed_result.success
assert free_result.success
assert fixed_result.dof == free_result.dof + 1, (
"fixing bg.c removes it from the free set, raising dof by exactly 1"
)
assert bg_fixed_stderr is None, (
"a vary=False parameter is never in the optimization vector, so its stderr is not estimable"
)
assert bg_free_stderr is not None
fixed_amp_stderr = fixed_result.parameters["peak.amplitude"].stderr
free_amp_stderr = free_result.parameters["peak.amplitude"].stderr
assert fixed_amp_stderr is not None
assert free_amp_stderr is not None
assert fixed_amp_stderr <= free_amp_stderr * 1.05, (
"a correctly-known fixed background should not noticeably widen the "
"peak amplitude's uncertainty relative to fitting it"
)
compare(fixed_result, free_result)
Why this works¶
Distinct from shared_params.md: that page ties one parameter's value
to another fitted parameter via expr. This example fixes a parameter to
a known constant, with no expr involved at all. Parameter's own
docstring calls out vary as "whether the solver may adjust this
parameter" — vary=False excludes it from the free set entirely for the
whole fit, regardless of how close or far its constructed value is from
any other parameter.
What just happened¶
-
Data creation — a single Gaussian peak on top of a background level (
TRUE_BACKGROUND = 0.35) known exactly, as if measured from a blank scan. -
Two graphs, one dataset —
build_graph(fix_background=True)constructsbg.casParameter(value=TRUE_BACKGROUND, vary=False); the free variant uses the same starting value withvary=True(the default). -
dof differs by exactly one — fixing
bg.cremoves it from the optimizer's free set, so the fixed run'sdofis one higher than the free run's. -
stderris absent for the fixed parameter —ParameterResult.stderrforbg.cisNonein the fixed run, since it was never part of the covariance matrix the solver estimated. -
Tighter peak-parameter uncertainty — with a correctly-known fixed background, the peak's
amplitude/sigmastderrcome out tighter than the free run's on this example, matching the expected direction (removing a genuinely-correlated nuisance parameter from the free set cannot increase the remaining parameters' Fisher information) — the exact margin is specific to this one seeded example, not a general accuracy claim.
See also¶
- Related examples:
shared_params.md(tying a parameter to another fitted parameter viaexpr, not a constant),confidence_intervals.md(turningstderrinto a reported confidence interval). - Reference:
python/spectrafit_core/parameters.py(Parameter.varydocstring). - API docs:
Parameter,ParameterResult,FitGraph,FitResult.
