Source code for opstool.pre._unit_system

import difflib
import re
from typing import Literal, TypeAlias, get_args

_TOKEN = re.compile(
    r"""
    (?P<op>[*/])?                 # operator
    (?P<unit>[A-Za-z]+)           # unit symbol
    (?:\^?(?P<pow>[-+]?\d+))?     # ^exponent
    """,
    re.VERBOSE,
)

_ratio_length = {
    "inch2m": 0.0254,  # exact
    "inch2dm": 0.254,  # exact
    "inch2cm": 2.54,  # exact
    "inch2mm": 25.4,  # exact
    "inch2km": 2.54e-5,  # exact
    "inch2ft": 1.0 / 12.0,  # exact (0.0833333333333…)
    "ft2mm": 304.8,  # exact
    "ft2cm": 30.48,  # exact
    "ft2dm": 3.048,  # exact
    "ft2m": 0.3048,  # exact
    "ft2km": 3.048e-4,  # exact
    "mm2cm": 0.1,  # exact
    "mm2dm": 0.01,  # exact
    "mm2m": 0.001,  # exact
    "mm2km": 1e-6,  # exact
    "cm2dm": 0.1,  # exact
    "cm2m": 0.01,  # exact
    "cm2km": 1e-5,  # exact
    "m2km": 1e-3,  # exact
}

_ratio_force = {
    "lb2lbf": 1.0,  # by convention
    "lb2kip": 0.001,  # exact
    "lb2n": 4.4482216152605,  # exact (1 lbf = 4.4482216152605 N)
    "lb2kn": 4.4482216152605e-3,
    "lb2mn": 4.4482216152605e-6,
    "lb2kgf": 0.45359237,  # exact (1 lb = 0.45359237 kg)
    "lb2tonf": 0.00045359237,  # exact
    "lbf2kip": 0.001,
    "lbf2n": 4.4482216152605,
    "lbf2kn": 4.4482216152605e-3,
    "lbf2mn": 4.4482216152605e-6,
    "lbf2kgf": 0.45359237,
    "lbf2tonf": 0.00045359237,
    "kip2n": 4448.2216152605,  # = 1000 x lbf2n
    "kip2kn": 4.4482216152605,  # = kip2n / 1000
    "kip2mn": 0.0044482216152605,  # = kip2n / 1e6
    "kip2kgf": 453.59237,  # = 1000 x lb2kgf
    "kip2tonf": 0.45359237,  # = 1000 x lb2tonf
    "n2kn": 1e-3,  # exact
    "n2mn": 1e-6,
    "n2kgf": 0.101971621297793,  # = 1 / 9.80665
    "n2tonf": 1.01971621297793e-4,  # = 1 / 9.80665 / 1000
    "kn2mn": 1e-3,
    "kn2kgf": 101.971621297793,
    "kn2tonf": 0.101971621297793,
    "mn2kgf": 101971.621297793,
    "mn2tonf": 101.971621297793,
    "kgf2tonf": 0.001,  # exact
}
_ratio_time = {
    "sec2msec": 1000,
    "sec2min": 1 / 60,
    "sec2hour": 1 / 3600,
    "sec2day": 1 / 24 / 3600,
    "sec2year": 1 / 365 / 24 / 3600,
    "min2msec": 1000 * 60,
    "min2hour": 1 / 60,
    "min2day": 1 / 24 / 60,
    "min2year": 1 / 365 / 24 / 60,
    "hour2msec": 60 * 60 * 1000,
    "hour2day": 1 / 24,
    "hour2year": 1 / 365 / 24,
    "day2msec": 24 * 60 * 60 * 1000,
    "day2hour": 24,
    "day2year": 1 / 365,
    "year2msec": 365 * 24 * 60 * 60 * 1000,
}


def ratio_update(ratio_dict):
    temp_dict = {}
    for key, value in ratio_dict.items():
        idx = key.index("2")
        new_key = key[idx + 1 :] + "2" + key[:idx]
        temp_dict[new_key] = 1 / value
        new_key = key[:idx] + "2" + key[:idx]
        temp_dict[new_key] = 1
    ratio_dict.update(temp_dict)


ratio_update(_ratio_length)
ratio_update(_ratio_force)
ratio_update(_ratio_time)

_unit_length: TypeAlias = Literal["inch", "ft", "mm", "cm", "m", "km"]
_unit_force: TypeAlias = Literal["lb", "lbf", "kip", "n", "kn", "mn", "kgf", "tonf"]
_unit_time: TypeAlias = Literal["msec", "sec", "min", "hour", "day", "year"]
_unit_mass: TypeAlias = Literal["mg", "g", "kg", "ton", "t", "slug"]
_unit_stress: TypeAlias = Literal["pa", "kpa", "mpa", "gpa", "bar", "psi", "ksi", "psf", "ksf"]


[docs] class UnitSystem: """A class for unit conversion. All unit factors are transformed into the base unit system as specified by the following ``length``, ``force``, and ``time`` parameters. Parameters ----------- length: str, default="m" Length unit base. Optional ["inch", "ft", "mm", "cm", "m", "km"]. force: str, default="kN" Force unit base. Optional ["lb"("lbf"), "kip", "n", "kn", "mn", "kgf", "tonf"]. time: str, default="sec" Time unit base. Optional ["sec"]. .. note:: * `Mass` and `stress` units can be automatically determined based on `length` and `force` units, optional mass units include ["mg", "g", "kg", "ton"("t"), "slug"], and optional stress units include ["pa", "kpa", "mpa", "gpa", "bar", "psi", "ksi", "psf", "ksf"]. * You can enter any uppercase and lowercase forms, such as ``kn`` and ``kN``, ``mpa`` and ``MPa`` are equivalent. * You can add a number (int) after the unit to indicate a power, such as ``.m3`` for ``m*m*m``. * You can use key indexing, such as: unit["m^3"], unit["kN/m^2"], unit["N*sec^2/m"], unit["MPa"] Examples --------- >>> UNIT = UnitSystem(length="m", force="kN", time="min") >>> # Call the __repr__ method, print the UnitSystem object information >>> print(UNIT) >>> # Call the print method, print all common units >>> UNIT.print() >>> # use key indexing >>> print("N/mm2", UNIT["N/mm2"]) >>> print("N*mm/m^2", UNIT["N*mm/m^2"]) >>> print("MPa", UNIT["MPa"]) >>> # Show some unit conversion effects >>> print("Length:", UNIT.mm, UNIT.Mm2, UNIT.cm, UNIT.m, UNIT.M2, UNIT.inch, UNIT.Ft) >>> print("Force", UNIT.n, UNIT.kN, UNIT.kN2, UNIT.lbf, UNIT.kip) >>> print("Stress", UNIT.mpa, UNIT.kpa, UNIT.pa, UNIT.psi, UNIT.ksi) >>> print("Mass", UNIT.g, UNIT.kg, UNIT.ton, UNIT.slug) >>> print("Time", UNIT.msec, UNIT.min, UNIT.hour, UNIT.day, UNIT.year) """ def __init__(self, length: _unit_length = "m", force: _unit_force = "kn", time: _unit_time = "sec") -> None: # cache self._cache = {} self._length = length.lower() self._force = force.lower() self._time = time.lower() for unit in get_args(_unit_length): val = _ratio_length[unit.lower() + "2" + self._length] setattr(self, unit, val) self._cache[unit] = val for unit in get_args(_unit_force): val = _ratio_force[unit.lower() + "2" + self._force] setattr(self, unit, val) self._cache[unit] = val for unit in get_args(_unit_time): val = _ratio_time[unit.lower() + "2" + self._time] setattr(self, unit, val) self._cache[unit] = val # alias self.s = self.sec self.ms = self.msec self.kips = self.kip # mass self.kg = self.n * (self.sec**2) / self.m self.mg, self.g, self.ton = 1e-6 * self.kg, 1e-3 * self.kg, 1e3 * self.kg self.t, self.slug, self.slinch = ( 1e3 * self.kg, 14.593902937 * self.kg, 175.126836 * self.kg, ) # stress self.pa = self.N / (self.m * self.m) # SI multiples self.kpa = 1e3 * self.pa self.mpa = 1e6 * self.pa self.gpa = 1e9 * self.pa self.bar = 1e5 * self.pa # Imperial & US customary self.psi = 6894.757293168 * self.pa # 1 psi = 6894.757293168 Pa self.ksi = 6894757.293168 * self.pa # 1 ksi = 1000 psi self.psf = 47.88025898033584 * self.pa # 1 psf = 47.88025898033584 Pa self.ksf = 47880.25898033584 * self.pa # 1 ksf = 1000 psf # gravity acceleration self.g0 = 9.80665 * self.m / (self.sec**2) self.grav = self.g0 @property def length(self): return self._length @property def force(self): return self._force @property def time(self): return self._time def __getitem__(self, expr: str) -> float: expr = expr.strip() if expr in self._cache: return self._cache[expr] val = self._parse_expr(expr) self._cache[expr] = val return val def _parse_expr(self, expr: str) -> float: """Parses expressions such as: "m^3", "kN/m^2", "N*sec^2/m", "MPa". Rules: Connect terms with * or /. Exponents can be written with ^ (e.g., ^3) or by appending the number directly (e.g., "m3"). """ expr = expr.replace(" ", "") pos = 0 total = 1.0 last_op = "*" while pos < len(expr): m = _TOKEN.match(expr, pos) if not m: raise ValueError(f"Bad unit token near: '{expr[pos:]}' in '{expr}'") # noqa: TRY003 op = m.group("op") or last_op unit = m.group("unit") pow_str = m.group("pow") pos = m.end() # Support trailing numeric exponents without '^', e.g., m3 if pow_str is None: # Attempt to match the immediately following numeric sequence tail = re.match(r"(\d+)", expr[pos:]) if tail: pow_str = tail.group(1) pos += len(pow_str) attr_map = {k.lower(): v for k, v in self.__dict__.items() if isinstance(v, (int, float))} factor = attr_map.get(unit.lower(), None) if factor is None: raise ValueError(f"Unknown unit: '{unit}' in '{expr}'") # noqa: TRY003 power = int(pow_str) if pow_str else 1 factor = factor**power if op == "*": total *= factor elif op == "/": total /= factor else: raise ValueError(f"Unknown operator '{op}' in '{expr}'") # noqa: TRY003 last_op = "*" return total # def __getattr__(self, item): # v = re.findall(r"-?\d+\.?\d*e?E?-?\d*?", item) # if v: # v = float(v[0]) # else: # return getattr(self, item.lower()) # s = "".join([x for x in item if x.isalpha()]) # base = getattr(self, s) # return base**v def _get_cache(self): """安全地获取缓存字典,避免递归调用""" return object.__getattribute__(self, "_cache") def __getattr__(self, expr: str): cache = self._get_cache() expr = expr.strip() if expr in cache: return cache[expr] # Uniformly convert to lowercase (preserve the numeric part) base_name = "".join([c for c in expr if not c.isdigit()]).lower() valid_units = ( get_args(_unit_length) + get_args(_unit_force) + get_args(_unit_time) + get_args(_unit_mass) + get_args(_unit_stress) ) if base_name in [u.lower() for u in valid_units]: val = self._get_unit_ratio(expr) self._cache[expr] = val return val raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{expr}'") # noqa: TRY003 def _get_unit_ratio(self, name: str) -> float: # Normalize the input name clean_name = re.sub(r"[^a-zA-Z0-9]", "", name).lower() # Separate the unit base name and exponent match = re.fullmatch(r"([a-z]+)(\d*)", clean_name) if not match: raise AttributeError(f"Invalid unit format: '{name}'") # noqa: TRY003 base_part, exponent_part = match.groups() # Build a dictionary for attribute lookup (lowercase keys) attr_map = {k.lower(): v for k, v in self.__dict__.items() if isinstance(v, (int, float))} if base_part.lower() in attr_map: exponent = int(exponent_part) if exponent_part else 1 base_value = attr_map[base_part.lower()] return base_value**exponent else: suggestions = difflib.get_close_matches(base_part, attr_map.keys(), n=3) err_msg = f"'{self.__class__.__name__}' has no attribute '{name}'" if suggestions: err_msg += f". Did you mean: {', '.join(suggestions)}?" raise AttributeError(err_msg) def __repr__(self) -> str: return f"<UnitSystem: length={self.length!r}, force={self.force!r}, time={self.time!r} ({hash(self)})>"
[docs] def print(self): """Show all unit conversion coefficients with colorful output""" from rich import print as rprint txt = "\n[bold #d20962]Length unit:[/bold #d20962]\n" for _i, unit in enumerate(get_args(_unit_length)): txt += f"{unit}={getattr(self, unit):.3g}; " txt += "\n\n[bold #f47721]Force unit:[/bold #f47721]\n" for _i, unit in enumerate(get_args(_unit_force)): txt += f"{unit}={getattr(self, unit):.3g}; " txt += "\n\n[bold #7ac143]Time unit:[/bold #7ac143]\n" for _i, unit in enumerate(get_args(_unit_time)): txt += f"{unit}={getattr(self, unit):.3g}; " txt += "\n\n[bold #00bce4]Mass unit:[/bold #00bce4]\n" for _i, unit in enumerate(get_args(_unit_mass)): txt += f"{unit}={getattr(self, unit):.3g}; " txt += "\n\n[bold #7d3f98]Pressure unit:[/bold #7d3f98]\n" for _i, unit in enumerate(get_args(_unit_stress)): txt += f"{unit}={getattr(self, unit):.3g}; " rprint(txt)
if __name__ == "__main__": UNIT = UnitSystem(length="m", force="kN", time="min") # Call the __repr__ method, print the UnitSystem object information print(UNIT) # Call the print method, print all common units UNIT.print() print("N/mm2", UNIT["N/mm2"]) print("N*mm/mm^2", UNIT["N*mm/mm^2"]) # Example of using __getitem__ to get a unit conversion value # Show some unit conversion effects print("Length:", UNIT.mm, UNIT.Mm2, UNIT.cm, UNIT.m, UNIT.M2, UNIT.inch, UNIT.Ft) print("Force", UNIT.n, UNIT.kN, UNIT.kN2, UNIT.lbf, UNIT.kip) print("Stress", UNIT.mpa, UNIT.kpa, UNIT.pa, UNIT.psi, UNIT.ksi) print("Mass", UNIT.g, UNIT.kg, UNIT.ton, UNIT.slug) print("Time", UNIT.msec, UNIT.min, UNIT.hour, UNIT.day, UNIT.year) UNIT = UnitSystem(length="inch", force="kip", time="sec") # Call the __repr__ method, print the UnitSystem object information print(UNIT) # Call the print method, print all common units UNIT.print() print("MPa", UNIT["MPa"]) # Example of using __getitem__ to get a unit conversion value print("kip*sec^2/inch", UNIT["kip*sec^2/inch"]) # Show some unit conversion effects print("Length:", UNIT.mm, UNIT.Mm2, UNIT.cm, UNIT.m, UNIT.M2, UNIT.inch, UNIT.Ft) print("Force", UNIT.n, UNIT.kN, UNIT.kN2, UNIT.lbf, UNIT.kip) print("Stress", UNIT.mpa, UNIT.kpa, UNIT.pa, UNIT.psi, UNIT.ksi) print("Mass", UNIT.g, UNIT.kg, UNIT.ton, UNIT.slug) # When inputting invalid unit, it will give smart suggestions print(UNIT.mmm)