Note
Go to the end to download the full example code.
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.
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.
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.
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.
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