CI test: unit system API checks#

This example checks how shamrock.UnitSystem and shamrock.Constants behave when used from Python: the unit and prefix names, the meaning of get / to with power and pref, the physical constants, and the JSON round trip.

It runs as part of the documentation build, so any change to that behaviour makes the build fail.

14 import math
15
16 import shamrock

Helpers#

Every check compares floats with a relative tolerance.

24 def check(actual, expected, what, rel_tol=1e-12):
25     assert math.isclose(actual, expected, rel_tol=rel_tol), (
26         f"{what}: got {actual!r}, expected {expected!r}"
27     )
28
29
30 def check_raises(exc_type, func, what):
31     try:
32         func()
33     except exc_type:
34         return
35     raise AssertionError(f"{what}: expected {exc_type.__name__} to be raised")

Unit and prefix names#

Every unit has a long and a short name, and both give the same value. Pairs are listed as (long name, short name).

 44 unit_names = [
 45     ("second", "s"),
 46     ("metre", "m"),
 47     ("kilogram", "kg"),
 48     ("Ampere", "A"),
 49     ("Kelvin", "K"),
 50     ("mole", "mol"),
 51     ("candela", "cd"),
 52     ("Hertz", "Hz"),
 53     ("Newton", "N"),
 54     ("Pascal", "Pa"),
 55     ("Joule", "J"),
 56     ("Watt", "W"),
 57     ("Coulomb", "C"),
 58     ("Volt", "V"),
 59     ("Farad", "F"),
 60     ("Ohm", "ohm"),
 61     ("Siemens", "S"),
 62     ("Weber", "Wb"),
 63     ("Tesla", "T"),
 64     ("Henry", "H"),
 65     ("lumens", "lm"),
 66     ("lux", "lx"),
 67     ("Becquerel", "Bq"),
 68     ("Gray", "Gy"),
 69     ("Sievert", "Sv"),
 70     ("katal", "kat"),
 71     ("minutes", "mn"),
 72     ("hours", "hr"),
 73     ("days", "dy"),
 74     ("years", "yr"),
 75     ("astronomical_unit", "au"),
 76     ("light_year", "ly"),
 77     ("parsec", "pc"),
 78     ("solar_radius", "rsol"),
 79     ("earth_radius", "rearth"),
 80     ("solar_mass", "sol_mass"),
 81     ("electron_volt", "eV"),
 82     ("ergs", "erg"),
 83     ("british_pint", "pint"),
 84 ]
 85
 86 # Prefixes as ``(long name, short name, power of ten)``.
 87 # Note that ``"m"`` as a prefix is milli, not metre.
 88 prefix_names = [
 89     ("tera", "T", 12),
 90     ("giga", "G", 9),
 91     ("mega", "M", 6),
 92     ("kilo", "k", 3),
 93     ("hecto", "hect", 2),
 94     ("deca", "dec", 1),
 95     ("None", "_", 0),
 96     ("centi", "c", -2),
 97     ("milli", "m", -3),
 98     ("micro", "mu", -6),
 99     ("nano", "n", -9),
100     ("pico", "p", -12),
101     ("femto", "f", -15),
102 ]

SI unit system#

The default constructor gives SI units, so every base unit is 1.

109 si = shamrock.UnitSystem()
110
111 check(si.get("m"), 1.0, 'si.get("m")')
112 check(si.get("metre"), 1.0, 'si.get("metre")')
113 check(si.get("s"), 1.0, 'si.get("s")')
114 check(si.get("kg"), 1.0, 'si.get("kg")')
115 check(si.get("s", power=-1), 1.0, 'si.get("s", power=-1)')

pref takes the long or the short prefix name, positionally or by keyword.

119 check(si.get("m", 1, "kilo"), 1e3, 'si.get("m", 1, "kilo")')
120 check(si.get("m", pref="k"), 1e3, 'si.get("m", pref="k")')
121 check(si.get("m", pref="c"), 1e-2, 'si.get("m", pref="c")')
122 check(si.get("kg", pref="m"), 1e-3, 'si.get("kg", pref="m")')
123
124 for long_name, short_name, exponent in prefix_names:
125     check(si.get("m", pref=long_name), 10.0**exponent, f'si.get("m", pref="{long_name}")')
126     check(si.get("m", pref=short_name), 10.0**exponent, f'si.get("m", pref="{short_name}")')

power raises the prefixed unit, so the prefix is raised too.

130 check(si.get("m", power=2, pref="k"), 1e6, 'si.get("m", power=2, pref="k")')
131 check(si.get("m", power=-3, pref="c"), 1e6, 'si.get("m", power=-3, pref="c")')

get gives the value of one unit in the system’s units, to gives the value of one system unit in the given unit. to is the inverse of get.

136 check(si.get("au"), 149597870700.0, 'si.get("au")')
137 check(si.to("au"), 1.0 / 149597870700.0, 'si.to("au")')
138 check(si.get("pc"), 3.0857e16, 'si.get("pc")')
139 check(si.get("yr"), 31557600.0, 'si.get("yr")')
140 check(si.to("m", pref="k"), 1e-3, 'si.to("m", pref="k")')

Conversions used by shamrock.utils.analysis.UnitHelper

145 # Distances
146 check(si.to("m"), 1.0, 'si.to("m")')
147 check(si.to("m", pref="c"), 1e2, 'si.to("m", pref="c")')
148 check(si.to("m", pref="k"), 1e-3, 'si.to("m", pref="k")')
149 check(si.to("au"), 1.0 / 149597870700.0, 'si.to("au")')
150 check(si.to("pc"), 1.0 / 3.0857e16, 'si.to("pc")')
151
152 # Times
153 check(si.to("s"), 1.0, 'si.to("s")')
154 check(si.to("mn"), 1.0 / 60.0, 'si.to("mn")')
155 check(si.to("hr"), 1.0 / 3600.0, 'si.to("hr")')
156 check(si.to("dy"), 1.0 / 86400.0, 'si.to("dy")')
157 check(si.to("yr"), 1.0 / 31557600.0, 'si.to("yr")')
158
159 # Inverse times
160 check(si.to("s", power=-1), 1.0, 'si.to("s", power=-1)')
161 check(si.to("yr", power=-1), 31557600.0, 'si.to("yr", power=-1)')
162
163 # Masses, surface densities and densities
164 check(si.to("kg"), 1.0, 'si.to("kg")')
165 check(si.to("kg", pref="m"), 1e3, 'si.to("kg", pref="m")')
166 check(si.to("m", power=-2), 1.0, 'si.to("m", power=-2)')
167 check(si.to("m", power=-2, pref="c"), 1e-4, 'si.to("m", power=-2, pref="c")')
168 check(si.to("m", power=-3), 1.0, 'si.to("m", power=-3)')
169 check(si.to("m", power=-3, pref="c"), 1e-6, 'si.to("m", power=-3, pref="c")')
170
171 # Accelerations and magnetic field
172 check(si.to("s", power=-2), 1.0, 'si.to("s", power=-2)')
173 check(si.to("T"), 1.0, 'si.to("T")')

Physical constants in SI#

Each constant takes an optional power.

180 sicte = shamrock.Constants(si)
181
182 si_constants = {
183     "au": 149597870700.0,
184     "G": 6.6743015e-11,
185     "c": 299792458.0,
186     "year": 31557600.0,
187     "sol_mass": 1.98847e30,
188     "second": 1.0,
189     "kb": 1.380649e-23,
190     "dalton": 1.66053906892e-27,
191     "mu_0": 1.2566370621219e-6,
192 }
193
194 for name, value in si_constants.items():
195     func = getattr(sicte, name)
196     check(func(), value, f"sicte.{name}()")
197     check(func(power=1), value, f"sicte.{name}(power=1)")
198     check(func(power=2), value**2, f"sicte.{name}(power=2)")
199     check(func(power=-1), 1.0 / value, f"sicte.{name}(power=-1)")

Every constant exposed to Python, by name.

203 constant_names = [
204     "delta_nu_cs",
205     "c",
206     "h",
207     "e",
208     "k",
209     "Na",
210     "Kcd",
211     "second",
212     "minute",
213     "hour",
214     "day",
215     "year",
216     "au",
217     "astronomical_unit",
218     "light_year",
219     "parsec",
220     "solar_radius",
221     "earth_radius",
222     "planck_length",
223     "proton_mass",
224     "electron_mass",
225     "earth_mass",
226     "jupiter_mass",
227     "sol_mass",
228     "planck_mass",
229     "dalton",
230     "guiness_density",
231     "G",
232     "hbar",
233     "mu_0",
234     "Z_0",
235     "epsilon_0",
236     "ke",
237     "kb",
238     "sigma",
239 ]
240
241 for name in constant_names:
242     value = getattr(sicte, name)()
243     assert math.isfinite(value) and value > 0, f"sicte.{name}() = {value!r}"
244     check(getattr(sicte, name)(power=2), value**2, f"sicte.{name}(power=2)")

Astrophysical unit system#

Time in Myr, length in au and mass in solar masses, built from literals.

251 unit_time = 3.15576e13
252 unit_length = 1.495978707e11
253 unit_mass = 1.98847e30
254
255 astro = shamrock.UnitSystem(unit_time=unit_time, unit_length=unit_length, unit_mass=unit_mass)
256
257 check(astro.to("au"), 1.0, 'astro.to("au")')
258 check(astro.get("au"), 1.0, 'astro.get("au")')
259 check(astro.to("sol_mass"), 1.0, 'astro.to("sol_mass")')
260 check(astro.to("yr"), 1e6, 'astro.to("yr")')
261 check(astro.to("s"), unit_time, 'astro.to("s")')
262 check(astro.to("m"), unit_length, 'astro.to("m")')
263 check(astro.to("kg"), unit_mass, 'astro.to("kg")')
264 check(astro.get("s"), 1.0 / unit_time, 'astro.get("s")')
265 check(astro.get("T"), unit_time**2 / unit_mass, 'astro.get("T")')
266 check(astro.to("m", power=-3, pref="c"), unit_length**-3 * 1e-6, 'astro.to("m", -3, "c")')

Long and short unit names resolve to the same unit.

270 for long_name, short_name in unit_names:
271     check(astro.get(long_name), astro.get(short_name), f'astro.get("{long_name}")')
272     check(astro.to(long_name), astro.to(short_name), f'astro.to("{long_name}")')

A prefix applies once to the whole unit, even for a unit built from other units: Myr is 1e6 yr and Gyr is 1e9 yr.

277 check(si.to("yr", pref="M"), 1e-6 / 31557600.0, 'si.to("yr", pref="M")')
278 check(astro.to("yr", pref="M"), 1.0, 'astro.to("yr", pref="M")')
279 check(astro.to("yr", pref="G"), 1e-3, 'astro.to("yr", pref="G")')

Constants in the astrophysical system

283 astrocte = shamrock.Constants(astro)
284
285 check(astrocte.au(), 1.0, "astrocte.au()")
286 check(astrocte.sol_mass(), 1.0, "astrocte.sol_mass()")
287 check(astrocte.year(), 1e-6, "astrocte.year()")
288 check(astrocte.second(), 1.0 / unit_time, "astrocte.second()")
289 check(astrocte.c(), 299792458.0 * unit_time / unit_length, "astrocte.c()")
290 check(
291     astrocte.G(),
292     6.6743015e-11 * unit_mass * unit_time**2 / unit_length**3,
293     "astrocte.G()",
294 )
295 check(astrocte.G(power=-1), 1.0 / astrocte.G(), "astrocte.G(power=-1)")
296
297 # In au, solar masses and years, G is close to 4 pi^2 (Kepler's third law)
298 check(astrocte.G() / 1e12, 4 * math.pi**2, "astrocte.G() vs 4 pi^2", rel_tol=1e-3)
299
300 print("G in au^3 Msun^-1 Myr^-2 :", astrocte.G())
G in au^3 Msun^-1 Myr^-2 : 39478129059350.664

JSON round trip#

to_json returns a dict with the seven base units in SI, and from_json sets a unit system from such a dict.

308 astro_json = astro.to_json()
309 print(astro_json)
310
311 expected_json = {
312     "unit_time": unit_time,
313     "unit_length": unit_length,
314     "unit_mass": unit_mass,
315     "unit_current": 1.0,
316     "unit_temperature": 1.0,
317     "unit_qte": 1.0,
318     "unit_lumint": 1.0,
319 }
320 assert set(astro_json.keys()) == set(expected_json.keys()), astro_json
321 for key, value in expected_json.items():
322     check(astro_json[key], value, f"astro.to_json()[{key!r}]")
323
324 reloaded = shamrock.UnitSystem()
325 reloaded.from_json(astro_json)
326
327 for key, value in expected_json.items():
328     check(reloaded.to_json()[key], value, f"reloaded.to_json()[{key!r}]")
329 for long_name, short_name in unit_names:
330     check(reloaded.get(short_name), astro.get(short_name), f'reloaded.get("{short_name}")')
331 check(shamrock.Constants(reloaded).G(), astrocte.G(), "Constants(reloaded).G()")
{'unit_current': 1.0, 'unit_length': 149597870700.0, 'unit_lumint': 1.0, 'unit_mass': 1.98847e+30, 'unit_qte': 1.0, 'unit_temperature': 1.0, 'unit_time': 31557600000000.0}

Errors#

Unknown unit or prefix names raise ValueError, and the constructor only takes keyword arguments.

339 check_raises(ValueError, lambda: si.get("not_a_unit"), 'si.get("not_a_unit")')
340 check_raises(ValueError, lambda: si.to("not_a_unit"), 'si.to("not_a_unit")')
341 check_raises(ValueError, lambda: si.get("m", pref="not_a_prefix"), 'pref="not_a_prefix"')
342 check_raises(TypeError, lambda: shamrock.UnitSystem(1.0), "UnitSystem(1.0)")
343
344 print("All unit system API checks passed")
All unit system API checks passed

Total running time of the script: (0 minutes 22.379 seconds)

Estimated memory usage: 164 MB

Gallery generated by Sphinx-Gallery