Source code for biosim_extractor.units.unitconversion

#!/usr/bin/env python3
"""
Convert units from various MD engine outputs to a consistent standard unit system.

This module provides the UnitConverter class for converting scientific values
between different units, standardizing to a chosen system (default: SI-like).
"""

from typing import Any, Dict, List, Optional, Tuple, Union


[docs] class UnitConverter: """ Simple unit conversion class for scientific calculations. Converts values to a chosen standard unit system. """ def __init__(self, standard_units: Optional[Dict[str, str]] = None) -> None: """ Initialize the UnitConverter. Args: standard_units (dict, optional): Mapping of unit types to standard units. """ self.standards: Dict[str, str] = standard_units or { "length": "nm", # nanometers "volume": "nm³", # nanometers cubed "time": "s", # seconds "energy": "kJ/mol", # kilojoules per mole "temperature": "K", # Kelvin "pressure": "bar", # bar "compressibility": "1/bar", # inverse bar "force": "kJ/mol/nm", # kilojoules per mole per nanometer "velocity": "nm/ps", # nanometers per picosecond "frequency": "1/ps", # inverse picoseconds "friction": "a/ps", # atomic mass units per picosecond "angle": "degree", # degree "charge": "e", # atomic charge units "molecular_weight": "g/mol", # grams per mol "information_storage": "GB", # gigabytes } # Conversion factors to standard units self.factors: Dict[str, Dict[str, Any]] = { "length": { "Å": 0.1, "A": 0.1, "Ang": 0.1, "angstrom": 0.1, "nm": 1.0, "pm": 0.001, "m": 1e9, }, "volume": { "nm³": 1, "nm^3": 1, "A^3": 1000, "Å^3": 1000, "Ang^3": 1000, "ų": 1000, }, "time": { "fs": 1e-15, "ps": 1e-12, "ns": 1e-9, "μs": 1e-6, "ms": 0.001, "s": 1.0, }, "energy": { "kcal/mol": 4.184, "kJ/mol": 1.0, "eV": 96.485, "J": 6.022e20, "hartree": 2625.5, }, "force": { "kcal/mol/Å": 41.84, "kJ/mol/nm": 1.0, }, "temperature": { "K": 1.0, "C": lambda x: x + 273.15, "°C": lambda x: x + 273.15, "F": lambda x: (x - 32) * 5 / 9 + 273.15, "°F": lambda x: (x - 32) * 5 / 9 + 273.15, }, "pressure": { "bar": 1.0, "atm": 1.01325, "Pa": 1e-5, "kPa": 0.01, "MPa": 10.0, }, "compressibility": { "1/bar": 1.0, "bar^-1": 1.0, "1e10-6 bar^-1": 1e-6, "1e10-6 1/bar": 1e-6, }, "frequency": { "ps^-1": 1.0, "1/ps": 1.0, "fs^-1": 1000.0, "1/fs": 1000.0, "GHz": 1e-3, "THz": 1e-6, "Hz": 1e-12, }, "friction": { "a/ps": 1.0, "amu/ps": 1.0, }, "angle": { "degree": 1.0, }, "charge": { "e": 1.0, "acu": 1.0, }, "molecular_weight": { "g/mol": 1.0, "amu": 1.0, "Da": 1.0, }, "information_storage": { "GB": 1.0, "MB": 0.001, }, } # Create reverse lookup: unit -> unit_type self.unit_to_type: Dict[str, str] = {} for unit_type, units in self.factors.items(): for unit in units.keys(): self.unit_to_type[unit] = unit_type
[docs] def convert( self, value: Union[float, List[float]], from_unit: str, unit_type: Optional[str] = None, decimals: Optional[int] = None, ) -> Union[float, List[float]]: """ Convert a value from one unit to the standard unit. Args: value (float or list): Value(s) to convert. from_unit (str): The original unit. unit_type (str, optional): The unit type (auto-detected if None). Returns: float or list: Converted value(s) in standard unit. Raises: ValueError: If unit or unit type is unknown. """ # Auto-detect unit type if not provided if unit_type is None: unit_type = self.get_unit_type(from_unit) if unit_type is None: raise ValueError(f"Unknown unit: {from_unit}") target_unit = self.standards[unit_type] # Skip conversion if already in standard unit if from_unit == target_unit: result = value if unit_type not in self.factors: raise ValueError(f"Unknown unit type: {unit_type}") factor = self.factors[unit_type].get(from_unit) if factor is None: raise ValueError(f"Unknown {unit_type} unit: {from_unit}") # Handle temperature conversions (functions instead of factors) if callable(factor): if isinstance(value, list): result = [factor(x) for x in value] else: result = factor(value) else: if isinstance(value, list): result = [x * factor for x in value] else: result = value * factor # Apply rounding if requested if decimals is not None: if isinstance(result, list): result = [round(float(x), decimals) for x in result] else: result = round(float(result), decimals) return result
[docs] def convert_with_unit( self, value: Union[float, List[float]], from_unit: str, unit_type: Optional[str] = None, ) -> Tuple[Union[float, List[float]], str]: """ Convert a value and return both the value and the target unit. Args: value (float or list): Value(s) to convert. from_unit (str): The original unit. unit_type (str, optional): The unit type (auto-detected if None). Returns: tuple: (converted value(s), target unit) Raises: ValueError: If unit or unit type is unknown. """ if unit_type is None: unit_type = self.get_unit_type(from_unit) if unit_type is None: raise ValueError(f"Unknown unit: {from_unit}") target_unit = self.standards[unit_type] converted_value = self.convert(value, from_unit, unit_type) return converted_value, target_unit
[docs] def get_target_unit(self, from_unit: str) -> str: """ Get the standard unit for a given unit. Args: from_unit (str): The original unit. Returns: str: The standard unit. Raises: ValueError: If unit is unknown. """ unit_type = self.get_unit_type(from_unit) if unit_type is None: raise ValueError(f"Unknown unit: {from_unit}") return self.standards[unit_type]
def __call__( self, value: Union[float, List[float]], from_unit: str, unit_type: Optional[str] = None, ) -> Union[float, List[float]]: """ Convert a value using the instance as a callable. Args: value (float or list): Value(s) to convert. from_unit (str): The original unit. unit_type (str, optional): The unit type (auto-detected if None). Returns: float or list: Converted value(s) in standard unit. """ return self.convert(value, from_unit, unit_type)
[docs] def get_unit_type(self, unit: str) -> Optional[str]: """ Get the unit type for a given unit string. Args: unit (str): The unit string. Returns: str or None: The unit type, or None if unknown. """ return self.unit_to_type.get(unit)
[docs] def is_standard_unit(self, unit: str) -> bool: """ Check if a unit is the standard for its type. Args: unit (str): The unit string. Returns: bool: True if unit is standard, False otherwise. """ unit_type = self.get_unit_type(unit) if unit_type is None: return False return unit == self.standards[unit_type]
[docs] def needs_conversion(self, from_unit: str) -> bool: """ Determine if a unit needs conversion to standard. Args: from_unit (str): The unit string. Returns: bool: True if conversion is needed, False if already standard. """ return not self.is_standard_unit(from_unit)