How to Do Coordinate Math

Domains, TimeUnits, NumberType, Coordinate arithmetic

How to Do Coordinate Math

Domains, TimeUnits, NumberTypes, and type-safe Coordinate arithmetic.

from fractions import Fraction

import pandas as pd

from timetoalign import Coordinate, Domain, NumberType, TimeUnit

The Three Domains

Domain Description Examples
Physical Real-world time Seconds, samples
Logical Symbolic/musical Beats, quarters, ticks
Graphical Visual/spatial Pixels, centimetres
list(Domain)
["logical", "physical", "graphical"]
Domain.physical == Domain.ph == Domain("physical") == Domain("ph")
True

TimeUnits

unit_data = [
    {"unit": u.name, "domain": u.domain.name, "discrete": u.is_discrete}
    for u in TimeUnit
]
pd.DataFrame(unit_data).sort_values(["domain", "discrete", "unit"])
unit domain discrete
12 centimeters graphical False
14 inches graphical False
11 meters graphical False
13 millimeters graphical False
15 points graphical False
10 pixels graphical True
1 beats logical False
2 measures logical False
0 number logical False
3 quarters logical False
4 ticks logical True
5 milliseconds physical False
7 minutes physical False
6 seconds physical False
9 frames physical True
8 samples physical True
# Convenient aliases
aliases = {
    "TimeUnit.s": TimeUnit.seconds,
    "TimeUnit.ms": TimeUnit.milliseconds,
    "TimeUnit.q": TimeUnit.quarters,
    "TimeUnit.b": TimeUnit.beats,
    "TimeUnit.px": TimeUnit.pixels,
    "TimeUnit.pulses": TimeUnit.ticks,
    "TimeUnit.divs": TimeUnit.ticks,
}
pd.Series({k: v.name for k, v in aliases.items()}, name="resolves_to")
TimeUnit.s              seconds
TimeUnit.ms        milliseconds
TimeUnit.q             quarters
TimeUnit.b                beats
TimeUnit.px              pixels
TimeUnit.pulses           ticks
TimeUnit.divs             ticks
Name: resolves_to, dtype: str

NumberType

Type Python Type Use Case
int int Discrete units (samples, ticks)
float float Physical time (seconds)
fraction Fraction Exact rationals (beats, quarters)
{
    "from int": NumberType.from_number(42),
    "from float": NumberType.from_number(3.14),
    "from Fraction": NumberType.from_number(Fraction(3, 4)),
}
{'from int': <NumberType.int: <class 'int'>>,
 'from float': <NumberType.float: <class 'float'>>,
 'from Fraction': <NumberType.fraction: <class 'fractions.Fraction'>>}

Why Fractions Matter

float_sum = sum(0.1 for _ in range(10))
fraction_sum = sum(Fraction(1, 10) for _ in range(10))

{
    "10x float": float_sum,
    "10x float == 1": float_sum == 1,
    "10x fraction": fraction_sum,
    "10x fraction == 1": fraction_sum == 1,
}
{'10x float': 0.9999999999999999,
 '10x float == 1': False,
 '10x fraction': Fraction(1, 1),
 '10x fraction == 1': True}

Coordinates

Immutable, hashable, type-safe value+unit pairs.

c1 = Coordinate(120, TimeUnit.ticks)
c2 = Coordinate(1.5, TimeUnit.seconds)
c3 = Coordinate(Fraction(3, 4), TimeUnit.quarters)

c1, c2, c3
(Coordinate(120, ticks),
 Coordinate(1.5, seconds),
 Coordinate(Fraction(3, 4), quarters))
{
    "value": c3.value,
    "unit": c3.unit,
    "number_type": c3.number_type,
    "domain": c3.domain,
}
{'value': Fraction(3, 4),
 'unit': "quarters",
 'number_type': <NumberType.fraction: <class 'fractions.Fraction'>>,
 'domain': "logical"}

Arithmetic

x = Coordinate(10, TimeUnit.seconds)
y = Coordinate(5, TimeUnit.seconds)

{"x > y": x > y, "x == y": x == y, "x <= y": x <= y}
{'x > y': True, 'x == y': False, 'x <= y': False}
# Unit mismatch raises TypeError
try:
    Coordinate(480, TimeUnit.ticks) + Coordinate(1.0, TimeUnit.seconds)
except TypeError as e:
    print(f"TypeError: {e}")
TypeError: Cannot add coordinates with different units: ticks vs seconds

Type Conversions

c = Coordinate(Fraction(7, 4), TimeUnit.quarters)

{
    "original": c,
    "to_float()": c.to_float(),
    "to_int()": c.to_int(),
    "to_int('round')": c.to_int("round"),
    "to_fraction()": c.to_fraction(),
}
{'original': Coordinate(Fraction(7, 4), quarters),
 'to_float()': 1.75,
 'to_int()': 1,
 "to_int('round')": 2,
 'to_fraction()': Fraction(7, 4)}
original = Coordinate(100, TimeUnit.ticks)

{
    "original": original,
    "with_value(200)": original.with_value(200),
    "with_unit(samples)": original.with_unit(TimeUnit.samples),
}
{'original': Coordinate(100, ticks),
 'with_value(200)': Coordinate(200, ticks),
 'with_unit(samples)': Coordinate(100, samples)}