Skip to main content

A Python library for representing and working with physical quantities and units

Project description

Physities

banner.png

CI codecov PyPI Python License Docs

A high-performance Python library for representing and working with physical quantities and units. Features dimensional analysis, unit conversion, and mathematical operations on physical measurements, powered by a Rust core for optimal performance.

Features

  • Type-safe physical quantity operations
  • Automatic dimensional analysis
  • Unit conversion with compile-time dimension checking
  • Elegant operator syntax (Meter / Second creates velocity units)
  • High-performance Rust backend using ndarray for linear algebra
  • UnitArray for batch operations on arrays of values
  • NumPy interoperability
  • Compact serialization (int64 encoding for dimensions)
  • Database-ready serialization (to_tuple(), to_dict())

Installation

pip install physities

Or with Poetry:

poetry add physities

Building from Source

Requires Rust toolchain and maturin:

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install maturin
pip install maturin

# Build and install
maturin develop --release

Quick Start

from physities.src.unit import Meter, Second, Kilometer, Hour

# Create composite unit types using operator syntax
MetersPerSecond = Meter / Second
KilometersPerHour = Kilometer / Hour

# Create values
v1 = MetersPerSecond(40)      # 40 m/s
v2 = KilometersPerHour(144)   # 144 km/h

# Convert between units
v3 = v2.convert(MetersPerSecond)  # 40 m/s

# Comparison works across compatible units
assert v1 == v2  # True: 40 m/s == 144 km/h

Available Units

Base SI Units

Dimension Base Unit
Length Meter
Mass Kilogram
Time Second
Temperature Kelvin
Amount Unity
Electric Current Ampere
Luminous Intensity Candela

Derived Units

Length

Gigameter, Megameter, Kilometer, Hectometer, Decameter, Decimeter, Centimeter, Millimeter, Micrometer, Nanometer, Foot, Yard, Inch, Mile, Furlong, Rod

Time

Nanosecond, Microsecond, Millisecond, Centisecond, Decisecond, Minute, Hour, Day, Week, Month, Year, Decade, Century, Millennium

Mass

Gigagram, Megagram, Tonne, Hectogram, Decagram, Gram, Decigram, Centigram, Milligram, Microgram, Nanogram, Pound, Ounce, Stone, Carat, Grain, Slug

Electric Current

Gigaampere, Megaampere, Kiloampere, Milliampere, Microampere, Nanoampere

Amount

Dozen, Moles, Pairs, Score

Area

Meter2, Kilometer2, Hectare, Centimeter2, Millimeter2, Foot2, Yard2, Inch2, Mile2, Acre

Volume

Meter3, Liter, Kiloliter, Milliliter, Centimeter3, Foot3, Gallon, Pint, Barrel

Examples

Creating and Using Units

from physities.src.unit import Meter, Second, Kilogram

# Create a velocity unit
Velocity = Meter / Second
v = Velocity(10)  # 10 m/s

# Create an acceleration unit
Acceleration = Meter / (Second ** 2)
a = Acceleration(9.8)  # 9.8 m/s²

# Create a force unit (Newton)
Newton = Kilogram * Meter / (Second ** 2)
force = Newton(100)  # 100 N

Unit Conversion

from physities.src.unit import Kilometer, Mile, Hour

# Create speed units
Kmh = Kilometer / Hour
Mph = Mile / Hour

# Convert between units
speed_kmh = Kmh(100)
speed_mph = speed_kmh.convert(Mph)

Mathematical Operations

from physities.src.unit import Meter, Second

Ms = Meter / Second

v1 = Ms(10)
v2 = Ms(20)

# Addition and subtraction (same units only)
v3 = v1 + v2  # 30 m/s
v4 = v2 - v1  # 10 m/s

# Multiplication and division with scalars
v5 = v1 * 2   # 20 m/s
v6 = v2 / 2   # 10 m/s

# Powers
v7 = v1 ** 2  # 100 m²/s²

Creating Custom Units

from physities.src.unit import Unit
from physities.src.scale import Scale
from physities.src.dimension import Dimension

# Define a custom unit
class Furlong(Unit):
    scale = Scale(
        dimension=Dimension.new_length(),
        from_base_scale_conversions=(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
        rescale_value=201.168,  # 1 furlong = 201.168 meters
    )
    value = None

# Or derive from existing units
from physities.src.unit import Meter
MyUnit = 201.168 * Meter  # Equivalent to Furlong

Batch Operations with UnitArray

For high-performance operations on many values, use UnitArray:

from physities.src.unit import Meter, Kilometer, UnitArray
import numpy as np

# Create array of 10,000 measurements
distances = UnitArray(Meter, np.random.rand(10000) * 1000)

# Batch operations (vectorized, ~100x faster than loops)
total = distances.sum()           # Returns a single Meter
average = distances.mean()        # Returns a single Meter
doubled = distances * 2           # Returns UnitArray

# Convert all values at once
km_distances = distances.convert(Kilometer)

Database Serialization

Units can be serialized for database storage:

from physities.src.unit import Meter, Second
from physities.src.unit.unit import Unit

velocity = (Meter / Second)(25)

# Compact format: (si_value, dimension_int64)
data = velocity.to_tuple()  # (25.0, 61441)

# Store in database: INSERT INTO measurements (value, dimension) VALUES (25.0, 61441)

# Restore from database
restored = Unit.from_tuple(data)

# Full format (preserves original unit)
full_data = velocity.to_dict()
restored = Unit.from_dict(full_data)

Using the Rust Backend Directly

For high-performance operations, you can use the Rust PhysicalScale directly:

from physities._physities_core import PhysicalScale

# Create a velocity scale (length=1, time=-1)
scale = PhysicalScale.from_components(
    (1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0),  # dimension exponents
    (1000.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),  # conversion factors (km)
    1.0  # rescale value
)

# Operations
squared = scale.power(2)
product = scale.multiply(scale)

# Serialization
json_str = scale.to_json()
int64_encoded = scale.to_dimension_int64()

# NumPy interop
import numpy as np
arr = scale.as_numpy()

Architecture

Physities uses a hybrid Python/Rust architecture for optimal performance and usability:

graph TB
    subgraph Python["Python API Layer"]
        Unit["Unit + MetaUnit"]
        Scale["Scale"]
        Dimension["Dimension"]
        BaseDim["BaseDimension"]

        Unit --> Scale --> Dimension --> BaseDim
    end

    subgraph Rust["Rust Core (Performance)"]
        PhysicalScale["PhysicalScale"]
        NDArray["ndarray (15 × f64)"]

        PhysicalScale --> NDArray
    end

    Python -.->|"optional"| Rust

    style Python fill:#3572A5,color:#fff
    style Rust fill:#dea584,color:#000

Python Layer (API)

  1. BaseDimension: Enum of 7 SI base dimensions
  2. Dimension: Frozen dataclass combining base dimensions into composite physical dimensions
  3. Scale: Frozen dataclass with dimension + conversion factors (uses Kobject for validation)
  4. Unit + MetaUnit: MetaUnit metaclass enables operator overloading at the class level

Rust Core (Performance)

The PhysicalScale struct provides high-performance operations:

  • Unified data structure: 15-element ndarray (7 dimension exponents + 7 conversion factors + 1 rescale)
  • Linear algebra operations: All physical operations implemented as vector math
  • SIMD-friendly: Contiguous memory layout enables vectorized operations
  • Serialization: Int64 bitwise encoding for compact dimension storage

See docs/ARCHITECTURE.md for detailed documentation.

API Reference

Unit Class

The main class for working with physical quantities.

class Unit:
    scale: Scale      # The scale/unit definition
    value: float      # The numeric value

    def convert(self, target: Type[Unit]) -> Unit:
        """Convert to another unit of the same dimension."""

    def to_si(self) -> Unit:
        """Convert to SI base units."""

    def to_tuple(self) -> tuple[float, int]:
        """Serialize to (si_value, dimension_int64) for database storage."""

    def to_dict(self) -> dict:
        """Serialize to dictionary with full scale information."""

    @classmethod
    def from_tuple(cls, data: tuple) -> Unit:
        """Restore from compact tuple format."""

    @classmethod
    def from_dict(cls, data: dict) -> Unit:
        """Restore from dictionary format."""

UnitArray Class

High-performance batch operations on arrays of values.

class UnitArray:
    unit_type: MetaUnit  # The unit class (e.g., Meter)
    values: np.ndarray   # NumPy array of values
    scale: Scale         # The scale definition

    def __init__(self, unit_type: MetaUnit, values: ArrayLike): ...

    # Arithmetic (vectorized)
    def __add__(self, other: Unit | UnitArray) -> UnitArray: ...
    def __mul__(self, scalar: float) -> UnitArray: ...

    # Reductions (return single Unit)
    def sum(self) -> Unit: ...
    def mean(self) -> Unit: ...
    def std(self) -> Unit: ...
    def min(self) -> Unit: ...
    def max(self) -> Unit: ...

    # Conversion
    def convert(self, target: MetaUnit) -> UnitArray: ...
    def to_numpy(self) -> np.ndarray: ...

Scale Class

Defines unit conversion factors.

@dataclass(frozen=True, slots=True)
class Scale:
    dimension: Dimension
    from_base_scale_conversions: tuple[float, ...]
    rescale_value: float

    @property
    def conversion_factor(self) -> float:
        """Total conversion factor to SI units."""

    @property
    def is_dimensionless(self) -> bool:
        """Check if scale has no dimensions."""

Dimension Class

Represents physical dimensions.

@dataclass(frozen=True, slots=True)
class Dimension:
    dimensions_tuple: tuple[float, ...]

    @classmethod
    def new_length(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_mass(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_time(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_temperature(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_amount(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_electric_current(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_luminous_intensity(cls, power: float = 1) -> Dimension: ...
    @classmethod
    def new_dimensionless(cls) -> Dimension: ...

PhysicalScale (Rust)

High-performance scale operations.

class PhysicalScale:
    # Properties
    length: float
    mass: float
    temperature: float
    time: float
    amount: float
    electric_current: float
    luminous_intensity: float
    rescale_value: float
    conversion_factor: float
    is_dimensionless: bool

    # Operations
    def multiply(self, other: PhysicalScale) -> PhysicalScale: ...
    def divide(self, other: PhysicalScale) -> PhysicalScale: ...
    def power(self, exp: float) -> PhysicalScale: ...
    def multiply_scalar(self, scalar: float) -> PhysicalScale: ...

    # Serialization
    def to_json(self) -> str: ...
    def to_dimension_int64(self) -> int: ...

    @staticmethod
    def from_json(json_str: str) -> PhysicalScale: ...
    @staticmethod
    def from_dimension_int64(encoded: int) -> PhysicalScale: ...

BaseDimension Enum

The 7 SI base dimensions:

class BaseDimension(IntEnum):
    LENGTH = 0
    MASS = 1
    TEMPERATURE = 2
    TIME = 3
    AMOUNT = 4
    ELECTRIC_CURRENT = 5
    LUMINOUS_INTENSITY = 6

Development

Prerequisites

  • Python 3.11+
  • Rust toolchain (for building from source)
  • Poetry (optional, for dependency management)

Setup

# Clone repository
git clone https://github.com/CenturyBoys/physities.git
cd physities

# Install dependencies
pip install -e ".[dev]"

# Or with Poetry
poetry install

# Build Rust extension
maturin develop

Running Tests

# Run all tests
pytest tests/ -v

# Run unit tests only
pytest tests/ -v -m unit

# Run with coverage
pytest tests/ --cov=physities

Linting

# Python linting
ruff check .

# Rust linting
cargo clippy

Building

# Development build
maturin develop

# Release build
maturin build --release

# Build wheels for distribution
maturin build --release -o dist

Performance

Physities adds ~20x overhead vs raw Python floats for type safety. For batch operations, use UnitArray to get near-NumPy performance.

Operation Plain Python Physities Overhead
a + b (floats) ~210 ns ~25 µs ~120x
a * b (floats) ~210 ns ~48 µs ~230x
Create value ~600 ns ~780 ns ~1.3x
Create type ~15 µs ~39 µs ~2.6x

Batch operations (UnitArray):

Operation Loop (100 units) UnitArray (100) Speedup
Add scalar 2.3 ms 22 µs 100x

See benchmarks for detailed performance data.

License

MIT

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

physities-0.2.0-cp312-cp312-win_amd64.whl (245.0 kB view details)

Uploaded CPython 3.12Windows x86-64

physities-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl (409.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.34+ x86-64

physities-0.2.0-cp312-cp312-macosx_11_0_arm64.whl (361.0 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

physities-0.2.0-cp311-cp311-win_amd64.whl (247.2 kB view details)

Uploaded CPython 3.11Windows x86-64

physities-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl (413.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.34+ x86-64

physities-0.2.0-cp311-cp311-macosx_11_0_arm64.whl (363.9 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

Details for the file physities-0.2.0-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: physities-0.2.0-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 245.0 kB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for physities-0.2.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 c44fabc90c54a1803ed86c5360805f046f83305afc514bf3a198bf5401deadd6
MD5 49073e8768ce8ac70f0d2449548bd40e
BLAKE2b-256 a03d826684c9171cdd2dcb61d586c28192c0c0e84254465705b19d1d32ad1c03

See more details on using hashes here.

File details

Details for the file physities-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for physities-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 0ce0a28101ce96424335a8a83ce7a50a095cb7d237a3c5586542f9c8f16898f1
MD5 0afa9ee6609ed588b502ecaed72a0de8
BLAKE2b-256 3d10cac160037ef6478e3f4c10ac44177b56b35f79764fb08d2f6ef6bc4c072e

See more details on using hashes here.

File details

Details for the file physities-0.2.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for physities-0.2.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 90485db4930efdb1014b5ed6045145b02136f032cfddb2d5f60e987aaf769b03
MD5 375b1344bc6e02b8725c292900e7d026
BLAKE2b-256 057b50b5b4c08cd0ba6910c894400c99ab763c54da9ca9ea74b4fcb03f7151e5

See more details on using hashes here.

File details

Details for the file physities-0.2.0-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: physities-0.2.0-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 247.2 kB
  • Tags: CPython 3.11, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for physities-0.2.0-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 6cfb21df35462d877cf85cf72593f4420e8e633d2fb1fa48db5df7e664a1d336
MD5 cbeba6f6f9c42584d10ecd7a8ecfe693
BLAKE2b-256 c87a1764bac71ce7f50abc8c8c331de67bb3baa150011c7fc80346ae35096015

See more details on using hashes here.

File details

Details for the file physities-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for physities-0.2.0-cp311-cp311-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 eb86a2e89cd84f3efdf7832f7f771603d58f960f1dc988ec33a3ca6942e5068c
MD5 7bf941a42c6d397cd5404e3b9a1dd178
BLAKE2b-256 27ccf6e67520fa4a5cdadea7d75aa210edbd0d3451375393a97f40e9bdcb967a

See more details on using hashes here.

File details

Details for the file physities-0.2.0-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for physities-0.2.0-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d4ff0bad5a395e9b3c4bf47f69fbfa5fea67632b001a437c7f2586fb3feb52d9
MD5 6638c95cd44394ac9bd867edc8e88155
BLAKE2b-256 3f26121c2452969ea1daae114d3066ad042200190f6978668d136060119ed8f3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page