Calibration fitting
rtd_sensor.fitting fits RTD models from measured temperature/resistance
calibration observations without requiring NumPy. The 0.6.0 API introduced
polynomial fitting; 0.7.0 adds fitting of a characterized IEC 60751 PT-385
reference resistance and selected custom Callendar–Van Dusen parameters.
The API deliberately returns two things together:
- the numerical model you can use for conversion; and
- immutable evidence describing how that fit was obtained and how well it matched the observations.
Available since: rtd-sensor 0.6.0.
Planned 0.9 calibration experiment design
The current fitting API analyzes calibration observations that already exist. The 0.9 roadmap separately plans a prospective calibration experiment designer that will help choose calibration temperatures before or during an experiment. That planner is still under design review and is not part of the current public API.
The provisional criterion is prediction-oriented: it minimizes a weighted integral of predicted fitted-curve variance over the declared operating range, after translating resistance-domain fit uncertainty into first-order temperature-equivalent uncertainty through a nominal RTD model. The engineering design document records the full equations, assumptions, candidate-set semantics, repeat policy, numerical integration, conditioning, and evidence requirements.
I-optimal versus V-optimal terminology
Optimal-design terminology is not perfectly uniform. Atkinson (2015) explicitly notes that designs minimizing average prediction variance over a region are variously called I-optimal or V-optimal. NIST's Engineering Statistics Handbook uses V-optimal for an average-prediction-variance criterion over a specified set of points.
rtd-sensor will use I-optimal consistently for the planned continuous
integral over the fitted operating range. This is a documentation choice intended
to distinguish that continuous region-of-interest formulation from NIST's discrete
V-optimal wording. It does not mean that V-optimal is wrong terminology in the
broader experimental-design literature.
The planned rtd-sensor criterion is more specifically sensitivity-weighted
I-optimal because it adds the local RTD dT/dR sensitivity needed to express
predicted fitted-curve uncertainty in temperature-equivalent units. That
RTD-specific weighting is a project design decision built on the classical
I/V-optimal average-prediction-variance framework.
Initial complete-design search scope
The provisional 0.9 complete-design operation is intentionally an exact search over a small, explicit candidate set. It is aimed at curated candidate temperatures in the low tens with run budgets in the single digits to low teens, not at silently searching every 1–5 °C grid point across a very wide range. A range/spacing helper may materialize a denser list, but that does not guarantee that the resulting joint design problem fits inside the implementation's exhaustive-search limit.
If a complete-design request is too large, the planner will report the calculated search size and fail explicitly rather than thinning the candidate set or switching to an unannounced heuristic. The one-step/next-observation operation remains a different case: it evaluates each supplied candidate once and can therefore remain useful with substantially denser candidate lists. Exact tested candidate/run envelopes will be published with the implementation after benchmarking rather than being guessed before code exists.
Fit a characterized IEC 60751 R0
Introduced in: rtd-sensor 0.7.0.
When the probe is assumed to retain the standard IEC 60751 PT-385 curve and you
only want to estimate its individual reference resistance, fit R0 directly:
from rtd_sensor import fitting
observations = (
fitting.CalibrationObservation(0.0, 100.037),
fitting.CalibrationObservation(100.0, 138.556),
)
fit = fitting.fit_iec60751_r0(observations)
print(fit.model.r0_ohms)
The returned numerical model is an ordinary IEC60751RTDModel; the calibration
process does not create a second model kind. The evidence retains the observations
and residuals separately.
With two or more distinct temperatures, the model range defaults to the observed
span. A single-temperature observation can identify R0, but you must then
declare a nonzero applicability range explicitly:
fit = fitting.fit_iec60751_r0(
(fitting.CalibrationObservation(0.0, 100.037),),
minimum_temperature_c=-50.0,
maximum_temperature_c=250.0,
)
An explicitly declared range describes where you intend to use the fitted model,
not where calibration observations were collected. With an independent basis it
may be broader, narrower, or even disjoint from the observation span. The fit
evidence keeps those two ranges separate so the applicability declaration is not
mistaken for calibration evidence. Fitting R0 does not by itself establish IEC
tolerance-class conformance or prove physical accuracy away from the calibration
points.
Fit selected Callendar–Van Dusen parameters
Introduced in: rtd-sensor 0.7.0.
When calibration observations are intended to define a custom platinum CVD curve, select exactly which parameters should be estimated and supply the others as fixed inputs:
from rtd_sensor import fitting
observations = (
fitting.CalibrationObservation(-50.0, 80.31),
fitting.CalibrationObservation(0.0, 100.025),
fitting.CalibrationObservation(100.0, 138.56),
fitting.CalibrationObservation(200.0, 175.90),
)
fit = fitting.fit_callendar_van_dusen(
observations,
fit_parameters=("r0_ohms", "a", "b", "c"),
)
The API does not guess which parameters are identifiable. fit_parameters is
explicit, and the scaled least-squares system must be full-rank and below the
project's severe-conditioning limit. C cannot be fitted without at least one
negative-temperature observation because its basis term is zero at and above
0 °C. You can also fit a subset while holding other values fixed—for example,
fit A and B while using a previously characterized R0.
Internally, a joint R0/A/B/C fit uses the exact algebraic linearization
(R0, R0*A, R0*B, R0*C) and then transforms the coefficient estimates back into the public CVD parameter
basis. When R0 and shape coefficients are fitted jointly, the public
coefficient covariance uses a first-order Jacobian/delta-method transformation from
the exact linearized-fit covariance, and that transformation is recorded in the
covariance evidence. No generic nonlinear
optimizer is introduced for this model. The evidence records the linearized
parameter names, design-column scales, and scaled-system condition diagnostic so
identifiability remains inspectable.
If any shape coefficient (A, B, or C) is estimated, the model range may be
narrowed but not extended beyond the observation span. An R0-only fit with all
shape coefficients fixed follows the characterized-model rule instead and may use
an independently justified explicit applicability interval.
Polynomial fit
from rtd_sensor import fitting
observations = (
fitting.CalibrationObservation(temperature_c=0.0, resistance_ohms=100.02),
fitting.CalibrationObservation(temperature_c=50.0, resistance_ohms=119.43),
fitting.CalibrationObservation(temperature_c=100.0, resistance_ohms=138.56),
)
fit = fitting.fit_polynomial(observations, degree=2)
model = fit.model
print(fit.evidence.rms_residual_ohms)
print(fit.evidence.max_absolute_residual_ohms)
What the evidence contains
All three fit families retain immutable observations, residuals, weighting and
uncertainty treatment, optional calibration provenance, and fit diagnostics.
Model-specific evidence adds the parameterization details needed to audit that fit.
For example, PolynomialFitEvidence retains information including:
- the original observations;
- per-point resistance residuals;
- polynomial degree;
- observation and fitted-parameter counts;
- residual degrees of freedom;
- fitting temperature range;
- unweighted RMS and maximum absolute residual;
- weighting method and normalized effective weights when used;
- weighted residual diagnostics when applicable;
- fitted-parameter covariance when the statistical basis supports it;
- covariance-derived parameter standard uncertainties and correlations;
- chi-square and reduced-chi-square diagnostics when absolute resistance standard uncertainties are supplied;
- scaled-system conditioning/identifiability diagnostics where applicable;
- solver and scaling information.
Residuals are observed resistance minus fitted resistance.
The reported RMS is descriptive sqrt(sum(residual²) / observation_count). It
is not a degrees-of-freedom-adjusted uncertainty estimate. A nearly saturated
fit can therefore have very small residuals without proving good predictive
performance.
Weighted fits with explicit weights
Every observation must use the same weighting convention:
observations = (
fitting.CalibrationObservation(0.0, 100.02, weight=1.0),
fitting.CalibrationObservation(50.0, 119.43, weight=2.0),
fitting.CalibrationObservation(100.0, 138.56, weight=1.0),
)
fit = fitting.fit_polynomial(observations, degree=2)
Weights must be positive. The package normalizes them so the largest effective weight is 1.0; the overall scale of a relative least-squares weight set does not change the fitted objective.
Weighted fits from resistance uncertainty
Instead of relative weights, every observation may provide a positive
standard_uncertainty_ohms:
observations = (
fitting.CalibrationObservation(0.0, 100.02, standard_uncertainty_ohms=0.01),
fitting.CalibrationObservation(50.0, 119.43, standard_uncertainty_ohms=0.02),
fitting.CalibrationObservation(100.0, 138.56, standard_uncertainty_ohms=0.01),
)
These values are converted to normalized inverse-variance weights. Temperature is
treated as the independent variable. If a calibration/reference temperature also
has a standard uncertainty, record it separately with
standard_uncertainty_temperature_c. Current fitters reject that field by default
rather than silently translating it into resistance uncertainty.
If you intentionally need to preserve that information while still performing the existing exact-temperature least-squares fit, opt in explicitly:
fit = fitting.fit_polynomial(
observations,
degree=2,
temperature_uncertainty_handling="retain_not_used",
)
The returned observations retain the temperature uncertainties and the evidence
reports temperature_uncertainty_treatment == "retained_not_used", but those
uncertainties did not affect the coefficients, weighting, chi-square, or
parameter covariance. This is not errors-in-variables regression. It is an explicit
record that the independent-variable uncertainty is known but not modeled. NIST
calibration literature uses errors-in-variables methods when uncertainty in the
applied/reference independent variable cannot reasonably be ignored.
Calibration provenance
Calibration context can be retained with the fit without changing the numerical model:
provenance = fitting.CalibrationProvenance(
certificate_identifier="CERT-42",
calibration_date="2026-08-20",
laboratory="Example Calibration Lab",
reference_standard="PRT-17",
)
fit = fitting.fit_iec60751_r0(
observations,
provenance=provenance,
)
This provenance belongs to the fit evidence. It is not automatically copied into
coefficient_source or portable-model metadata, so a calibration record cannot
silently change model behavior or acquire a downstream deployment meaning.
Fitted-parameter covariance
In 0.7.0, IEC R0, selected custom CVD, and polynomial fitting retain parameter
covariance when the regression assumptions provide enough information to estimate
it:
covariance = fit.evidence.parameter_covariance
if covariance is not None:
print(covariance.parameter_names)
print(covariance.covariance_matrix)
For an unweighted fit, or one using only relative weights, the overall residual variance is not supplied externally. The fitter estimates that scale from residual scatter and residual degrees of freedom. A saturated fit therefore cannot report parameter covariance from those data alone; the evidence records the reason instead of returning a zero matrix just because the fitted curve passes through every point.
When every observation supplies standard_uncertainty_ohms, those values have an
absolute physical scale. Under the fit assumptions they define the parameter
covariance directly, so covariance can remain available even when residual degrees
of freedom are zero. The observed residuals remain a separate diagnostic and do not
silently rescale the supplied uncertainties.
Custom CVD covariance is reported in the fitted subset of the public R0, A,
B, C basis. Polynomial covariance is reported for the resistance-space power
series at the returned model's reference temperature:
R(T) = a0 + a1*x + a2*x² + .... This is fit evidence, not additional state
embedded into the portable model. In 0.7.0, supported fitted covariance can be
propagated into predicted resistance uncertainty with
uncertainty.propagate_fit_covariance_to_resistance() or into first-order inferred
temperature uncertainty with
uncertainty.propagate_fit_covariance_to_temperature().
See Fitted-model covariance propagation.
Fit range
Range semantics depend on what the calibration observations actually determine.
Polynomial fits and CVD fits that estimate any shape coefficient (A, B, or C)
may not extend the deployable model beyond the observed calibration span. You may
narrow them inside that span:
fit = fitting.fit_polynomial(
observations,
degree=2,
minimum_temperature_c=10.0,
maximum_temperature_c=90.0,
)
IEC R0 fitting, and CVD R0-only fitting with independently fixed shape
coefficients, are different: the standard/fixed curve shape is not inferred from
the observations, so an explicitly justified applicability range may be broader,
narrower, or disjoint from the observation span within the underlying model's
supported domain. Fit evidence retains the observation span separately so that an
applicability declaration is not mistaken for calibration evidence.
When fitting fails
RTDFitError is raised rather than returning an unsafe model for conditions
such as rank-deficient observations, severe ill-conditioning, or a fitted curve
that becomes non-positive or non-monotonic over its declared range.
This is an important distinction: a least-squares solver producing coefficients does not automatically mean those coefficients define a usable invertible RTD model.
Save the fitted model
The numerical model returned by any supported IEC R0, custom CVD, or polynomial
fit can be passed directly to portable model definitions
so another process or language can reconstruct the deployable model without
rerunning the fit. Fit evidence, covariance, and calibration provenance remain
separate from that portable numerical definition.