Skip to content

Docstrings and expand tests #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions test/test_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def _all_dimensions() -> Iterator[type[core.ValueWithDimension]]:

@pytest.mark.parametrize('dimension', _ALL_DIMENSIONS)
def test_arithmetic_ops_preserve_type(dimension: type[core.ValueWithDimension]) -> None:
"""
addition, subtraction and multiplication by scalars preserve the dimensions
"""
u = dimension(dimension.valid_base_units()[0])
a = u * 2
b = u * 3
Expand All @@ -61,6 +64,10 @@ def test_arithmetic_ops_preserve_type(dimension: type[core.ValueWithDimension])
def test_arithmetic_ops_preserve_type_array(
dimension: type[core.ValueWithDimension],
) -> None:
"""
addition, subtraction and multiplication by scalars preserve the dimensions
even when it is an array of multiple such elements
"""
u = dimension(dimension.valid_base_units()[0])
a = u * [2, 3]
b = u * [5, 7]
Expand All @@ -79,3 +86,35 @@ def test_arithmetic_ops_preserve_type_array(

c = a / 11
assert all(c == u * [2 / 11, 3 / 11])


@pytest.mark.parametrize('dimension', _ALL_DIMENSIONS)
def test_arithmetic_ops_preserve_type_multi_array(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from TypedUnits perspective there is no difference between a 1-D array and a multidimensional, both are a numpy.ndarray so this case is implicitly covered by the tests.

We can consider explicitly testing for this though but for that please improve the test.

dimension: type[core.ValueWithDimension],
) -> None:
"""
addition, subtraction and multiplication by scalars preserve the dimensions
even when it is a multidimensional array of multiple such elements
"""
u = dimension(dimension.valid_base_units()[0])
a = u * [[2, 3], [4, 9]]
b = u * [[5, 7], [8, 5]]

# Declare with correct type so that mypy knows the type to compare to.
c = u * [[1.0, 1.0], [1.0, 1.0]]

c = a + b
should_all_true = c == u * [[7.0, 10.0], [12.0, 14.0]]
assert should_all_true.all().all()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a single .all() is enough


c = a - b
should_all_true = c == u * [[-3.0, -4.0], [-4.0, 4.0]]
assert should_all_true.all().all()

c = a * 7
should_all_true = c == u * [[14.0, 21.0], [28.0, 63.0]]
assert should_all_true.all().all()

c = a / 11
should_all_true = c == u * [[2 / 11, 3 / 11], [4 / 11, 9 / 11]]
assert should_all_true.all().all()
26 changes: 23 additions & 3 deletions test/test_protos.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@

@pytest.mark.parametrize('unit', _ONE_UNIT + _TWO_UNITS)
def test_value_conversion_trip(unit: Value) -> None:
"""
when making a unitful value
we can convert it back and forth to the proto of that unit
and get the same value
"""
rs = np.random.RandomState(0)
for value in rs.random(10):
v = value * unit
Expand All @@ -55,6 +60,10 @@ def test_value_conversion_trip(unit: Value) -> None:

@pytest.mark.parametrize('unit', _ONE_UNIT + _TWO_UNITS)
def test_complex_conversion_trip(unit: Value) -> None:
"""
the roundtrip of conversion back and forth
still works the same way even if the value is complex valued
"""
rs = np.random.RandomState(0)
for value in rs.random(10):
v = 1j * value * unit
Expand All @@ -63,6 +72,10 @@ def test_complex_conversion_trip(unit: Value) -> None:

@pytest.mark.parametrize('unit', _ONE_UNIT + _TWO_UNITS)
def test_valuearray_conversion_trip(unit: Value) -> None:
"""
the roundtrip of conversion back and forth
still works the same way even if the value is array
"""
rs = np.random.RandomState(0)
for value in rs.random((4, 2, 4, 3)):
v = value * unit
Expand All @@ -73,6 +86,10 @@ def test_valuearray_conversion_trip(unit: Value) -> None:

@pytest.mark.parametrize('unit', _ONE_UNIT + _TWO_UNITS)
def test_complex_valuearray_conversion_trip(unit: Value) -> None:
"""
the roundtrip of conversion back and forth
still works the same way even if the value is complex and array
"""
rs = np.random.RandomState(0)
for real, imag in zip(rs.random((4, 2, 4, 3)), rs.random((4, 2, 4, 3))):
v = (real + 1j * imag) * unit
Expand Down Expand Up @@ -128,9 +145,12 @@ def test_unit_exponent_with_zero_denominator_raises() -> None:


def test_scale_values_are_correct() -> None:
assert len(SCALE_PREFIXES) == len(
tunits_pb2.Scale.items()
), f'differing number of scales in proto and SCALE_PREFIXES. If you are adding new scales please update the SCALE_PREFIXES map'
assert len(SCALE_PREFIXES) == len(tunits_pb2.Scale.items()), " ".join(
[
"differing number of scales in proto and SCALE_PREFIXES.",
"If you are adding new scales please update the SCALE_PREFIXES map",
]
)

scale_to_prefix = {
'YOTTA': 'Y',
Expand Down
51 changes: 49 additions & 2 deletions test/test_unit_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,48 @@


def test_all_default_units_and_simple_variations_thereof_are_parseable() -> None:
"""
parsing unit formulas recovers that same default unit
even after doing basic arithmetic, the printed expression
remains parseable and parses to the given value
"""
db = core.default_unit_database
for k, u in db.known_units.items():
assert db.parse_unit_formula(k) == u
for v in [u, 1 / u, 5 * u, 1.1 * u, u**2]:
for v in [u, 1 / u, 5 * u, 1.1 * u, u**2, 1e-20 * u]:
assert db.parse_unit_formula(str(v)) == v


def test_unit_relationship_energy_stored_in_capacity() -> None:
"""
unit relationship of volts, farads and joules
"""
capacitance = 2 * units.uF
voltage = 5 * units.V
stored = capacitance * voltage**2 / 2
assert stored == 25 * units.uJ


def test_durations() -> None:
"""
time units are compatible
- can be added to give another time
- explicit conversion factors for some of the specified durations
- can divide times and get something unitless
"""
a = units.week + units.year + units.day + units.hour + units.minute
assert a.is_compatible(units.second)
assert round(units.year / units.week) == 52
assert np.isclose(units.year / units.second, 31557600)


def test_lengths() -> None:
"""
length units are compatible
- can be added to give another length
- explicit conversion factors for some of the specified lengths (mix of imperial and SI)
- can divide lengths and get something unitless
"""
a = (
units.inch
+ units.foot
Expand All @@ -60,6 +80,11 @@ def test_lengths() -> None:


def test_areas() -> None:
"""
area units are compatible
- can be added to give another area
- can compare areas even if they have different units
"""
assert (units.hectare + units.barn).is_compatible(units.meter**2)

# *Obviously* a hectare of land can hold a couple barns.
Expand All @@ -69,12 +94,23 @@ def test_areas() -> None:


def test_angles() -> None:
"""
angle units are compatible
- can be added to give another angle
- explicit conversion factors for the different units
"""
assert (units.deg + units.cyc).is_compatible(units.rad)
assert np.isclose((math.pi * units.rad)[units.deg], 180)
assert np.isclose((math.pi * units.rad)[units.cyc], 0.5)


def test_volumes() -> None:
"""
volume units are compatible
- can be added to give another volume
- explicit conversion factors for some of the specified volumes (mix of imperial and SI)
- can divide volumes and get something unitless
"""
a = (
units.teaspoon
+ units.tablespoon
Expand All @@ -92,6 +128,12 @@ def test_volumes() -> None:


def test_masses() -> None:
"""
mass units are compatible
- can be added to give another mass
- explicit conversion factors for some of the specified masses (mix of imperial and SI)
- can divide masses and get something unitless
"""
assert np.isclose((units.ounce + units.pound + units.ton) / units.megagram, 0.9077, atol=1e-4)


Expand All @@ -100,7 +142,9 @@ def test_pressures() -> None:


def test_basic_constants() -> None:
# Just some random products compared against results from Wolfram Alpha.
"""
Just some random products compared against results from Wolfram Alpha.
"""

u = units.c * units.mu0 * units.eps0 * units.G * units.hplanck
v = 1.475e-52 * units.m**4 / units.s**2
Expand All @@ -127,5 +171,8 @@ def test_basic_constants() -> None:
],
)
def test_other_constants(lhs: core.Value, rhs: core.Value, ratio: float) -> None:
"""
more physically relevant unitful constants with parameterized pytest
"""
r = lhs / rhs
assert np.isclose(r, ratio, atol=1e-3)
56 changes: 56 additions & 0 deletions test/test_unit_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,26 @@


def test_auto_create() -> None:
"""
will automatically create a unit depending on auto_create_units parameter of __init__
"""
with pytest.raises(KeyError):
UnitDatabase(auto_create_units=False).parse_unit_formula('tests')

db = UnitDatabase(auto_create_units=True)
u = db.parse_unit_formula('tests')
assert str(u) == 'tests'
u_again = db.get_unit('tests')
assert str(u_again) == 'tests'
assert 5 == 5 * u / u


def test_auto_create_disabled_when_purposefully_adding_units() -> None:
"""
even when auto_create_units is True
the auto creation does not happen
with these methods
"""
db = UnitDatabase(auto_create_units=True)

with pytest.raises(KeyError):
Expand All @@ -47,6 +58,10 @@ def test_auto_create_disabled_when_purposefully_adding_units() -> None:


def test_get_unit_with_auto_create_override() -> None:
"""
can override the instance level auto_create_units
with auto_create in method call
"""
db_auto = UnitDatabase(auto_create_units=True)
db_manual = UnitDatabase(auto_create_units=False)

Expand All @@ -67,6 +82,9 @@ def test_get_unit_with_auto_create_override() -> None:


def test_add_root_unit() -> None:
"""
adding a root unit not derived from anything else
"""
db = UnitDatabase(auto_create_units=False)
db.add_root_unit('cats')

Expand All @@ -85,6 +103,10 @@ def test_add_root_unit() -> None:


def test_add_base_unit_with_prefixes() -> None:
"""
adding a root unit not derived from anything else
and prefixes for orders of magnitude
"""
db = UnitDatabase(auto_create_units=False)
db.add_base_unit_data(
BaseUnitData('b', 'base', True),
Expand Down Expand Up @@ -120,6 +142,9 @@ def test_add_base_unit_with_prefixes() -> None:


def test_add_base_unit_without_prefixes() -> None:
"""
can disallow prefixes with False in use_prefixes argument to BaseUnitData
"""
db = UnitDatabase(auto_create_units=False)
db.add_base_unit_data(
BaseUnitData('b', 'base', False),
Expand Down Expand Up @@ -157,6 +182,9 @@ def test_add_base_unit_without_prefixes() -> None:


def test_add_derived_unit_with_prefixes() -> None:
"""
can add a derived unit that can be prefixed
"""
db = UnitDatabase(auto_create_units=False)
with pytest.raises(KeyError):
db.add_derived_unit_data(DerivedUnitData('tails', 't', 'shirts'), [])
Expand Down Expand Up @@ -187,13 +215,24 @@ def test_add_derived_unit_with_prefixes() -> None:
assert db.get_unit('super_tails') == v * 10
assert db.get_unit('d_t') == v * 100
assert db.get_unit('duper_tails') == v * 100
with pytest.raises(KeyError):
db.get_unit('super_shirts')
with pytest.raises(KeyError):
db.get_unit('s_shirts')
with pytest.raises(KeyError):
db.get_unit('d_shirts')
with pytest.raises(KeyError):
db.get_unit('duper_shirts')
with pytest.raises(KeyError):
db.get_unit('s_tails')
with pytest.raises(KeyError):
db.get_unit('super_t')


def test_add_derived_unit_without_prefixes() -> None:
"""
can add a derived unit that cannot be prefixed
"""
db = UnitDatabase(auto_create_units=False)

db.add_root_unit('shirts')
Expand Down Expand Up @@ -226,6 +265,10 @@ def test_add_derived_unit_without_prefixes() -> None:


def test_kilogram_special_case() -> None:
"""
kilogram example
unusual in that the base unit has a prefix
"""
db = UnitDatabase(auto_create_units=False)
db.add_base_unit_data(BaseUnitData('kg', 'kilogram'), SI_PREFIXES)
assert db.get_unit('g').base_units == raw_UnitArray([('kg', 1, 1)])
Expand All @@ -234,6 +277,11 @@ def test_kilogram_special_case() -> None:


def test_parse_unit_formula() -> None:
"""
with custom root units
usual dimensional arithmetic with those units holds
when not using any float numerical factors
"""
db = UnitDatabase(auto_create_units=False)
db.add_root_unit('cats')
db.add_root_unit('dogs')
Expand All @@ -256,6 +304,10 @@ def test_parse_unit_formula() -> None:


def test_parse_float_formula() -> None:
"""
with custom root units
usual dimensional arithmetic with those units and floats holds
"""
db = UnitDatabase(auto_create_units=False)
db.add_root_unit('J')
db.add_root_unit('s')
Expand All @@ -268,6 +320,9 @@ def test_parse_float_formula() -> None:


def test_is_consistent_with_database() -> None:
"""
are conversions consistent with what is known in db
"""
db = UnitDatabase(auto_create_units=True)

# Empty.
Expand All @@ -282,6 +337,7 @@ def test_is_consistent_with_database() -> None:

# Self-contradictory conversion.
assert not db.is_value_consistent_with_database(val(6, conv=conv(3), units=unit('theorems')))
assert db.is_value_consistent_with_database(val(6, conv=conv(1), units=unit('theorems')))

# Inconsistent conversion.
db.add_scaled_unit('kilo_theorems', 'theorems', exp10=3)
Expand Down
Loading