Skip to content

Commit

Permalink
New thermocouple scaling implementation (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamreeve committed Sep 2, 2020
1 parent 0ff4e42 commit 59ef961
Show file tree
Hide file tree
Showing 5 changed files with 986 additions and 53 deletions.
56 changes: 18 additions & 38 deletions nptdms/scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re

from nptdms.log import log_manager
import nptdms.thermocouples as thermocouples


log = log_manager.get_logger(__name__)
Expand Down Expand Up @@ -72,7 +73,7 @@ def scale(self, data):

class RtdScaling(object):
""" Converts a signal from a resistance temperature detector into
degrees celcius using the Callendar-Van Dusen equation
degrees Celsius using the Callendar-Van Dusen equation
"""
def __init__(
self, current_excitation, r0_nominal_resistance,
Expand Down Expand Up @@ -261,26 +262,24 @@ def scale(self, data):


class ThermocoupleScaling(object):
""" Convert between voltages in uV and degrees celcius for a Thermocouple.
""" Convert between voltages in uV and degrees Celsius for a Thermocouple.
Can convert in either direction depending on the scaling direction
parameter.
"""
def __init__(self, type_code, scaling_direction, input_source):
from thermocouples_reference import thermocouples

# Thermocouple types from
# http://zone.ni.com/reference/en-XX/help/371361R-01/glang/tdms_create_scalinginfo/#instance2
thermocouple_type = {
10047: 'B',
10055: 'E',
10072: 'J',
10073: 'K',
10077: 'N',
10082: 'R',
10085: 'S',
10086: 'T',
}[type_code]
self.thermocouple = thermocouples[thermocouple_type]
thermocouple_types = {
10047: thermocouples.type_b,
10055: thermocouples.type_e,
10072: thermocouples.type_j,
10073: thermocouples.type_k,
10077: thermocouples.type_n,
10082: thermocouples.type_r,
10085: thermocouples.type_s,
10086: thermocouples.type_t,
}
self.thermocouple = thermocouple_types[type_code]

self.scaling_direction = scaling_direction
self.input_source = input_source
Expand All @@ -299,31 +298,12 @@ def from_properties(properties, scale_index):
def scale(self, data):
""" Apply thermocouple scaling
"""

# Note that the thermocouples_reference package uses mV for voltages,
# but TDMS uses uV.
nan = float('nan')

def scale_uv_to_c(micro_volts):
"""Convert micro volts to degrees celcius"""
milli_volts = micro_volts / 1000.0
try:
return self.thermocouple.inverse_CmV(milli_volts, Tref=0.0)
except ValueError:
return nan

def scale_c_to_uv(temp):
"""Convert degrees celcius to micro volts"""
try:
return 1000.0 * self.thermocouple.emf_mVC(temp, Tref=0.0)
except ValueError:
return nan

# Note that the thermocouple conversions use mV for voltages, but TDMS uses uV.
if self.scaling_direction == 1:
scaled = np.vectorize(scale_c_to_uv)(data)
return 1000.0 * self.thermocouple.celsius_to_mv(data)
else:
scaled = np.vectorize(scale_uv_to_c)(data)
return scaled
milli_volts = data / 1000.0
return self.thermocouple.mv_to_celsius(milli_volts)


class AddScaling(object):
Expand Down
55 changes: 42 additions & 13 deletions nptdms/test/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@
from nptdms import types
from nptdms.scaling import get_scaling

try:
import thermocouples_reference
except ImportError:
thermocouples_reference = None
try:
import scipy
except ImportError:
scipy = None


def test_unsupported_scaling_type():
"""Raw data is returned unscaled when the scaling type is unsupported.
Expand Down Expand Up @@ -271,13 +262,11 @@ def test_subtract_scaling():
np.testing.assert_almost_equal(scaled_data, expected_scaled_data)


@pytest.mark.skipif(thermocouples_reference is None, reason="thermocouples_reference is not installed")
@pytest.mark.skipif(scipy is None, reason="scipy is not installed")
def test_thermocouple_scaling_voltage_to_temperature():
"""Test thermocouple scaling from a voltage in uV to temperature"""

data = StubTdmsData(np.array([0.0, 10.0, 100.0, 1000.0]))
expected_scaled_data = np.array([0.0, 0.2534448, 2.5309141, 24.9940185])
expected_scaled_data = np.array([0.000000, 0.250843, 2.508899, 24.983648])

properties = {
"NI_Number_Of_Scales": 1,
Expand All @@ -293,7 +282,6 @@ def test_thermocouple_scaling_voltage_to_temperature():
scaled_data, expected_scaled_data, decimal=3)


@pytest.mark.skipif(thermocouples_reference is None, reason="thermocouples_reference is not installed")
def test_thermocouple_scaling_temperature_to_voltage():
"""Test thermocouple scaling from a temperature to voltage in uV"""

Expand All @@ -315,6 +303,47 @@ def test_thermocouple_scaling_temperature_to_voltage():
scaled_data, expected_scaled_data, decimal=3)


def test_thermocouple_scaling_voltage_to_temperature_benchmark(benchmark):
"""Test thermocouple scaling from a voltage in uV to temperature"""

data = StubTdmsData(np.tile(np.array([0.0, 10.0, 100.0, 1000.0]), 100))
expected_scaled_data = np.tile(np.array([0.000000, 0.250843, 2.508899, 24.983648]), 100)

properties = {
"NI_Number_Of_Scales": 1,
"NI_Scale[0]_Scale_Type": "Thermocouple",
"NI_Scale[0]_Thermocouple_Thermocouple_Type": 10073,
"NI_Scale[0]_Thermocouple_Scaling_Direction": 0,
"NI_Scale[0]_Thermocouple_Input_Source": 0xFFFFFFFF,
}
scaling = get_scaling(properties, {}, {})
scaled_data = benchmark(scaling.scale, data)

np.testing.assert_almost_equal(
scaled_data, expected_scaled_data, decimal=3)


def test_thermocouple_scaling_temperature_to_voltage_benchmark(benchmark):
"""Test thermocouple scaling from a temperature to voltage in uV"""

data = StubTdmsData(np.tile(np.array([0.0, 10.0, 50.0, 100.0]), 100))
expected_scaled_data = np.tile(np.array([
0.0, 396.8619078, 2023.0778862, 4096.2302187]), 100)

properties = {
"NI_Number_Of_Scales": 1,
"NI_Scale[0]_Scale_Type": "Thermocouple",
"NI_Scale[0]_Thermocouple_Thermocouple_Type": 10073,
"NI_Scale[0]_Thermocouple_Scaling_Direction": 1,
"NI_Scale[0]_Thermocouple_Input_Source": 0xFFFFFFFF,
}
scaling = get_scaling(properties, {}, {})
scaled_data = benchmark(scaling.scale, data)

np.testing.assert_almost_equal(
scaled_data, expected_scaled_data, decimal=3)


@pytest.mark.parametrize(
"resistance_configuration,lead_resistance,expected_data",
[
Expand Down
195 changes: 195 additions & 0 deletions nptdms/test/test_thermocouples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import numpy as np
import pytest
from hypothesis import (given, strategies, settings)

from nptdms import thermocouples
import thermocouples_reference


settings.register_profile("thermocouples", deadline=None, max_examples=100)
settings.load_profile("thermocouples")


reference_thermocouple_b = thermocouples_reference.thermocouples['B']
reference_thermocouple_e = thermocouples_reference.thermocouples['E']
reference_thermocouple_j = thermocouples_reference.thermocouples['J']
reference_thermocouple_k = thermocouples_reference.thermocouples['K']
reference_thermocouple_n = thermocouples_reference.thermocouples['N']
reference_thermocouple_r = thermocouples_reference.thermocouples['R']
reference_thermocouple_s = thermocouples_reference.thermocouples['S']
reference_thermocouple_t = thermocouples_reference.thermocouples['T']


def test_scale_temperature_to_voltage():
thermocouple = thermocouples.Thermocouple(
forward_polynomials=[
thermocouples.Polynomial(
applicable_range=thermocouples.Range(None, 10),
coefficients=[0.0, 1.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(10, 20),
coefficients=[1.0, 2.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(20, None),
coefficients=[2.0, 3.0]),
],
inverse_polynomials=[]
)
voltages = thermocouple.celsius_to_mv(np.array([0.0, 9.0, 10.0, 11.0, 19.0, 20.0, 21.0]))
np.testing.assert_almost_equal(voltages, np.array([0.0, 9.0, 21.0, 23.0, 39.0, 62.0, 65.0]))


def test_scale_voltage_to_temperature():
thermocouple = thermocouples.Thermocouple(
forward_polynomials=[],
inverse_polynomials=[
thermocouples.Polynomial(
applicable_range=thermocouples.Range(None, 10),
coefficients=[0.0, 1.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(10, 20),
coefficients=[1.0, 2.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(20, None),
coefficients=[2.0, 3.0]),
]
)
temperatures = thermocouple.mv_to_celsius(np.array([0.0, 9.0, 10.0, 11.0, 19.0, 20.0, 21.0]))
np.testing.assert_almost_equal(temperatures, np.array([0.0, 9.0, 21.0, 23.0, 39.0, 62.0, 65.0]))


def test_non_contiguous_forward_polynomial_ranges():
with pytest.raises(ValueError) as exc_info:
_ = thermocouples.Thermocouple(
forward_polynomials=[
thermocouples.Polynomial(
applicable_range=thermocouples.Range(None, 10),
coefficients=[0.0, 1.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(11, None),
coefficients=[0.0, 1.0]),
],
inverse_polynomials=[]
)
assert "Polynomial ranges must be contiguous" in str(exc_info.value)


def test_non_contiguous_inverse_polynomial_ranges():
with pytest.raises(ValueError) as exc_info:
_ = thermocouples.Thermocouple(
forward_polynomials=[],
inverse_polynomials=[
thermocouples.Polynomial(
applicable_range=thermocouples.Range(None, 10),
coefficients=[0.0, 1.0]),
thermocouples.Polynomial(
applicable_range=thermocouples.Range(11, None),
coefficients=[0.0, 1.0]),
]
)
assert "Polynomial ranges must be contiguous" in str(exc_info.value)


def test_no_range():
with pytest.raises(ValueError) as exc_info:
_ = thermocouples.Range(None, None)
assert "At least one of start and end must be provided" in str(exc_info.value)


@pytest.mark.parametrize("start,end", [(2.0, 1.0), (1.0, 1.0)])
def test_invalid_range(start, end):
with pytest.raises(ValueError) as exc_info:
_ = thermocouples.Range(start, end)
assert "start must be less than end" in str(exc_info.value)


@given(temperature=strategies.floats(0.0, 1820.0))
def test_type_b_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_b, thermocouples.type_b, temperature)


@given(voltage=strategies.floats(0.291, 13.820))
def test_type_b_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_b, thermocouples.type_b, voltage, max_error=0.03)


@given(temperature=strategies.floats(-270.0, 1000.0))
def test_type_e_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_e, thermocouples.type_e, temperature)


@given(voltage=strategies.floats(-8.825, 76.373))
def test_type_e_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_e, thermocouples.type_e, voltage, max_error=0.03)


@given(temperature=strategies.floats(-210.0, 1200.0))
def test_type_j_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_j, thermocouples.type_j, temperature)


@given(voltage=strategies.floats(-8.095, 69.553))
def test_type_j_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_j, thermocouples.type_j, voltage, max_error=0.05)


@given(temperature=strategies.floats(-270.000, 1372.000))
def test_type_k_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_k, thermocouples.type_k, temperature)


@given(voltage=strategies.floats(-5.891, 54.886))
def test_type_k_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_k, thermocouples.type_k, voltage, max_error=0.06)


@given(temperature=strategies.floats(-270.000, 1300.0))
def test_type_n_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_n, thermocouples.type_n, temperature)


@given(voltage=strategies.floats(-3.990, 47.513))
def test_type_n_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_n, thermocouples.type_n, voltage, max_error=0.04)


@given(temperature=strategies.floats(-50.000, 1768.1))
def test_type_r_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_r, thermocouples.type_r, temperature)


@given(voltage=strategies.floats(-0.226, 21.103))
def test_type_r_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_r, thermocouples.type_r, voltage, max_error=0.02)


@given(temperature=strategies.floats(-50.000, 1768.1))
def test_type_s_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_s, thermocouples.type_s, temperature)


@given(voltage=strategies.floats(-0.235, 18.693))
def test_type_s_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_s, thermocouples.type_s, voltage, max_error=0.02)


@given(temperature=strategies.floats(-270.000, 400.000))
def test_type_t_temperature_to_voltage(temperature):
_test_temperature_to_voltage(reference_thermocouple_t, thermocouples.type_t, temperature)


@given(voltage=strategies.floats(-5.603, 20.872))
def test_type_t_voltage_to_temperature(voltage):
_test_voltage_to_temperature(reference_thermocouple_t, thermocouples.type_t, voltage, max_error=0.04)


def _test_temperature_to_voltage(reference_thermocouple, thermocouple, temperature):
reference_voltage = reference_thermocouple.emf_mVC(temperature, Tref=0.0)
voltage = thermocouple.celsius_to_mv(temperature)
assert abs(voltage - reference_voltage) < 1.0E-6


def _test_voltage_to_temperature(reference_thermocouple, thermocouple, voltage, max_error):
reference_temperature = reference_thermocouple.inverse_CmV(voltage, Tref=0.0)
temperature = thermocouple.mv_to_celsius(voltage)
assert abs(temperature - reference_temperature) < max_error + 1.0E-6

0 comments on commit 59ef961

Please sign in to comment.