1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
|
"""
pint.facets.measurement.objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
import copy
import re
from typing import Generic
from ...compat import ufloat
from ..plain import MagnitudeT, PlainQuantity, PlainUnit
MISSING = object()
class MeasurementQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]):
# Measurement support
def plus_minus(self, error, relative=False):
if isinstance(error, self.__class__):
if relative:
raise ValueError(f"{error} is not a valid relative error.")
error = error.to(self._units).magnitude
else:
if relative:
error = error * abs(self.magnitude)
return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units)
class MeasurementUnit(PlainUnit):
pass
class Measurement(PlainQuantity):
"""Implements a class to describe a quantity with uncertainty.
Parameters
----------
value : pint.Quantity or any numeric type
The expected value of the measurement
error : pint.Quantity or any numeric type
The error or uncertainty of the measurement
Returns
-------
"""
def __new__(cls, value, error=MISSING, units=MISSING):
if units is MISSING:
try:
value, units = value.magnitude, value.units
except AttributeError:
# if called with two arguments and the first looks like a ufloat
# then assume the second argument is the units, keep value intact
if hasattr(value, "nominal_value"):
units = error
error = MISSING # used for check below
else:
units = ""
if error is MISSING:
# We've already extracted the units from the Quantity above
mag = value
else:
try:
error = error.to(units).magnitude
except AttributeError:
pass
if error < 0:
raise ValueError("The magnitude of the error cannot be negative")
else:
mag = ufloat(value, error)
inst = super().__new__(cls, mag, units)
return inst
@property
def value(self):
return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units)
@property
def error(self):
return self._REGISTRY.Quantity(self.magnitude.std_dev, self.units)
@property
def rel(self):
return abs(self.magnitude.std_dev / self.magnitude.nominal_value)
def __reduce__(self):
# See notes in Quantity.__reduce__
from pint import _unpickle_measurement
return _unpickle_measurement, (Measurement, self.magnitude, self._units)
def __repr__(self):
return "<Measurement({}, {}, {})>".format(
self.magnitude.nominal_value, self.magnitude.std_dev, self.units
)
def __str__(self):
return f"{self}"
def __format__(self, spec):
spec = spec or self._REGISTRY.default_format
return self._REGISTRY.formatter.format_measurement(self, spec)
def old_format(self, spec):
# TODO: provisional
from ...formatting import _FORMATS, extract_custom_flags, siunitx_format_unit
# special cases
if "Lx" in spec: # the LaTeX siunitx code
# the uncertainties module supports formatting
# numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45),
# using type code 'S', which siunitx actually accepts as input.
# However, the implementation is incompatible with siunitx.
# Uncertainties will do 9.1(1.1), which is invalid, should be 9.1(11).
# TODO: add support for extracting options
#
# Get rid of this code, we'll deal with it here
spec = spec.replace("Lx", "")
# The most compatible format from uncertainties is the default format,
# but even this requires fixups.
# For one, SIUnitx does not except some formats that unc does, like 'P',
# and 'S' is broken as stated, so...
spec = spec.replace("S", "").replace("P", "")
# get SIunitx options
# TODO: allow user to set this value, somehow
opts = _FORMATS["Lx"]["siopts"]
if opts != "":
opts = r"[" + opts + r"]"
# SI requires space between "+-" (or "\pm") and the nominal value
# and uncertainty, and doesn't accept "+/-", so this setting
# selects the desired replacement.
pm_fmt = _FORMATS["Lx"]["pm_fmt"]
mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt)
# Also, SIunitx doesn't accept parentheses, which uncs uses with
# scientific notation ('e' or 'E' and sometimes 'g' or 'G').
mstr = mstr.replace("(", "").replace(")", " ")
ustr = siunitx_format_unit(self.units._units.items(), self._REGISTRY)
return rf"\SI{opts}{{{mstr}}}{{{ustr}}}"
# standard cases
if "L" in spec:
newpm = pm = r" \pm "
pars = _FORMATS["L"]["parentheses_fmt"]
elif "P" in spec:
newpm = pm = "±"
pars = _FORMATS["P"]["parentheses_fmt"]
else:
newpm = pm = "+/-"
pars = _FORMATS[""]["parentheses_fmt"]
if "C" in spec:
sp = ""
newspec = spec.replace("C", "")
pars = _FORMATS["C"]["parentheses_fmt"]
else:
sp = " "
newspec = spec
if "H" in spec:
newpm = "±"
newspec = spec.replace("H", "")
pars = _FORMATS["H"]["parentheses_fmt"]
mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp)
if "(" in mag:
# Exponential format has its own parentheses
pars = "{}"
if "L" in newspec and "S" in newspec:
mag = mag.replace("(", r"\left(").replace(")", r"\right)")
if "L" in newspec:
space = r"\ "
else:
space = " "
uspec = extract_custom_flags(spec)
ustr = format(self.units, uspec)
if not ("uS" in newspec or "ue" in newspec or "u%" in newspec):
mag = pars.format(mag)
if "H" in spec:
# Fix exponential format
mag = re.sub(r"\)e\+0?(\d+)", r")×10<sup>\1</sup>", mag)
mag = re.sub(r"\)e-0?(\d+)", r")×10<sup>-\1</sup>", mag)
return mag + space + ustr
|