Skip to content

Equation¤

Suppose you have a variable named equation (which is an instance of Equation) that represents the following information in an equipment register for equipment that measures relative humidity. The corrected value depends on two variables (r and t) and the standard uncertainty is a constant.

<equation>
  <value variables="r t">r-0.71-0.04*r+3.4e-4*pow(r,2)+2.4e-3*t+1.3e-3*r*t</value>
  <uncertainty variables="">0.355</uncertainty>
  <unit>%rh</unit>
  <ranges>
    <range variable="r">
      <minimum>30</minimum>
      <maximum>80</maximum>
    </range>
    <range variable="t">
      <minimum>15</minimum>
      <maximum>25</maximum>
    </range>
  </ranges>
</equation>

You can access the unit, degrees of freedom and comment as attributes of equation

>>> equation.unit
'%rh'
>>> equation.degree_freedom
inf
>>> equation.comment
''

To evaluate an equation, call the appropriate attribute with the variable(s) that are required to evaluate the equation with

>>> equation.value.variables
('r', 't')
>>> equation.uncertainty.variables
()
>>> assert equation.value(r=50.3, t=20.4) == 49.8211466
>>> assert equation.uncertainty() == 0.355

A variable can have multiple values. Any sequence of numbers, i.e., a list, tuple, ndarray, etc., may be used (tip: using ndarray will improve performance since a copy of the values is not required),

>>> equation.value(r=[50.3, 52.1, 48.7], t=[20.4, 19.7, 20.0])
array([49.8211466, 51.6104604, 48.1625746])

the values of the variables do not need to be 1-dimensional arrays,

>>> equation.value(r=[(50.3, 52.1), (48.7, 47.9)], t=[(20.4, 19.7), (20.0, 19.6)])
array([[49.8211466, 51.6104604],
       [48.1625746, 47.3216314]])

and the array broadcasting rules of numpy also apply, i.e., multiple r values and a single t value

>>> equation.value(r=(50.3, 52.1, 48.7), t=20.4)
array([49.8211466, 51.6595514, 48.1888586])

If you forget to specify a variable (in the following case, t) a NameError will be raised,

>>> equation.value(r=50.3)
Traceback (most recent call last):
...
NameError: name 't' is not defined

however, if you specify more variables than are required to evaluate the equation, the additional variables are ignored

>>> equation.uncertainty(r=50.3, t=20.4)
array(0.355)

Notice in the last returned value that the result was printed as array(0.355) even though a single r and t value was specified (although these variables were ignored in this particular example, since the standard uncertainty is a constant, the principle remains the same if they were not ignored). All evaluated returned types are an instance of a numpy ndarray even if a single value is specified. These particular returned array instances are referred to as 0-dimensional array scalars in numpy terminology.

When evaluating an equation, the value(s) of the input variables are checked to ensure that the value(s) are within the ranges that the equation is valid for. The XML data above shows that the temperature, t, value must be in the range 15 to 25. If you evaluate the corrected value at t=30 a ValueError is raised

>>> equation.value.ranges
{'r': Range(minimum=30, maximum=80), 't': Range(minimum=15, maximum=25)}
>>> equation.value(r=50.3, t=30)
Traceback (most recent call last):
...
ValueError: The value 30.0 is not within the range [15, 25]

You can bypass range checking by including a check_range=False keyword argument

>>> equation.value(r=50.3, t=30, check_range=False)
array(50.4719306)

Equation dataclass ¤

Equation(
    value: Evaluable,
    uncertainty: Evaluable,
    unit: str,
    degree_freedom: float = float("inf"),
    comment: str = "",
)

Represents the equation element in an equipment register.

Parameters:

Name Type Description Default
value Evaluable

The equation to evaluate to calculate the corrected value.

required
uncertainty Evaluable

The equation to evaluate to calculate the standard uncertainty.

required
unit str

The unit of the measured quantity.

required
degree_freedom float

The degrees of freedom.

float('inf')
comment str

A comment to associate with the equation.

''

comment class-attribute instance-attribute ¤

comment: str = ''

A comment associated with the equation.

degree_freedom class-attribute instance-attribute ¤

degree_freedom: float = float('inf')

The degrees of freedom.

uncertainty instance-attribute ¤

uncertainty: Evaluable

The equation to evaluate to calculate the standard uncertainty.

unit instance-attribute ¤

unit: str

The unit of the measured quantity.

value instance-attribute ¤

value: Evaluable

The equation to evaluate to calculate the corrected value.

from_xml classmethod ¤

from_xml(element: Element[str]) -> Equation

Convert an XML element into an Equation instance.

Parameters:

Name Type Description Default
element Element[str]

An equation XML element from an equipment register.

required

Returns:

Type Description
Equation

The Equation instance.

Source code in src/msl/equipment/schema.py
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
@classmethod
def from_xml(cls, element: Element[str]) -> Equation:
    """Convert an XML element into an [Equation][msl.equipment.schema.Equation] instance.

    Args:
        element: An [equation][type_equation] XML element from an equipment register.

    Returns:
        The [Equation][msl.equipment.schema.Equation] instance.
    """
    # Schema forces order
    value = element[0]
    uncertainty = element[1]
    ranges = {
        r.attrib["variable"]: Range(minimum=float(r[0].text or -np.inf), maximum=float(r[1].text or np.inf))
        for r in element[3]
    }

    return cls(
        value=Evaluable(
            equation=value.text or "", variables=tuple(value.attrib["variables"].split()), ranges=ranges
        ),
        uncertainty=Evaluable(
            equation=uncertainty.text or "", variables=tuple(uncertainty.attrib["variables"].split()), ranges=ranges
        ),
        unit=element[2].text or "",
        degree_freedom=float(element[4].text or np.inf) if len(element) > 4 else np.inf,  # noqa: PLR2004
        comment=element.attrib.get("comment", ""),
    )

to_xml ¤

to_xml() -> Element[str]

Convert the Equation class into an XML element.

Returns:

Type Description
Element[str]

The Equation as an XML element.

Source code in src/msl/equipment/schema.py
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
def to_xml(self) -> Element[str]:
    """Convert the [Equation][msl.equipment.schema.Equation] class into an XML element.

    Returns:
        The [Equation][msl.equipment.schema.Equation] as an XML element.
    """
    attrib = {"comment": self.comment} if self.comment else {}
    e = Element("equation", attrib=attrib)
    value = SubElement(e, "value", attrib={"variables": " ".join(self.value.variables)})
    value.text = self.value.equation
    uncertainty = SubElement(e, "uncertainty", attrib={"variables": " ".join(self.uncertainty.variables)})
    uncertainty.text = self.uncertainty.equation
    unit = SubElement(e, "unit")
    unit.text = self.unit

    ranges = SubElement(e, "ranges")
    for name, _range in self.value.ranges.items():  # self.value.ranges and self.uncertainty.ranges are the same
        rng = SubElement(ranges, "range", attrib={"variable": name})
        mn = SubElement(rng, "minimum")
        mn.text = str(_range.minimum)
        mx = SubElement(rng, "maximum")
        mx.text = str(_range.maximum)

    if not isinf(self.degree_freedom):
        dof = SubElement(e, "degreeFreedom")
        dof.text = str(self.degree_freedom)

    return e

Evaluable dataclass ¤

Evaluable(
    equation: str,
    variables: tuple[str, ...] = (),
    ranges: dict[str, Range] = dict(),
)

Represents the <value> and <uncertainty> XML elements in an equation.

Parameters:

Name Type Description Default
equation str

The string representation of the equation to evaluate.

required
variables tuple[str, ...]

The names of the variables in the equation.

()
ranges dict[str, Range]

The numeric range for a variable that the equation is valid for. The keys are the variable names. A range does not need to be defined for every variable. If a range is not defined then a range of \([-\infty, +\infty]\) is assumed.

dict()

equation instance-attribute ¤

equation: str

The string representation of the equation to evaluate.

ranges class-attribute instance-attribute ¤

ranges: dict[str, Range] = field(default_factory=dict)

The numeric range for each variable that the equation is valid for. The keys are the variable names.

variables class-attribute instance-attribute ¤

variables: tuple[str, ...] = ()

The names of the variables in the equation.

Range ¤

Bases: NamedTuple

The numeric range of a variable that an equation is valid for.

Parameters:

Name Type Description
minimum float

Minimum value in range.

maximum float

Maximum value in range.

maximum instance-attribute ¤

maximum: float

Maximum value in range.

minimum instance-attribute ¤

minimum: float

Minimum value in range.

check_within_range ¤

check_within_range(
    value: float | ArrayLike,
) -> Literal[True]

Check that the values are within the range.

Parameters:

Name Type Description Default
value float | ArrayLike

The values to check, raises

required

Returns:

Type Description
Literal[True]

Always returns True. Raises ValueError if value is not within the range.

Source code in src/msl/equipment/schema.py
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def check_within_range(self, value: float | ArrayLike) -> Literal[True]:
    """Check that the values are within the range.

    Args:
        value: The values to check, raises

    Returns:
        Always returns `True`. Raises [ValueError][] if
            `value` is not within the range.
    """
    if isinstance(value, (int, float)) or (isinstance(value, np.ndarray) and value.ndim == 0):
        if value < self.minimum or value > self.maximum:
            msg = f"The value {value} is not within the range [{self.minimum}, {self.maximum}]"
            raise ValueError(msg)
    elif np.any(np.less(value, self.minimum)) or np.any(np.greater(value, self.maximum)):  # pyright: ignore[reportUnknownArgumentType]
        msg = f"A value in the sequence is not within the range [{self.minimum}, {self.maximum}]"
        raise ValueError(msg)
    return True