jaxsr.superposition#
Superposition: discover a symbolic transform that collapses a family of curves.
A large class of experiments produces a set of curves indexed by a condition, where the scientific claim is that some transform of the axes collapses them onto a single master curve. Rheology calls it time-temperature superposition; the same shape appears in time-concentration and time-moisture superposition, finite-size scaling, Larson-Miller creep master curves, and isoconversional kinetics.
The practitioner normally does two things by hand: shift each curve until the overlap
looks right, then assert a functional form (Arrhenius, WLF, a polynomial) for how the
shift depends on the condition. SuperpositionRegressor learns that second step
symbolically, from a sparse library of candidate shift laws.
The model#
With the abscissa in log units and a dimensionless condition coordinate \(q = (c - c_{ref}) / c_{ref}\),
where \(f\) is the unknown master curve, \(s\) the horizontal shift
(\(s = \log_{10} a_T\)), \(v\) an optional vertical shift, and \(\sigma\)
a sign fixed by the domain (+1 for a frequency-like abscissa, -1 for a
time-like one). Differentiating with respect to \(q\) eliminates the unknown
master curve entirely:
So a sparse regression of \(y_q\) against the structured blocks
\(\Theta(q) \odot y_x \,|\, \Theta(q)\) recovers \(s'\) and \(v'\) as
coefficient functions, which are integrated back with the anchor \(s(0) = 0\).
Both partials come from one smoothed surface per channel
(SurfaceDerivatives), never from finite differences of
noisy data.
Two findings shape this API#
The transform is identifiable; the equation is not. Over a realistic condition
window, \(1/(1+q)^2\) (Arrhenius), \(1/(c_2+q)^2\) (WLF) and low-order
polynomials span nearly the same function space. In a 90-fit synthetic study only 62%
of fits selected the true Arrhenius basis, yet all of the structural variants produced
the same transform to ~0.01 decades. SuperpositionRegressor.shift_expression_
is therefore not the headline result – the transform is, together with a
structure-independent physical summary such as
effective_activation_energy().
Expression stability is anti-correlated with validity. In the same study a negative
control (two relaxation groups with different activation energies, for which no scalar
shift factor exists) produced a perfectly stable shift law – 12/12 replicates agreed
on the structure – while the genuine case disagreed 38% of the time. Only collapse on
withheld conditions separated them: at the noise floor for the real case, eight times
the noise floor for the control. SuperpositionRegressor.validity_report_ is
consequently a graded verdict from leave-one-condition-out collapse, never a binary
claim read off a visual collapse.
Conventions#
These are the classic source of silent error in superposition work, so they are explicit and validated rather than assumed:
the abscissa is already in log units, and the domain sets the sign (
"frequency"gives \(z = x + s\),"time"gives \(z = x - s\));temperatures must be in kelvin before reciprocal features are built (
condition_scale="kelvin"checks this and refuses a Celsius-looking column);the regression runs in the dimensionless \(q\), so design-matrix columns stay order-one instead of mixing \(T^{-2}\) against polynomials;
the master-curve smoother is fitted separately from the derivative surface – reusing the surface that produced the shift law to also certify it would defeat the validation.
Examples
>>> from jaxsr.superposition import SuperpositionRegressor
>>> model = SuperpositionRegressor(
... condition="temperature", abscissa="log_omega", response="log_Gp",
... domain="frequency", candidate_families=("arrhenius", "wlf", "polynomial"),
... )
>>> model.fit(data)
>>> model.shift_factors([300.0, 320.0])
>>> print(model.validity_report_.verdict)
'supported'
- class jaxsr.superposition.MasterCurve(channel: Any, z: ndarray, y: ndarray, std: ndarray, z_min: float, z_max: float, n_points: int, smoother: SurfaceDerivatives)#
Bases:
objectA reconstructed master curve for one channel, with an uncertainty band.
Fitted by a smoother that is deliberately separate from the derivative surface used to discover the shift law – reusing that surface to also certify the collapse would let the discovery grade its own homework.
- Parameters:
channel (Any) – Channel label this curve belongs to.
z (np.ndarray of shape (n_grid,)) – Reduced-coordinate grid spanning the observed range.
y (np.ndarray of shape (n_grid,)) – Smoothed master curve on that grid.
std (np.ndarray of shape (n_grid,)) – Standard error of the smoothed value.
z_min (float) – Lowest reduced coordinate covered by data.
z_max (float) – Highest reduced coordinate covered by data.
n_points (int) – Number of observations that went into the curve.
smoother (SurfaceDerivatives) – The fitted 1-D smoother, for evaluation at arbitrary points.
- channel: Any#
- z: ndarray#
- y: ndarray#
- std: ndarray#
- z_min: float#
- z_max: float#
- n_points: int#
- smoother: SurfaceDerivatives#
- predict(z: ndarray, return_std: bool = False) ndarray | tuple[ndarray, ndarray]#
Evaluate the master curve.
- Parameters:
z (np.ndarray of shape (n_query,)) – Reduced coordinates. Values outside
[z_min, z_max]are extrapolations of the smoother and are not flagged here; usecovers().return_std (bool) – Also return the standard error of the smoothed value.
- Returns:
y (np.ndarray of shape (n_query,)) – Master-curve values.
std (np.ndarray of shape (n_query,)) – Standard errors. Only returned when return_std is True.
- covers(z: ndarray) ndarray#
Return a boolean mask of query points inside the curve’s observed range.
- Parameters:
z (np.ndarray of shape (n_query,)) – Reduced coordinates.
- Return type:
np.ndarray of bool, shape (n_query,)
- class jaxsr.superposition.ShiftTerm(name: str, deriv: Callable[[...], ndarray], antideriv: Callable[[...], ndarray], expression: str, complexity: int = 2, param_bounds: dict[str, tuple[float, float]] | None = None, log_scale: bool = False)#
Bases:
objectOne candidate term of a shift law, as a derivative/antiderivative pair.
The regression works on \(s'(q)\), but the reported transform needs \(s(q)\). Carrying both halves analytically keeps the integration exact and lets
SuperpositionRegressor.shift_expression_be a real symbolic expression rather than a quadrature table.- Parameters:
name (str) – Name of the derivative term as it appears in the design matrix, e.g.
"1/(1+q)^2". Parametric terms name their free parameters, e.g."1/(c2+q)^2".deriv (callable) –
deriv(q, **params) -> np.ndarray. Evaluates \(\theta_j(q)\), a candidate term of \(s'(q)\).antideriv (callable) –
antideriv(q, **params) -> np.ndarray. Evaluates \(\int_0^q \theta_j(u)\,du\), anchored so that the reference condition has zero shift.expression (str) – The antiderivative as a Python/sympy-parseable string in
q, with free parameters left as symbols.complexity (int) – Complexity score used for Pareto ranking.
param_bounds (dict, optional) –
{param_name: (lower, upper)}for a parametric term, or None.log_scale (bool) – Search the parameter in log space. Only meaningful with param_bounds.
Examples
>>> import numpy as np >>> term = ShiftTerm( ... name="1/(1+q)^2", ... deriv=lambda q: 1.0 / (1.0 + q) ** 2, ... antideriv=lambda q: q / (1.0 + q), ... expression="q/(1 + q)", ... ) >>> float(term.antideriv(np.array([0.0]))[0]) 0.0
- name: str#
- deriv: Callable[[...], ndarray]#
- antideriv: Callable[[...], ndarray]#
- expression: str#
- complexity: int = 2#
- param_bounds: dict[str, tuple[float, float]] | None = None#
- log_scale: bool = False#
- property is_parametric: bool#
Whether the term carries free nonlinear parameters.
- class jaxsr.superposition.SuperpositionRegressor(condition: str = 'condition', abscissa: str = 'x', response: str = 'y', channel: str | None = None, *, domain: str = 'frequency', condition_scale: str | None = 'kelvin', reference: float | None = None, vertical_shift: str = 'none', candidate_families: Sequence[str] = ('arrhenius', 'wlf', 'polynomial'), poly_degree: int = 2, wlf_c2_bounds: tuple[float, float] = (0.05, 5.0), max_terms: int = 3, selection: str = 'bic', strategy: str = 'exhaustive', smoother: str = 'tensor_spline', smoothing: float | str = 'auto', surface_degree: int = 3, weighting: str = 'derivative_se', log_base: float = 10.0, validation: str = 'loco', max_holdout_conditions: int | None = None, collapse_thresholds: tuple[float, float] = (2.0, 4.0), n_stability: int = 0, stability_resampling: str = 'residual', random_state: int | None = None)#
Bases:
objectDiscover a symbolic transform that collapses a family of curves.
Given a tidy table of
(condition, abscissa, response[, channel]), learn a sparse symbolic horizontal shift \(s(c)\) – and optionally a vertical shift \(v(c)\) – reconstruct the master curve, and test whether the collapse actually holds on conditions withheld from the fit.The abscissa must already be in log units. The shift is reported in those same units, so with a base-10 log abscissa the shift factors are \(\log_{10} a_T\) in decades.
- Parameters:
condition (str) – Name of the condition column (temperature, concentration, pressure, …).
abscissa (str) – Name of the log-abscissa column.
response (str) – Name of the response column.
channel (str, optional) – Name of the channel column. Channels (e.g.
G'andG'') share one horizontal shift but get their own master curve, and their own vertical shift whenvertical_shift="per_channel". If None the whole table is one channel, labelled with the response column name.domain (str) –
"frequency"for a frequency-like abscissa (\(z = x + s\)) or"time"for a time-like one (\(z = x - s\)).This names the transform rather than changing it: the collapse is identical either way, because the data alone fixes the reduced coordinate. What the domain decides is whether the offset is reported as \(+\log a_T\) or \(-\log a_T\) – so getting it wrong flips the sign of every shift factor and of
effective_activation_energy(), while every plot still looks perfect. Hence no guessing from the data.condition_scale (str, optional) –
"kelvin"declares the condition to be an absolute temperature, which enables the reciprocal-condition families andeffective_activation_energy(), and rejects a Celsius-looking column. None treats the condition as a generic scalar; only the polynomial family is then available.reference (float, optional) – Reference condition, anchoring \(s(c_{ref}) = 0\). Defaults to the median of the measured conditions, which keeps \(q\) small at both ends.
vertical_shift (str) –
"none","shared"(one \(v(c)\) for all channels), or"per_channel".candidate_families (sequence of str) – Any of
"arrhenius","wlf","polynomial".poly_degree (int) – Highest power of \(q\) in the polynomial family’s contribution to \(s'\); the recovered shift law reaches one degree higher.
wlf_c2_bounds (tuple of float) – Search bounds on the dimensionless WLF denominator constant \(c_2 = C_2 / c_{ref}\).
max_terms (int) – Maximum number of terms across both shift laws.
selection (str) – Information criterion for selection:
"aic","aicc", or"bic".strategy (str) – Selection strategy, passed to
SymbolicRegressor.smoother (str) – Derivative-surface smoother:
"tensor_spline","local_poly", or"gp".smoothing (float or str) – Smoothing level for the derivative surface:
"auto"(GCV or marginal likelihood),"sigma"to match the noise level measured from replicates or from single-condition curves, or a number. Never chosen against the symbolic score – tuning a smoother against the regression that consumes it can manufacture whichever law the regression prefers, and the failure is silent.surface_degree (int) – Spline / local-polynomial degree of the derivative surface.
weighting (str) – How to weight rows of the \(y_q\) regression.
"derivative_se"(the default) weights by \(1/\sigma^2\) of the smoother’s estimate of that target, so rows near the edges of the condition range – where derivative standard errors blow up, and where a shift law is most tempted to bend – stop dominating the selection."none"fits unweighted.log_base (float) – Base of the log abscissa. Only used to convert a shift slope into an activation energy.
validation (str) –
"loco"for leave-one-condition-out validation (the default and the module’s verdict) or"none"to skip it.max_holdout_conditions (int, optional) – Cap on how many conditions are withheld, for large tables. Conditions are sampled evenly across the measured range.
collapse_thresholds (tuple of float) –
(supported, weakly_supported)multiples of the noise floor.n_stability (int) – Size of the stability ensemble. 0 disables it.
stability_resampling (str) –
"residual"re-draws measurement noise and re-runs the whole pipeline;"conditions"resamples whole curves with replacement. Both perturb the smoothing stage, which a row-wise bootstrap cannot.random_state (int, optional) – Seed for the stability ensemble.
- shift_expression_#
The selected symbolic shift law in
q. Not the headline result – structurally different laws routinely produce the same transform to within 0.01 decades. Report the transform and a physical summary instead.- Type:
str
- vertical_expressions_#
Channel label (or
"shared") mapped to the selected vertical shift law.- Type:
dict
- master_curve_#
Channel label mapped to a
MasterCurve.- Type:
dict
- validity_report_#
The verdict, from held-out collapse.
- Type:
- stability_#
Selection frequencies, parameter distributions and the spread of the physical summary over the ensemble; None when
n_stability == 0.- Type:
dict or None
- channels_#
Channel labels, in first-appearance order.
- Type:
list
- conditions_#
Distinct conditions, sorted.
- Type:
list of float
- reference_#
Reference condition actually used.
- Type:
float
- surfaces_#
Channel label mapped to the fitted
SurfaceDerivativesused for the derivative stage. Kept so that the smoothing level is inspectable.- Type:
dict
- noise_floor_#
Measurement noise standard deviation, in response units. The collapse cannot be better than this, so it is the yardstick the verdict is read against.
- Type:
float
- noise_floor_source_#
How the floor was measured:
"replicates","curve_smoother", or"surface"(in decreasing order of directness).- Type:
str
- selection_model_#
The fitted sparse regression over the structured blocks, exposed for the usual diagnostics (
selection_path_,pareto_front_,metrics_). Its expression is written in the working features (q,y_x, channel indicators) and its terms are block functions, so it is for inspection rather than export – do not hand it toto_sympy()orto_callable(). The result you want isshift_factors().- Type:
- Raises:
ValueError – If any constructor argument is invalid.
Examples
>>> from jaxsr.superposition import SuperpositionRegressor >>> model = SuperpositionRegressor( ... condition="temperature", abscissa="log_omega", response="log_Gp", ... domain="frequency", condition_scale="kelvin", ... ) >>> model.fit({"temperature": T, "log_omega": x, "log_Gp": y}) >>> model.shift_factors([280.0, 320.0]) array([ 0.83, -0.72]) >>> model.validity_report_.verdict 'supported'
See also
jaxsr.derivatives.SurfaceDerivativesThe derivative stage this builds on.
jaxsr.basis.BasisLibrary.add_blockThe structured blocks the regression uses.
- fit(data: Any) SuperpositionRegressor#
Discover the transform, reconstruct the master curve, and test the collapse.
- Parameters:
data (mapping or pandas.DataFrame) – Tidy table. Any object supporting
data[column_name]works, so a dict of 1-D arrays and a DataFrame are both accepted. The columns named by condition, abscissa, response and (if given) channel must be present and equal in length.- Returns:
self – The fitted estimator.
- Return type:
- Raises:
KeyError – If a required column is missing.
ValueError – If the table is malformed, holds non-finite values, has fewer than four distinct conditions, or declares kelvin for a column that cannot be one.
- shift_factors(condition: ndarray | None = None) ndarray#
Evaluate the horizontal shift \(s(c) = \log_{b} a_T\).
This – not
shift_expression_– is the identified quantity. Laws with different structure routinely agree here to within 0.01 decades.- Parameters:
condition (array-like, optional) – Conditions at which to evaluate, in the same units as the fitted column. Defaults to the distinct measured conditions.
- Returns:
Shift in abscissa units, zero at the reference condition.
- Return type:
np.ndarray of shape (n_conditions,)
- Raises:
RuntimeError – If the model has not been fitted.
Examples
>>> model.shift_factors([300.0, 320.0]) array([ 0. , -0.71])
- vertical_shifts(condition: ndarray | None = None, channel: Any = None) ndarray#
Evaluate the vertical shift \(v(c)\).
- Parameters:
condition (array-like, optional) – Conditions at which to evaluate. Defaults to the measured conditions.
channel (Any, optional) – Channel label, required when
vertical_shift="per_channel".
- Returns:
Vertical shift in response units, zero at the reference condition and identically zero when
vertical_shift="none".- Return type:
np.ndarray of shape (n_conditions,)
- Raises:
RuntimeError – If the model has not been fitted.
ValueError – If channel is missing or unknown for a per-channel vertical shift.
- transform(data: Any = None) dict[str, ndarray]#
Apply the discovered transform, returning the reduced coordinates.
- Parameters:
data (mapping or pandas.DataFrame, optional) – Table with the same columns as
fit(). Defaults to the training data.- Returns:
The input columns plus
"z"(reduced abscissa) and"w"(reduced response). Returned as a plain dict of arrays so the caller can build whatever table type they use.- Return type:
dict
- Raises:
RuntimeError – If the model has not been fitted.
KeyError – If a required column is missing from data.
- predict(condition: ndarray, abscissa: ndarray, channel: Any = None) ndarray#
Predict the response at new conditions by shifting the master curve.
- Parameters:
condition (array-like of shape (n_samples,)) – Conditions, including ones never measured – extrapolating to those is the point of learning the shift law symbolically.
abscissa (array-like of shape (n_samples,)) – Log abscissa values.
channel (Any, optional) – Channel label. Required when the model was fitted with several channels.
- Returns:
Predicted response.
- Return type:
np.ndarray of shape (n_samples,)
- Raises:
RuntimeError – If the model has not been fitted.
ValueError – If the arrays disagree in length, or the channel is missing or unknown.
- effective_activation_energy(gas_constant: float = 8.314462618) float#
Report the activation energy implied by the shift law at the reference.
Defined as \(E_{eff} = -\ln(b)\, R\, T_{ref}\, s'(0)\) with \(b\) the log base of the abscissa. This is the summary to compare across fits: in a 90-fit study spanning nine distinct selected structures it came back at 54.2 +/- 1.3 kJ/mol against a true 55.9, while the expressions disagreed 38% of the time.
- Parameters:
gas_constant (float) – Molar gas constant, in the energy units wanted for the result. Defaults to 8.314462618 J/(mol K), so the result is in J/mol.
- Returns:
Effective activation energy at the reference condition.
- Return type:
float
- Raises:
RuntimeError – If the model has not been fitted.
ValueError – If the condition is not an absolute temperature (
condition_scale="kelvin"), in which case the quantity is meaningless.
Examples
>>> model.effective_activation_energy() / 1000.0 # kJ/mol 54.2
- property shift_expression_: str#
The selected shift law as a string in
q.- Returns:
The law, with any fitted nonlinear parameter substituted.
"0"when no horizontal term survived selection.- Return type:
str
- Raises:
RuntimeError – If the model has not been fitted.
Notes
Deliberately not the headline result. Over a realistic condition window,
1/(1+q)^2,1/(c2+q)^2and low-order polynomials span nearly the same function space, so this expression is far less reproducible than the transform it encodes. Readshift_factors(), andeffective_activation_energy()for a structure-independent summary.
- property vertical_expressions_: dict[Any, str]#
Selected vertical-shift laws, keyed by channel or by
"shared".- Returns:
Key mapped to the law as a string in
q. Empty whenvertical_shift="none".- Return type:
dict
- Raises:
RuntimeError – If the model has not been fitted.
- summary() str#
Return a human-readable report, leading with the transform and the verdict.
- Returns:
Multi-line text.
- Return type:
str
- Raises:
RuntimeError – If the model has not been fitted.
- class jaxsr.superposition.ValidityReport(verdict: str, noise_floor: float, noise_floor_source: str, in_sample_collapse: float, holdout: list[dict[str, ~typing.Any]] = <factory>, holdout_collapse_median: float | None = None, holdout_ratio_median: float | None = None, shift_error_median: float | None = None, thresholds: tuple[float, float] = (2.0, 4.0), flags: list[str] = <factory>)#
Bases:
objectGraded verdict on whether the discovered collapse actually holds.
The verdict comes from collapse on withheld conditions, not from the in-sample collapse and not from the stability of the selected expression. A negative control with no valid shift factor can produce a beautiful in-sample collapse and a perfectly reproducible shift law; only a condition that took no part in the smoothing or the discovery separates the cases.
- Parameters:
verdict (str) –
"supported","weakly_supported","not_supported", or"not_evaluated"when validation was disabled or impossible.noise_floor (float) – Estimated measurement noise standard deviation, in response units. The collapse cannot be better than this.
noise_floor_source (str) – How the floor was estimated:
"replicates","curve_smoother", or"surface"(in decreasing order of directness).in_sample_collapse (float) – Pooled RMSE of the collapse on the conditions used for fitting. Reported for context only – it is not the verdict.
holdout (list of dict) – One entry per withheld condition, with keys
"condition","collapse_rmse","ratio"(to the noise floor),"shift_predicted","shift_aligned","shift_error", and"coverage"(fraction of the withheld curve landing inside the master curve’s range).holdout_collapse_median (float or None) – Median of the held-out collapse RMSEs.
holdout_ratio_median (float or None) – Median held-out collapse RMSE as a multiple of the noise floor. This is the number the verdict is read from.
shift_error_median (float or None) – Median absolute difference between the predicted shift for a withheld condition and the shift that would have aligned it best, in the units of the abscissa (decades, for a base-10 log abscissa).
thresholds (tuple of float) – The
(supported, weakly_supported)multiples of the noise floor used.flags (list of str) – Machine-readable warnings raised during fitting or validation.
Examples
>>> report = ValidityReport( ... verdict="supported", noise_floor=0.030, ... ... ) >>> report.verdict 'supported'
- verdict: str#
- noise_floor: float#
- noise_floor_source: str#
- in_sample_collapse: float#
- holdout: list[dict[str, Any]]#
- holdout_collapse_median: float | None = None#
- holdout_ratio_median: float | None = None#
- shift_error_median: float | None = None#
- thresholds: tuple[float, float] = (2.0, 4.0)#
- flags: list[str]#
- to_dict() dict[str, Any]#
Serialize the report to a plain dictionary.
- Returns:
All fields, with the per-condition list copied.
- Return type:
dict
- summary() str#
Return a human-readable summary of the verdict and the numbers behind it.
- Returns:
Multi-line text.
- Return type:
str
- jaxsr.superposition.collapse_rmse(z: ndarray, y: ndarray, channel: ndarray | None = None, smoothing: float | str = 'auto', degree: int = 3, sigma: float | None = None) float#
Root-mean-square scatter of points about a smooth curve through them.
This is the quantity that decides whether a proposed collapse is any good: fit a single smooth curve per channel to the reduced coordinates and measure how far the data sit from it. It is public because it is also the right way to score a collapse produced by hand or by another tool.
- Parameters:
z (np.ndarray of shape (n_samples,)) – Reduced abscissa (shifted).
y (np.ndarray of shape (n_samples,)) – Reduced response (vertically shifted, if applicable).
channel (np.ndarray of shape (n_samples,), optional) – Channel label per row. Each channel gets its own master curve; the returned value pools their residuals.
smoothing (float or str) – Passed to
SurfaceDerivatives."auto"selects the smoothing level by GCV.degree (int) – Spline degree of the master-curve smoother.
sigma (float, optional) – Known measurement noise standard deviation. Required by
smoothing="sigma", and otherwise only sharpens the reported uncertainty.
- Returns:
Pooled root-mean-square residual.
- Return type:
float
- Raises:
ValueError – If the arrays disagree in length, contain non-finite entries, or a channel has too few distinct abscissa values to smooth.
Examples
>>> import numpy as np >>> z = np.linspace(-2, 2, 60) >>> y = np.tanh(z) >>> bool(collapse_rmse(z, y) < 1e-3) True