Accurate calculation of PT100/PT1000 temperature from resistance

TL;DR for impatient readers

PT100/PT1000 temperatures calculation suffers from accuracy issues for large sub-zero temperatures. UliEngineering implements a polynomial-fit based algorithm to provide $58.6 \mu{\degree}C$ peak-error over the full defined temperature range from -200 {\degree}C to +850 °C.

Use this code snippet (replace pt1000_ by pt100- to use PT100 coefficients) to compute an accurate temperature (in degrees celsius) e.g. for a resistane of 829.91 Ω of a PT1000 sensor.

from UliEngineering.Physics.RTD import pt1000_temperature
# The following calls are equivalent and print -43.2316359463
print(pt1000_temperature("829.91 Ω"))
print(pt1000_temperature(829.91))

You install the library (compatible to Python 3.2+) using

pip3 install -U UliEngineering

The problem

The formula to compute PT100/PT1000 resistance from temperature is well-known (see e.g. Thermometrics):

$R_t,=,R_0\cdot(1,+,A\cdot t,+,B\cdot t^2,+,C\cdot (t-100)\cdot t^3)$

where $t$ is the temperature, $R_0$ is the zero-{\degree}C resistance (i.e. 100 Ω for PT100 and 1000 Ω for PT1000).

The remaining parameters A, B and C depend on the temperature standard in use and might be measured by the sensor manufacturer for additional accuracy. For the ITU-90 standard they equal (see code10.info)

$$\begin{array}{lll}A&=&3.9083\cdot10^{-3}\\B\,&=&\,-5.7750\cdot10^{-7}\\C\,&=&\,\begin{cases}-4.1830\cdot 10^{-12}&\text{for}\ t&\lt& 0{\degree}C\\0.0&\text{for}\ t &\geq&\;0\;{\degree}C\end{cases}\end{array}$$

Furthermore, $C$ is set to 0 for temperatures > 0°C, simplifying the formula to:

$$R_t\,=\,R_0\cdot(1\,+\,A\cdot t\,+\,B\cdot t^2)$$

It it obvious that, given this information, the resistance at a given temperature can be calculated without any error term.

The problems arise when attempting to solve the equation in the general case.

The formula for $t,\geq,0$ is easily solvable (source for the shown form):

$$t\,=\,\frac{-R_0\cdot\,a\,+\,\sqrt{R_0^2\cdot a^2\,-\,4\cdot R_0\cdot B\cdot (R_0-R_t)}}{2\cdot R_0\cdot b}$$

Although the formula for $t\lt0 {\degree}C$ has exact algebraic solutions, it is so large that it probably won’t fit on your screen. It therefore can be considered infeasible to implement this formula as one would sacrifice simplicity and speed for an exact solution.

However, it is inherently true that a given sensor can only work up to a certain precision. Therefore, an approximate solution with sufficient precision is sufficent for all practical applications.

The solution: Fit a polynomial to the error function

At first I thought of implementing an iterative function that refines an initial temperature guess. While this approach would certainly work as an exact error function is available, it does not scale well and does not have deterministic runtime.

Let’s visualize the error function being present without any correction term. As the $C$ term reduces to 0 for $t\geq0{\degree}C$, we are interested only in the range from -200 °C to 0°C (PT100/PT1000 sensors are not defined below -200°C in the relevant standards, but in principle this method extends down to 0°K).

The method I’m about to present — as well as all tools neccessary to validate it — is implemented in my UliEngineering library in the UliEngineering.Physics.RTD module.

Using matplotlib and UliEngineering, generating this plot is possible in only 12 lines of code:

import matplotlib.pyplot as plt
from UliEngineering.Physics.RTD import *
import numpy as np
plt.style.use("ggplot")
plt.gcf().set_size_inches(12, 4)
plt.title("Uncorrected PT1000 error")
plt.ylabel("Relative error [°C]")
plt.xlabel("Absolute actual temperature [°C]")
# Create 1M datapoints of reference temperatures
temp = np.linspace(-200.0, 0.0, 1000000)
# Plot deviation: reference temperature - calculated temperature
x, y, _ = checkCorrectionPolynomialQuality(1000.0, temp, poly=noCorrection)
plt.plot(temp, y)
plt.savefig("PT1000-uncorrected.svg")

We call checkCorrectionPolynomialQuality() with the canned noCorrection polynomial which always evaluates to zero: In this configuration, the function computes the resistances from our reference temperatures and the re-computes actual reference temperature values from said resistances. It returns three values: - A numpy array of resistances, corresponding to our reference temperatures - A numpy array of difference from the reference temperature at any given resistance / temperature, in °C - A peak-absolute value scalar quality indicator (i.e. what’s the worst error to expect)

It is easily observable that, while the error reaches almost 2.5°C at -200°C, it is montonous and uniformly continous.

Our approach therefore comprises of fitting a polynomial on this function, minimizing the difference from the reference temperature using np.polyfit.

This algorithm is available in the computeCorrectionPolynomial() function from UliEngineering. It has been determined experimentally that a 5th-degree polynomial exhibits results that are significantly better that those of higher- or lower-degree polynomials. Nevertheless, the function lets you specify a custom degree if you intend to experiment with the parameters.

plt.gcf().clf() # Clear current figure
plt.gcf().set_size_inches(12, 4)
plt.title("Polynomially corrected PT1000 error")
plt.ylabel("Relative error [°C]")
plt.xlabel("Absolute actual temperature [°C]")
# Compute correction potential
mypoly = computeCorrectionPolynomial(1000.0)
# Plot deviation: reference temperature - calculated temperature
_, y, pp = checkCorrectionPolynomialQuality(1000.0, temp, poly=mypoly)
plt.plot(temp, y)
plt.savefig("PT1000-corrected.svg")
print("Peak-to-peak error: {0}".format(pp))

It can be clearly seen that the remaining error is extremely small now: Its peak-absolute value over the entire defined temperature range is only $58.6,\mu{\degree}C$. This is considered sufficient for all practical applications besides reference temperature measurement in metrology (and even there a few tens of micro-degrees.

For a simple (and relatively fast-to-compute) 5th-degree polynomial, these results are astonishingly good. The current git version of the UliEngineering library implements this algorithm by using precomputed polynomials that are automatically selected if you pass $R_0=100.0$ or $R_0=1000.0$ to ptx_temperature(), which is internally called from pt100_temperature() and pt1000_temperature(). For other $R_0$ values you’ll need to manually compute the polynomial and pass it to ptx_temperature().

Of course, NumPy arrays and similar objects can also be passed to the functions in UliEngineering.