diff --git a/src/dsiUnits.py b/src/dsiUnits.py index 57ac184a90c6d38c4f2eccdd207debdeb89292a7..aa6bb04bb84df6e35d312954d90e1a274badc6be 100644 --- a/src/dsiUnits.py +++ b/src/dsiUnits.py @@ -267,9 +267,6 @@ class dsiUnit: self._latexDefaultPrefix = latexDefaultPrefix self._latexDefaultSuffix = latexDefaultSuffix - - - def toLatex(self, wrapper=None, prefix=None, suffix=None): """converts D-SI unit string to LaTeX @@ -423,10 +420,10 @@ class dsiUnit: i += 1 # Calculate overall scale factor and apply it to the first node - overall_scale_factor = 1.0 + overall_scale_factor = Decimal(1) for node in consolidated_nodes: overall_scale_factor *= node.scaleFactor - node.scaleFactor = 1.0 # Reset scale factor for individual nodes + node.scaleFactor = Decimal(1) # Reset scale factor for individual nodes # Sort nodes alphabetically by unit consolidated_nodes.sort(key=lambda x: x.unit) # Apply overall scale factor to the first node, if it exists @@ -437,14 +434,14 @@ class dsiUnit: if node.exponent != 0: nodesWOPowerZero.append(node) if len(nodesWOPowerZero) == 0: # ok all nodes have ben power of zero so we deleted them so we end up with one as unit and 1.0 as exponent - nodesWOPowerZero.append(_node("", "one", 1.0,scaleFactor=overall_scale_factor)) + nodesWOPowerZero.append(_node("", "one", Decimal(1),scaleFactor=overall_scale_factor)) consolidated_nodes=nodesWOPowerZero # Check for ones and delete them if they are not the only node ad set there exponent to 1.0 since 1^x = 1 if len(consolidated_nodes) > 1: consolidated_nodes = [node for node in consolidated_nodes if node.unit != "one"] else: if consolidated_nodes[0].unit == "one": - consolidated_nodes[0].exponent=1.0 + consolidated_nodes[0].exponent=Decimal(1) # Create and return a new instance of _dsiTree with consolidated nodes return dsiUnit(self.dsiString, [consolidated_nodes], self.warnings, self._latexDefaultWrapper, self._latexDefaultPrefix, self._latexDefaultSuffix) @@ -475,7 +472,7 @@ class dsiUnit: self.tree[0].append(_node("", node.unit, node.exponent)) self.tree[1].remove(node) if len(self.tree[0]) == 0: - self.tree[0].append(_node("", "one", 1.0)) + self.tree[0].append(_node("", "one", Decimal(1))) return self @@ -519,7 +516,7 @@ class dsiUnit: sortedOther.sortTree() # okay now check if is identical if sortedSelf.tree == sortedOther.tree: - return (1.0,self) + return (Decimal(1),self) scaleFactor=1 for fracIdx,unitFraction in enumerate(sortedSelf.tree): try: @@ -545,7 +542,7 @@ class dsiUnit: if len(selfBaseUnitTree.tree) != len(otherBaseUnitTree.tree): return (math.nan, None) # Calculate scale factor - scaleFactor = 1.0 + scaleFactor = Decimal(1) if len(selfBaseUnitTree.tree) != 1 or len(otherBaseUnitTree.tree) != 1: raise RuntimeError("D-SI tree with more than one fraction cannot be compared. And should not exist here since we consolidated earlier") for selfNode, otherNode in zip(selfBaseUnitTree.tree[0], otherBaseUnitTree.tree[0]): @@ -557,7 +554,7 @@ class dsiUnit: # resetting scaleFactor to 1.0 for unitFraction in selfBaseUnitTree.tree: for node in unitFraction: - node.scaleFactor = 1.0 + node.scaleFactor = Decimal(1) return (scaleFactor,selfBaseUnitTree) def __str__(self): @@ -581,12 +578,10 @@ class dsiUnit: def __pow__(self, other): - if not isinstance(other, numbers.Real): - raise TypeError("Exponent must be a real number") resultNodeLIst = deepcopy(self.tree) for unitFraction in resultNodeLIst: for node in unitFraction: - node.exponent *= other + node.exponent *= Decimal(other) resultTree =dsiUnit("", resultNodeLIst, self.warnings, self._latexDefaultWrapper, self._latexDefaultPrefix, self._latexDefaultSuffix) resultTree = resultTree.reduceFraction() if len(self.tree)==2: # check if we had a per representation @@ -615,15 +610,15 @@ class dsiUnit: def __truediv__(self, other): if dsiConfigConfiginstance.createPerByDivision: - return (self * (other**-1)).negExponentsToPer() + return (self * (other**Decimal(-1))).negExponentsToPer() else: - return self * (other ** -1) + return self * (other ** -Decimal(1)) class _node: """one node of the D-SI tree, containing prefix, unit, power """ - def __init__(self, prefix: str,unit: str,exponent: Fraction=Fraction(1), valid:bool=True,scaleFactor: float = 1.0 ):# Adding scale factor with default value 1.0 + def __init__(self, prefix: str,unit: str,exponent: Fraction=Fraction(1), valid:bool=True,scaleFactor: Decimal = Decimal(1) ):# Adding scale factor with default value 1.0 self.prefix=prefix self.unit=unit self.valid=valid @@ -639,7 +634,7 @@ class _node: exponent=exponent warnings.warn(f"Exponent «{exponent}» is not a number!", RuntimeWarning) self.exponent=exponent - self.scaleFactor=scaleFactor # Adding scale factor with default value 1.0 + self.scaleFactor=Decimal(scaleFactor) # Adding scale factor with default value 1.0 def toLatex(self): """generates a latex string from a node @@ -695,7 +690,7 @@ class _node: List['_node']: List of nodes representing the base units or kg, s, m equivalents. """ # Adjust the scale factor for the prefix - prefixScale = _dsiPrefixesScales.get(self.prefix, 1) # Default to 1 if no prefix + prefixScale = _dsiPrefixesScales.get(self.prefix, Decimal(1)) # Default to 1 if no prefix adjustedScaleFactor = self.scaleFactor * prefixScale # Convert to base units if it's a derived unit @@ -704,7 +699,7 @@ class _node: baseUnits = [] for i, (baseUnit, exponent, scaleFactor) in enumerate(baseUnitsInfo): # Apply the adjusted scale factor only to the first base unit - finalScaleFactor = math.pow(adjustedScaleFactor * scaleFactor, self.exponent) if i == 0 else 1.0 + finalScaleFactor = math.pow(adjustedScaleFactor * scaleFactor, self.exponent) if i == 0 else Decimal(1) baseUnits.append(_node('', baseUnit, exponent * self.exponent, scaleFactor=finalScaleFactor)) return baseUnits elif complete: @@ -713,7 +708,7 @@ class _node: kgsUnitsInfo = _additionalConversions[self.unit] kgsUnits = [] for i, (kgsUnit, exponent, scaleFactor) in enumerate(kgsUnitsInfo): - finalScaleFactor = math.pow(adjustedScaleFactor * scaleFactor, self.exponent) if i == 0 else 1.0 + finalScaleFactor = math.pow(adjustedScaleFactor * scaleFactor, self.exponent) if i == 0 else Decimal(1) kgsUnits.append(_node('', kgsUnit, exponent * self.exponent, scaleFactor=finalScaleFactor)) return kgsUnits @@ -801,35 +796,35 @@ _dsiPrefixesLatex = { #TODO maybe directlusing the exponents is better # mapping D-SI prefixes to scale factors _dsiPrefixesScales = { - 'yotta': 1e24, - 'zetta': 1e21, - 'exa': 1e18, - 'peta': 1e15, - 'tera': 1e12, - 'giga': 1e9, - 'mega': 1e6, - 'kilo': 1e3, - 'hecto': 1e2, - 'deca': 1e1, - '':1.0, - 'deci': 1e-1, - 'centi': 1e-2, - 'milli': 1e-3, - 'micro': 1e-6, - 'nano': 1e-9, - 'pico': 1e-12, - 'femto': 1e-15, - 'atto': 1e-18, - 'zepto': 1e-21, - 'yocto': 1e-24, - 'kibi': 1024, #2^10 - 'mebi': 1048576, #2^20 - 'gibi': 1073741824, #2^30 - 'tebi': 1099511627776, #2^40 - 'pebi': 1125899906842624, #2^50 - 'exbi': 1152921504606846976, #2^60 larger than 2^53 so quantization error is possible - 'zebi': 1180591620717411303424, #2^70 larger than 2^53 so quantization error is possible - 'yobi': 1208925819614629174706176 #2^80 larger than 2^53 so quantization error is possible + 'yotta': Decimal(1e24), + 'zetta': Decimal(1e21), + 'exa': Decimal(1e18), + 'peta': Decimal(1e15), + 'tera': Decimal(1e12), + 'giga': Decimal(1e9), + 'mega': Decimal(1e6), + 'kilo': Decimal(1e3), + 'hecto': Decimal(1e2), + 'deca': Decimal(1e1), + '':Decimal(1), + 'deci': Decimal(1e-1), + 'centi': Decimal(1e-2), + 'milli': Decimal(1e-3), + 'micro': Decimal(1e-6), + 'nano': Decimal(1e-9), + 'pico': Decimal(1e-12), + 'femto': Decimal(1e-15), + 'atto': Decimal(1e-18), + 'zepto': Decimal(1e-21), + 'yocto': Decimal(1e-24), + 'kibi': Decimal(1024), #2^10 + 'mebi': Decimal(1048576), #2^20 + 'gibi': Decimal(1073741824), #2^30 + 'tebi': Decimal(1099511627776), #2^40 + 'pebi': Decimal(1125899906842624), #2^50 + 'exbi': Decimal(1152921504606846976), #2^60 + 'zebi': Decimal(1180591620717411303424), #2^70 + 'yobi': Decimal(1208925819614629174706176 ) #2^80 } # UTF-8 equivalents for SI prefixes _dsiPrefixesUTF8 = { @@ -1010,59 +1005,69 @@ _dsiUnitsUTF8 = { _derivedToBaseUnits = { # Time units - 'day': [('second', 1, 86400)], # 1 day = 86400 seconds - 'hour': [('second', 1, 3600)], # 1 hour = 3600 seconds - 'minute': [('second', 1, 60)], # 1 minute = 60 seconds + 'day': [('second', 1, Decimal(86400))], # 1 day = 86400 seconds + 'hour': [('second', 1, Decimal(3600))], # 1 hour = 3600 seconds + 'minute': [('second', 1, Decimal(60))], # 1 minute = 60 seconds # Angle units - 'degree': [('radian', 1, math.pi/180)], # 1 degree = π/180 radians - 'arcminute': [('radian', 1, math.pi/10800)], # 1 arcminute = π/10800 radians - 'arcsecond': [('radian', 1, math.pi/648000)], # 1 arcsecond = π/648000 radians + 'degree': [('radian', 1, Decimal(math.pi) / Decimal(180))], # 1 degree = π/180 radians + 'arcminute': [('radian', 1, Decimal(math.pi) / Decimal(10800))], # 1 arcminute = π/10800 radians + 'arcsecond': [('radian', 1, Decimal(math.pi) / Decimal(648000))], # 1 arcsecond = π/648000 radians # Mass units - 'gram': [('kilogram', 1, 0.001)], # 1 gram = 0.001 kilograms + 'gram': [('kilogram', 1, Decimal(0.001))], # 1 gram = 0.001 kilograms # Derived units - 'hertz': [('second', -1,1)], # 1 Hz = 1/s - 'newton': [('kilogram', 1, 1), ('metre', 1, 1), ('second',-2, 1)], # 1 N = 1 kg·m/s² - 'pascal': [('kilogram', 1, 1), ('metre',-1, 1), ('second',-2, 1)], # 1 Pa = 1 kg/m·s² - 'joule': [('kilogram', 1, 1), ('metre',2, 1), ('second',-2, 1)], # 1 J = 1 kg·m²/s² - 'watt': [('kilogram', 1, 1), ('metre',2, 1), ('second',-3, 1)], # 1 W = 1 kg·m²/s³ - 'coulomb': [('second', 1, 1), ('ampere', 1, 1)], # 1 C = 1 s·A - 'volt': [('kilogram', 1, 1), ('metre',2, 1), ('second',-3, 1), ('ampere',-1, 1)], # 1 V = 1 kg·m²/s³·A - 'farad': [('kilogram',-1, 1), ('metre',-2, 1), ('second', 4, 1), ('ampere',2, 1)],# 1 F = 1 kg⁻¹·m⁻²·s⁴·A² - 'ohm': [('kilogram', 1, 1), ('metre',2, 1), ('second',-3, 1), ('ampere',-2, 1)], # 1 Ω = 1 kg·m²/s³·A⁻² - 'siemens': [('kilogram',-1, 1), ('metre',-2, 1), ('second',3, 1), ('ampere',2, 1)],# 1 S = 1 kg⁻¹·m⁻²·s³·A² - 'weber': [('kilogram', 1, 1), ('metre',2, 1), ('second',-2, 1), ('ampere',-1, 1)], # 1 Wb = 1 kg·m²/s²·A - 'tesla': [('kilogram', 1, 1), ('second',-2, 1), ('ampere',-1, 1)], # 1 T = 1 kg/s²·A - 'henry': [('kilogram', 1, 1), ('metre',2, 1), ('second',-2, 1), ('ampere',-2, 1)], # 1 H = 1 kg·m²/s²·A² - #'degreecelsius': [('kelvin', 1, 1)], # Degree Celsius is a scale, not a unit; the unit is Kelvin - 'lumen': [('candela', 1, 1), ('steradian', 1, 1)], # 1 lm = 1 cd·sr #TODO full conversion to base units - 'lux': [('candela', 1, 1), ('steradian', 1, 1), ('metre',-2, 1)], # 1 lx = 1 cd·sr/m² #TODO full conversion to base units - 'becquerel': [('second',-1, 1)], # 1 Bq = 1/s - 'sievert': [('metre',2, 1), ('second',-2, 1)], # 1 Sv = 1 m²/s² - 'gray': [('metre',2, 1), ('second',-2, 1)], # 1 Gy = 1 m²/s² - 'katal': [('mole', 1, 1), ('second',-1, 1)], # 1 kat = 1 mol/s - # Other units - 'hectare': [('metre',2, 10000)], # 1 ha = 10000 m² - 'litre': [('metre',3, 0.001)], # 1 L = 0.001 m³ - 'tonne': [('kilogram', 1, 1000)], # 1 t = 1000 kg - 'electronvolt': [('joule', 1, 1.602176634e-19)], # 1 eV = 1.602176634 × 10⁻¹⁹ J - 'dalton': [('kilogram', 1, 1.66053906660e-27)], # 1 Da = 1.66053906660 × 10⁻²⁷ kg - 'astronomicalunit': [('metre', 1, 149597870700)], # 1 AU = 149597870700 m - 'neper': [('one', 1,1)], # Neper is a logarithmic unit for ratios of measurements, not directly convertible - 'bel': [('one', 1,1)], # Bel is a logarithmic unit for ratios of power, not directly convertible - 'decibel': [('one', 1,1)], # Decibel is a logarithmic unit for ratios of power, not directly convertible - 'byte':[('bit',1,8)], ## TODO overthink this -# Note: For logarithmic units like neper, bel, and decibel, conversion to base units is not straightforward due to their nature. + 'hertz': [('second', -1, Decimal(1))], # 1 Hz = 1/s + 'newton': [('kilogram', 1, Decimal(1)), ('metre', 1, Decimal(1)), ('second', -2, Decimal(1))], # 1 N = 1 kg·m/s² + 'pascal': [('kilogram', 1, Decimal(1)), ('metre', -1, Decimal(1)), ('second', -2, Decimal(1))], # 1 Pa = 1 kg/m·s² + 'joule': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -2, Decimal(1))], # 1 J = 1 kg·m²/s² + 'watt': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -3, Decimal(1))], # 1 W = 1 kg·m²/s³ + 'coulomb': [('second', 1, Decimal(1)), ('ampere', 1, Decimal(1))], # 1 C = 1 s·A + 'volt': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -3, Decimal(1)), + ('ampere', -1, Decimal(1))], # 1 V = 1 kg·m²/s³·A + 'farad': [('kilogram', -1, Decimal(1)), ('metre', -2, Decimal(1)), ('second', 4, Decimal(1)), + ('ampere', 2, Decimal(1))], # 1 F = 1 kg⁻¹·m⁻²·s⁴·A² + 'ohm': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -3, Decimal(1)), + ('ampere', -2, Decimal(1))], # 1 Ω = 1 kg·m²/s³·A⁻² + 'siemens': [('kilogram', -1, Decimal(1)), ('metre', -2, Decimal(1)), ('second', 3, Decimal(1)), + ('ampere', 2, Decimal(1))], # 1 S = 1 kg⁻¹·m⁻²·s³·A² + 'weber': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -2, Decimal(1)), + ('ampere', -1, Decimal(1))], # 1 Wb = 1 kg·m²/s²·A + 'tesla': [('kilogram', 1, Decimal(1)), ('second', -2, Decimal(1)), ('ampere', -1, Decimal(1))], # 1 T = 1 kg/s²·A + 'henry': [('kilogram', 1, Decimal(1)), ('metre', 2, Decimal(1)), ('second', -2, Decimal(1)), + ('ampere', -2, Decimal(1))], # 1 H = 1 kg·m²/s²·A² + # 'degreecelsius': [('kelvin', 1, 1)], # Degree Celsius is a scale, not a unit; the unit is Kelvin + 'lumen': [('candela', 1, Decimal(1)), ('steradian', 1, Decimal(1))], + # 1 lm = 1 cd·sr #TODO full conversion to base units + 'lux': [('candela', 1, Decimal(1)), ('steradian', 1, Decimal(1)), ('metre', -2, Decimal(1))], + # 1 lx = 1 cd·sr/m² #TODO full conversion to base units + 'becquerel': [('second', -1, Decimal(1))], # 1 Bq = 1/s + 'sievert': [('metre', 2, Decimal(1)), ('second', -2, Decimal(1))], # 1 Sv = 1 m²/s² + 'gray': [('metre', 2, Decimal(1)), ('second', -2, Decimal(1))], # 1 Gy = 1 m²/s² + 'katal': [('mole', 1, Decimal(1)), ('second', -1, Decimal(1))], # 1 kat = 1 mol/s + +# Other units + 'hectare': [('metre',2, Decimal(10000))], # 1 ha = 10000 m² + 'litre': [('metre',3, Decimal(0.001))], # 1 L = 0.001 m³ + 'tonne': [('kilogram', 1, Decimal(1000))], # 1 t = 1000 kg + 'electronvolt': [('joule', 1, Decimal(1.602176634e-19))], # 1 eV = 1.602176634 × 10⁻¹⁹ J + 'dalton': [('kilogram', 1, Decimal(1.66053906660e-27))], # 1 Da = 1.66053906660 × 10⁻²⁷ kg + 'astronomicalunit': [('metre', 1, Decimal(149597870700))], # 1 AU = 149597870700 m + 'neper': [('one', 1, Decimal(1))], # Neper is a logarithmic unit for ratios of measurements, not directly convertible + 'bel': [('one', 1, Decimal(1))], # Bel is a logarithmic unit for ratios of power, not directly convertible + 'decibel': [('one', 1, Decimal(1))], # Decibel is a logarithmic unit for ratios of power, not directly convertible + 'byte':[('bit', 1, Decimal(8))], # TODO overthink this + # Note: For logarithmic units like neper, bel, and decibel, conversion to base units is not straightforward due to their nature. } + _additionalConversions = { # Conversions for ampere, volt, and mole into kg, s, m equivalents - 'volt': [('metre', 2, 1), ('kilogram', 1, 1), ('second', -3, 1), ('ampere', -1, 1)], # V = kg·m²/s³·A⁻¹ - 'percent':[('one',1,0.01)], - 'ppm':[('one',1,1e-6)], - 'byte':[('one',1,8)], - 'bit':[('one',1,1)], + 'volt': [('metre', 2, Decimal(1)), ('kilogram', 1, Decimal(1)), ('second', -3, Decimal(1)), ('ampere', -1, Decimal(1))], # V = kg·m²/s³·A⁻¹ + 'percent': [('one', 1, Decimal(0.01))], + 'ppm': [('one', 1, Decimal(1e-6))], + 'byte': [('one', 1, Decimal(8))], + 'bit': [('one', 1, Decimal(1))], # Note: These are placeholders and need to be adjusted to reflect accurate conversions. } _dsiKeyWords = { diff --git a/tests/test_dsiUnits.py b/tests/test_dsiUnits.py index b3ef3ca4cdf70c7dfe375d8a24778c08c06cd216..0ea52d58800131dc8e1f4e120e9ee8f272ab859d 100644 --- a/tests/test_dsiUnits.py +++ b/tests/test_dsiUnits.py @@ -18,6 +18,8 @@ import pytest from dsiUnits import _node, _getClosestStr,dsiUnit,dsiParser import sys import math +from fractions import Fraction +from decimal import Decimal from itertools import combinations # Access the machine epsilon for the float data type epsilon = sys.float_info.epsilon @@ -157,7 +159,7 @@ def test_baseUnitConversion(): # Test conversion of a single derived unit to its base unit derivedUnitTree = dsiUnit(r'\kilo\metre') baseUnitTree = derivedUnitTree.toBaseUnitTree() - assert baseUnitTree.tree == [[_node('', 'metre', '', scaleFactor=1000.0)]] + assert baseUnitTree.tree == [[_node('', 'metre', '', scaleFactor=Decimal(1000))]] assert baseUnitTree.toLatex() == '$$\\mathrm{m}$$' # Test conversion of a complex unit with a fraction to base units @@ -177,7 +179,7 @@ def test_baseUnitConversion(): kmh = dsiUnit(r'\kilo\metre\per\hour') ms = dsiUnit(r'\metre\per\second') scalFactor, commonBaseUnit = kmh.isScalablyEqualTo(ms) - assert scalFactor-3.6<epsilon + assert scalFactor-Decimal(3.6)<epsilon assert commonBaseUnit.toLatex() == '$$\\mathrm{m}\\,\\mathrm{s}^{-1}$$' ohmUnitTree = dsiUnit(r'\ohm') otherOhmesList = [ @@ -247,7 +249,7 @@ def test_complete(): VA = dsiUnit(r"\volt\ampere") Watt = dsiUnit(r"\watt") scaleFactor, commonBaseUnit = VA.isScalablyEqualTo(Watt, complete=True) - assert abs(scaleFactor - 1.0) < epsilon, "Scale factor for VA to Watt should be 1.0" + assert abs(scaleFactor - Decimal(1)) < epsilon, "Scale factor for VA to Watt should be 1.0" assert commonBaseUnit.toLatex() == '$$\\mathrm{kg}\\,\\mathrm{m}^{2}\\,\\mathrm{s}^{-3}$$', "Base unit representation for power should be in kg, m^2, s^-3" # Expanded Test Cases for Other Units of Power Equal to Watt @@ -523,3 +525,10 @@ def test_scalability_relative_units(): assert percent.isScalablyEqualTo(ppm, complete=True)[1] == dsiUnit(r'\one') assert ppm.isScalablyEqualTo(percent, complete=True)[1] == dsiUnit(r'\one') +def test_sameBaseUnitButPrefixedMultiplication(): + m=dsiUnit(r'\metre') + km=dsiUnit(r'\kilo\metre') + mm = dsiUnit(r'\milli\metre') + assert m*km==km*m + assert km*mm==mm*km==m*m + print("Debug") \ No newline at end of file