Source code for resokit.units.units

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of the
#   ResoKit Project (https://github.com/Gianuzzi/resokit).
# Copyright (c) 2025, Emmanuel Gianuzzi
# License: MIT
#   Full Text: https://github.com/Gianuzzi/resokit/blob/master/LICENSE

# =============================================================================
# DOCS
# =============================================================================

"""Module with utilities for unit conversion and manipulation."""

# =============================================================================
# IMPORTS
# =============================================================================

from types import MappingProxyType
from typing import List, Tuple, Union

from numpy import pi

# =============================================================================
# CONSTANTS AND NORMALIZED UNIT DICTIONARIES
# =============================================================================

# Gravitational constant in SI units
G = 6.67430e-11  # m^3 kg^-1 s^-2

# UNITS dictionary
_units = {
    "mass": {
        "g": 1e-3,  # alias
        "gr": 1e-3,  # grams → kg
        "kg": 1.0,
        "ton": 1e3,  # metric ton → kg
        "me": 5.9722e24,  # Earth mass → kg
        "mj": 1.89813e27,  # Jupiter mass → kg
        "ms": 1.989e30,  # Solar mass → kg
    },
    "distance": {
        "cm": 1e-2,  # cm → m
        "m": 1.0,
        "km": 1e3,  # km → m
        "re": 6.371e6,  # Earth eq radius → m
        "rj": 6.9911e7,  # Jupiter radius → m
        "rs": 6.957e8,  # Solar radius → m
        "au": 1.495978e11,  # Astronomical unit → m
        "pc": 3.0857e16,  # Parsec → m
    },
    "time": {
        "sec": 1.0,
        "s": 1.0,  # alias
        "min": 60.0,  # min → s
        "hour": 3600.0,  # hour → s
        "day": 86400.0,  # day → s
        "year": 31557600.0,  # year → s
        "yr": 31557600,  # alias 2
    },
    "angle": {
        "rad": 1.0,
        "deg": pi / 180.0,
    },
}
# Add density units
_units["density"] = {
    "rhow": _units["mass"]["g"] / _units["distance"]["cm"] ** 3,
    "rhos": _units["mass"]["ms"]
    / (4.0 / 3.0 * pi * _units["distance"]["rs"] ** 3),
    "rhoj": _units["mass"]["mj"]
    / (4.0 / 3.0 * pi * _units["distance"]["rj"] ** 3),
    "rhoe": _units["mass"]["me"]
    / (4.0 / 3.0 * pi * _units["distance"]["re"] ** 3),
}

# Create immutable dict
UNITS = MappingProxyType(_units)

# Custom with MKS
_mks = {u: v for uv in UNITS.values() for u, v in uv.items()}
_mks["G"] = G

# Create dict to set MKS
MKS = MappingProxyType(_mks)


def _convert_units(from_unit, to_unit, power=1):
    from_unit = from_unit.lower()
    to_unit = to_unit.lower()

    for unit_dict in UNITS.values():
        if from_unit in unit_dict and to_unit in unit_dict:
            origin = unit_dict[from_unit]
            dest = unit_dict[to_unit]
            return (origin / dest) ** power

    raise ValueError(
        f"Cannot convert '{from_unit}' to '{to_unit}': incompatible or unknown."
    )


def __normalize_units(u):
    if isinstance(u, str):
        return [u]
    return u


def __normalize_powers(p, n):
    if isinstance(p, int):
        return [p] * n
    if len(p) != n:
        raise ValueError(
            f"Power length {len(p)} does not match units length {n}."
        )
    return p


[docs] def convert( *values, from_units: Union[None, str, Tuple[str, ...], List[str]] = None, to_units: Union[None, str, Tuple[str, ...], List[str]] = None, power: Union[int, Tuple[int, ...], List[int]] = 1, ) -> Union[float, List[float]]: """ Convert between compound units (e.g. km^2/s → m^2/s). Parameters ---------- from_units : str or list of str Units to convert from. to_units : str or list of str Units to convert to. power : int or list of int, optional Power(s) for each unit (default = 1). Must match length if list. Examples -------- To convert 5 km s^{-2} to au yr^{-2}: >>> resokit.units.convert( ... 5, ... from_units=("km", "s"), ... to_units=("au", "yr"), ... power=(1, -2) ... ) 33247713.903743323 Returns ------- float or list of float Converted value, or list of converted values. """ if from_units is None: raise ValueError("Argument 'from_units' must be set.") if to_units is None: raise ValueError("Argument 'to_units' must be set.") from_units = __normalize_units(from_units) to_units = __normalize_units(to_units) powers = __normalize_powers(power, len(from_units)) if len(from_units) != len(to_units): raise ValueError("Mismatch between number of from_units and to_units.") factor = 1.0 for fu, tu, pw in zip(from_units, to_units, powers): factor *= _convert_units(fu, tu, power=pw) if not values: return factor if len(values) == 1: return factor * values[0] return [value * factor for value in values]