"""
The SI Units: "For all people, for all time"
A module to model the seven SI base units:
kg
cd m
SI
mol s
K A
...and other derived and non-SI units for practical calculations.
"""
# Copyright 2020 Connor Ferster
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
__version__ = "2.7.1"
from fractions import Fraction
from typing import Union, Optional
from forallpeople.dimensions import Dimensions
import forallpeople.physical_helper_functions as phf
import forallpeople.tuplevector as vec
from forallpeople.environment import Environment
import math
import builtins
import sys
import warnings
NUMBER = (int, float)
[docs]
class Physical(object):
"""
A class that defines any physical quantity that can be described
within the BIPM SI unit system.
"""
__slots__ = ("value", "dimensions", "factor", "precision", "prefixed")
def __init__(
self,
value: float,
dimensions: Dimensions,
factor: float,
precision: int = 3,
prefixed: str = "",
):
"""Constructor"""
super(Physical, self).__setattr__("value", float(value))
super(Physical, self).__setattr__("dimensions", dimensions)
super(Physical, self).__setattr__("factor", factor)
super(Physical, self).__setattr__("precision", precision)
super(Physical, self).__setattr__("prefixed", prefixed)
### API Methods ###
@property
def latex(self) -> str:
return self._repr_latex_()
@property
def html(self) -> str:
return self._repr_html_()
[docs]
def prefix(self, prefixed: str = "") -> Physical:
"""
Return a Physical instance with 'prefixed' property set to 'prefix'
if 'prefixed' is set to "unity" then the unit will be forced into its
unprefixed state.
"""
if self.factor != 1:
raise AttributeError(
"Cannot set a prefix on a Physical if it has a factor."
)
# check if eligible for prefixing; do not rely on __repr__ to ignore it
return Physical(
self.value, self.dimensions, self.factor, self.precision, prefixed
)
@property
def repr(self) -> str:
"""
Returns a traditional Python string representation of the Physical instance.
"""
repr_str = (
"Physical(value={}, dimensions={}, factor={:.5}, precision={}, prefixed={})"
)
factor = float(self.factor)
if self.factor == 1:
repr_str = "Physical(value={}, dimensions={}, factor={}, precision={}, prefixed={})"
factor = 1
return repr_str.format(
self.value, self.dimensions, factor, self.precision, self.prefixed
) # check
def round(self, n: int):
"""
Returns a new Physical with a new precision, 'n'. Precision controls
the number of decimal places displayed in repr and str.
"""
warnings.warn(
"Using .round() is going to be deprecated. "
"Use Python's built-in round() function instead.",
DeprecationWarning,
)
return round(self, n)
[docs]
def split(self, base_value: bool = True) -> tuple:
"""
Returns a tuple separating the value of `self` with the units of `self`.
If base_value is True, then the value will be the value in base units. If False, then
the apparent value of `self` will be used.
This method is to allow flexibility in working with Physical instances when working
with numerically optimized libraries such as numpy which cannot accept non-numerical
objects in some of their operations (such as in matrix inversion).
"""
if base_value:
return (
self.value * float(self.factor),
Physical(
1 / float(self.factor), self.dimensions, self.factor, self.precision
),
)
return (float(self), Physical(1, self.dimensions, self.factor, self.precision))
def sqrt(self, n: Union[int, float] = 2):
"""
Returns a Physical instance that represents the square root of `self`.
`n` can be set to an alternate number to compute an alternate root (e.g. 3.0 for cube root)
"""
return self ** (1 / n)
[docs]
def to(self, unit_name="") -> Optional[Physical]:
"""
Returns a Physical instance representing self converted into one of the available
conversion units for its dimension.
If .to() is called without any arguments, then it will return None and instead
print a list of available conversion units.
"""
dims = self.dimensions
env_dims = environment.units_by_dimension
derived = env_dims()["derived"]
defined = env_dims()["defined"]
power, dims_orig = phf._powers_of_derived(dims, env_dims)
if not unit_name:
print("Available units to convert to: ")
for key in derived.get(dims_orig, {}):
print(key)
for key in defined.get(dims_orig, {}):
print(key)
if unit_name:
defined_match = defined.get(dims_orig, {}).get(unit_name, {})
derived_match = derived.get(dims_orig, {}).get(unit_name, {})
unit_match = defined_match or derived_match
if not unit_match:
warnings.warn(f"No unit defined for '{unit_name}' on {self}.")
new_factor = unit_match.get("Factor", 1) ** Fraction(power)
return Physical(self.value, self.dimensions, new_factor, self.precision)
def si(self):
"""
Return a new Physical instance with self.factor set to 1, thereby returning
the instance to SI units display.
"""
return Physical(self.value, self.dimensions, 1, self.precision)
### repr Methods (the "workhorse" of Physical) ###
def __repr__(self):
return self._repr_template_()
def _repr_html_(self):
return self._repr_template_(template="html")
def _repr_markdown_(self):
return self._repr_template_(template="html")
def _repr_latex_(self):
return self._repr_template_(template="latex")
def _repr_template_(self, template: str = "", format_spec="") -> str:
"""
Returns a string that appropriately represents the Physical
instance. The parameter,'template', allows two optional values:
'html' and 'latex'. which will only be utilized if the Physical
exists in the Jupyter/iPython environment.
"""
if not format_spec:
format_spec = f".{self.precision}f"
dims = self.dimensions
factor = self.factor
val = self.value
prefix = ""
prefixed = self.prefixed
kg_bool = False
# Access external environment
env_fact = environment.units_by_factor or dict()
env_dims = environment.units_by_dimension or dict()
# Do the expensive vector math method (call once, only)
power, dims_orig = phf._powers_of_derived(dims, env_dims)
# Determine if there is a symbol for these dimensions in the environment
# and if the quantity is eligible to be prefixed
# mod_factor is either the original factor or the new default factor
# if a default look-up was triggered
symbol, prefix_bool, mod_factor = phf._evaluate_dims_and_factor(
dims_orig, factor, power, env_fact, env_dims
)
factor = mod_factor
float_factor = float(factor)
# Get the appropriate prefix
if prefix_bool and prefixed == "unity":
prefix = ""
if dims_orig == Dimensions(1, 0, 0, 0, 0, 0, 0):
kg_bool = True
elif prefix_bool and prefixed:
prefix = prefixed
elif prefix_bool and dims_orig == Dimensions(1, 0, 0, 0, 0, 0, 0):
kg_bool = True
prefix = phf._auto_prefix(val, power, kg=kg_bool)
elif prefix_bool:
prefix = phf._auto_prefix(val, power, kg=kg_bool)
# Format the exponent (may not be used, though)
exponent = phf._format_exponent(power, repr_format=template)
# Format the units
if not symbol and phf._dims_basis_multiple(dims):
components = phf._get_unit_components_from_dims(dims)
units_symbol = phf._get_unit_string(components, repr_format=template)
units = units_symbol
units = phf._format_symbol(prefix, units_symbol, repr_format=template)
exponent = ""
elif not symbol:
components = phf._get_unit_components_from_dims(dims)
units_symbol = phf._get_unit_string(components, repr_format=template)
units = units_symbol
exponent = ""
else:
units = phf._format_symbol(prefix, symbol, repr_format=template)
# Determine the appropriate display value
value = val * float_factor
if prefix_bool:
# If the quantity has a "pre-fixed" prefix, it will override
# the value generated in _auto_prefix_value
value = phf._auto_prefix_value(val, power, prefix, kg_bool)
pre_super = ""
post_super = ""
space = " "
pre_inline = ""
post_inline = ""
if template == "latex":
space = r"\ "
pre_super = "^{"
post_super = "}"
pre_inline = "$"
post_inline = "$"
elif template == "html":
space = " "
pre_super = "<sup>"
post_super = "</sup>"
if not exponent:
pre_super = ""
post_super = ""
formatted_value = f"{value:{format_spec}}"
if "e" in format_spec.lower():
formatted_value = phf.format_scientific_notation(
formatted_value, template=template
)
return f"{pre_inline}{formatted_value}{space}{units}{pre_super}{exponent}{post_super}{post_inline}"
### "Magic" Methods ###
def __float__(self):
value = self.value
factor = float(self.factor)
if factor != 1.0:
return value * factor
kg_bool = False
dims = self.dimensions
env_dims = environment.units_by_dimension or dict()
power, _ = phf._powers_of_derived(dims, env_dims)
dim_components = phf._get_unit_components_from_dims(dims)
if len(dim_components) == 1 and dim_components[0][0] == "kg":
kg_bool = True
if self.prefixed:
prefix = self.prefixed
else:
prefix = phf._auto_prefix(value, power, kg_bool)
float_value = phf._auto_prefix_value(value, power, prefix, kg_bool)
return float_value
def __int__(self):
return int(float(self))
def __neg__(self):
return self * -1
def __abs__(self):
if self.value < 0:
return self * -1
return self
def __bool__(self):
return True
def __format__(self, format_spec=""):
template = ""
if "L" in format_spec:
template = "latex"
format_spec = format_spec.replace("L", "")
elif "H" in format_spec:
template = "html"
format_spec = format_spec.replace("H", "")
return self._repr_template_(template=template, format_spec=format_spec)
def __hash__(self):
return hash(
(self.value, self.dimensions, self.factor, self.precision, self.prefixed)
)
def __round__(self, n=0):
return Physical(self.value, self.dimensions, self.factor, n, self.prefixed)
def __contains__(self, other):
return False
def __eq__(self, other):
if isinstance(other, NUMBER):
return math.isclose(self.value, other)
elif type(other) == str:
return False
elif isinstance(other, Physical) and self.dimensions == other.dimensions:
return math.isclose(self.value, other.value)
else:
raise ValueError(
"Can only compare between Physical instances of equal dimension."
)
def __gt__(self, other):
if isinstance(other, NUMBER):
return self.value > other
elif isinstance(other, Physical) and self.dimensions == other.dimensions:
return self.value > other.value
else:
raise ValueError(
"Can only compare between Physical instances of equal dimension."
)
def __ge__(self, other):
if isinstance(other, NUMBER):
return self.value >= other
elif isinstance(other, Physical) and self.dimensions == other.dimensions:
return self.value >= other.value
else:
raise ValueError(
"Can only compare between Physical instances of equal dimension."
)
def __lt__(self, other):
if isinstance(other, NUMBER):
return self.value < other
elif isinstance(other, Physical) and self.dimensions == other.dimensions:
return self.value < other.value
else:
raise ValueError(
"Can only compare between Physical instances of equal dimension."
)
def __le__(self, other):
if isinstance(other, NUMBER):
return self.value <= other
elif isinstance(other, Physical) and self.dimensions == other.dimensions:
return self.value <= other.value
else:
raise ValueError(
"Can only compare between Physical instances of equal dimension."
)
def __add__(self, other):
if other != other: # Test for nans
return other
if isinstance(other, Physical):
if self.dimensions == other.dimensions:
try:
return Physical(
self.value + other.value,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
except:
raise ValueError(
f"Cannot add between {self} and {other}: "
+ ".value attributes are incompatible."
)
else:
raise ValueError(
f"Cannot add between {self} and {other}: "
+ ".dimensions attributes are incompatible (not equal)"
)
else:
try:
other = other / float(self.factor)
return Physical(
self.value + other,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
except:
raise ValueError(
f"Cannot add between {self} and {other}: "
+ ".value attributes are incompatible."
)
def __radd__(self, other):
return self.__add__(other)
def __iadd__(self, other):
raise ValueError(
"Cannot incrementally add Physical instances because they are immutable."
+ " Use 'a = a + b', to make the operation explicit."
)
def __sub__(self, other):
if other != other: # Test for nans
return other
if isinstance(other, Physical):
if self.dimensions == other.dimensions:
try:
return Physical(
self.value - other.value,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
except:
raise ValueError(f"Cannot subtract between {self} and {other}")
else:
raise ValueError(
f"Cannot subtract between {self} and {other}:"
+ ".dimensions attributes are incompatible (not equal)"
)
else:
try:
other = other / float(self.factor)
return Physical(
self.value - other,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
except:
raise ValueError(
f"Cannot subtract between {self} and {other}: "
+ ".value attributes are incompatible."
)
def __rsub__(self, other):
if isinstance(other, Physical):
return self.__sub__(other)
else:
try:
other = other / float(self.factor)
return Physical(
other - self.value,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
except:
raise ValueError(
f"Cannot subtract between {self} and {other}: "
+ ".value attributes are incompatible."
)
def __isub__(self, other):
raise ValueError(
"Cannot incrementally subtract Physical instances because they are immutable."
+ " Use 'a = a - b', to make the operation explicit."
)
def __mul__(self, other):
if other != other: # Test for nans
return other
elif isinstance(other, NUMBER):
return Physical(
self.value * other,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
elif isinstance(other, Physical):
new_dims = vec.add(self.dimensions, other.dimensions)
new_power, new_dims_orig = phf._powers_of_derived(
new_dims, environment.units_by_dimension
)
new_factor = self.factor * other.factor
test_factor = phf._get_units_by_factor(
new_factor, new_dims_orig, environment.units_by_factor, new_power
)
# if not test_factor:
# print(new_factor)
# new_factor = 1
try:
new_value = self.value * other.value
except:
raise ValueError(
f"Cannot multiply between {self} and {other}: "
+ ".value attributes are incompatible."
)
if new_dims == Dimensions(0, 0, 0, 0, 0, 0, 0):
return new_value
else:
return Physical(new_value, new_dims, new_factor, self.precision)
else:
try:
return Physical(
self.value * other, self.dimensions, self.factor, self.precision
)
except:
raise ValueError(
f"Cannot multiply between {self} and {other}: "
+ ".value attributes are incompatible."
)
def __imul__(self, other):
raise ValueError(
"Cannot incrementally multiply Physical instances because they are immutable."
+ " Use 'a = a * b' to make the operation explicit."
)
def __rmul__(self, other):
return self.__mul__(other)
def __truediv__(self, other):
if other != other: # Test for nans
return other
elif isinstance(other, NUMBER):
return Physical(
self.value / other,
self.dimensions,
self.factor,
self.precision,
self.prefixed,
)
elif isinstance(other, Physical):
new_dims = vec.subtract(self.dimensions, other.dimensions)
new_power, new_dims_orig = phf._powers_of_derived(
new_dims, environment.units_by_dimension
)
new_factor = self.factor / other.factor
# if not phf._get_units_by_factor(
# new_factor, new_dims_orig, environment.units_by_factor, new_power
# ):
# new_factor = 1
try:
new_value = self.value / other.value
except:
raise ValueError(
f"Cannot divide between {self} and {other}: "
+ ".value attributes are incompatible."
)
if new_dims == Dimensions(0, 0, 0, 0, 0, 0, 0):
return new_value
else:
return Physical(new_value, new_dims, new_factor, self.precision)
else:
try:
return Physical(
self.value / other, self.dimensions, self.factor, self.precision
)
except:
raise ValueError(
f"Cannot divide between {self} and {other}: "
+ ".value attributes are incompatible."
)
def __rtruediv__(self, other):
if other != other: # Test for nans
return other
if isinstance(other, NUMBER):
new_value = other / self.value
new_dimensions = vec.multiply(self.dimensions, -1)
new_factor = self.factor**-1 # added new_factor
return Physical(
new_value,
new_dimensions,
new_factor, # updated from self.factor to new_factor
self.precision,
)
else:
try:
return Physical(
other / self.value,
vec.multiply(self.dimensions, -1),
self.factor**-1, # updated to ** -1
self.precision,
)
except:
raise ValueError(
f"Cannot divide between {other} and {self}: "
+ ".value attributes are incompatible."
)
def __itruediv__(self, other):
raise ValueError(
"Cannot incrementally divide Physical instances because they are immutable."
+ " Use 'a = a / b' to make the operation explicit."
)
def __pow__(self, other):
if other != other: # Test for nans
return other
if isinstance(other, NUMBER):
if self.prefixed:
return float(self) ** other
new_value = self.value**other
new_dimensions = vec.multiply(self.dimensions, other)
new_factor = phf.fraction_pow(self.factor, other)
if new_dimensions == Dimensions(0, 0, 0, 0, 0, 0, 0):
return float(new_value * new_factor)
return Physical(new_value, new_dimensions, new_factor, self.precision)
else:
raise ValueError(
"Cannot raise a Physical to the power of \
another Physical -> ({self}**{other})".format(
self, other
)
)
# The seven SI base units...
_the_si_base_units = {
"kg": Physical(1, Dimensions(1, 0, 0, 0, 0, 0, 0), 1),
"m": Physical(1, Dimensions(0, 1, 0, 0, 0, 0, 0), 1),
"s": Physical(1, Dimensions(0, 0, 1, 0, 0, 0, 0), 1),
"A": Physical(1, Dimensions(0, 0, 0, 1, 0, 0, 0), 1),
"cd": Physical(1, Dimensions(0, 0, 0, 0, 1, 0, 0), 1),
"K": Physical(1, Dimensions(0, 0, 0, 0, 0, 1, 0), 1),
"mol": Physical(1, Dimensions(0, 0, 0, 0, 0, 0, 1), 1),
}
environment = Environment(Physical, builtins, _the_si_base_units)
environment._push_vars(_the_si_base_units, sys.modules[__name__])