Skip to content

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 while keeping the standard characteristic fixed.

The API deliberately returns two things together:

  1. the numerical model you can use for conversion; and
  2. immutable evidence describing how that fit was obtained and how well it matched the observations.

Available since: rtd-sensor 0.6.0.

Fit a characterized IEC 60751 R0

Planned for: 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.

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

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;
  • scaled-system conditioning diagnostic;
  • 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; this fitter does not model uncertainty in the temperature coordinate.

Fit range

By default, the fitted model uses the observed calibration span. You may narrow it inside that span:

fit = fitting.fit_polynomial(
    observations,
    degree=2,
    minimum_temperature_c=10.0,
    maximum_temperature_c=90.0,
)

The fitting API does not silently extrapolate the deployable model beyond the observed calibration span.

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

A fitted polynomial can be passed directly to portable model definitions so another process or language can reconstruct the numerical model without rerunning the fit.