Skip to content

Commit 6ca6b8d

Browse files
author
Martin D. Weinberg
committed
Add unit conversion explanation and script
1 parent b7323a2 commit 6ca6b8d

6 files changed

Lines changed: 458 additions & 0 deletions

File tree

EXP-units/README_expunits.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Example unit conversion files (Earth–Sun)
2+
3+
## This directory contains:
4+
5+
| File | Contents |
6+
| --- | --- |
7+
|earth_sun_cgs.txt | two-particle file (Sun + Earth) in CGS units |
8+
|earth_sun_scaled.txt | expected converted file in scaled units (AU, M_sun, v'=1) |
9+
| expunits.py | conversion script (compute scales + optional conversion) |
10+
|README_expunits.md | this file |
11+
12+
## Quick reproduction
13+
14+
- The Python3 script requires numpy (e.g. pip install numpy).
15+
16+
- From this directory, run:
17+
18+
- python3 expunits.py --old-length 1 --new-length 1.495978707e13 --old-mass 1 --new-mass 1.98847e33 --G_old 6.67430e-8 --G_new 1 --infile earth_sun_cgs.txt --outfile earth_sun_scaled_by_script.txt --print-scales --sci
19+
20+
### Explanation of flags
21+
22+
- --old-length 1 and --new-length 1.495978707e13: Interpret old-length unit = 1 cm, new-length unit = 1 AU = 1.495978707e13 cm, so s_L = new_length / old_length = 1.495978707e13.
23+
- --old-mass 1 and --new-mass 1.98847e33: Interpret old-mass unit = 1 g, new-mass unit = 1 M_sun = 1.98847e33 g, so s_M = 1.98847e33.
24+
- --G_old 6.67430e-8 is the cgs gravitational constant (cm^3 g^-1 s^-2).
25+
- --G_new 1 requests that numeric G in the new numeric units equals 1.
26+
- The script computes s_V from the relation s_M = s_L * s_V^2 * (G_new/G_old). Thus V_new (the new velocity unit) is chosen so that v_earth_old / s_V = 1.0.
27+
28+
## Expected checks
29+
30+
The script prints s_L, s_V, s_M.
31+
32+
Expected approximate values: s_L ≈ 1.4959787e13 s_M ≈ 1.98847e33 s_V ≈ 2.978e6 (cm/s per one new velocity unit)
33+
34+
After conversion, earth_sun_scaled_by_script.txt should match
35+
earth_sun_scaled.txt to within rounding:
36+
37+
Sun: m' ≈ 1.0, r' ≈ 0.0, v' ≈ -3.00349e-6
38+
Earth: m' ≈ 3.00349e-6, r' ≈ 1.0, v' ≈ 1.0

EXP-units/earth_sun_cgs.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# m[g] x[cm] y[cm] z[cm] u[cm/s] v[cm/s] w[cm/s]
2+
1.98847e33 0.0 0.0 0.0 0.0 -8.944963 0.0
3+
5.97219e27 1.495978707e13 0.0 0.0 0.0 2.978000e6 0.0

EXP-units/earth_sun_scaled.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
m'[M_sun] x'[AU] y'[AU] z'[AU] u' v' w'
2+
1.000000000000000 0.0 0.0 0.0 0.0 -3.0034896e-06 0.0
3+
3.0034896e-06 1.0 0.0 0.0 0.0 1.0000000e+00 0.0

EXP-units/expunits.pdf

162 KB
Binary file not shown.

EXP-units/expunits.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#!/usr/bin/env python3
2+
"""
3+
expunits.py
4+
5+
General unit-scale calculator & optional particle-file converter.
6+
7+
Core relation (general):
8+
G_new = G_old * (s_L * s_V^2) / s_M
9+
so
10+
s_M = s_L * s_V^2 * G_new / G_old
11+
s_V = sqrt( s_M * G_old / (s_L * G_new) )
12+
s_L = s_M * G_old / (s_V^2 * G_new)
13+
14+
Command-line usage notes:
15+
- Provide old/new unit definitions in *the same physical units* (e.g. both
16+
lengths in kpc, both masses in Msun).
17+
18+
For example: --old-length 1 --new-length 300 (means old length unit = 1 kpc,
19+
new length unit = 300 kpc)
20+
21+
- You must supply enough information to determine the three scale factors:
22+
Provide any two of (s_L, s_V, s_M) by giving the corresponding
23+
old/new pairs.
24+
25+
Example ways to provide two:
26+
* old-length & new-length AND old-velocity & new-velocity
27+
* old-length & new-length AND old-mass & new-mass
28+
* old-velocity & new-velocity AND old-mass & new-mass
29+
30+
Optionally, instead of giving two pairs, use the circular-velocity preset
31+
mode (see README below).
32+
33+
- G_old and G_new are numeric gravitational constants in the old/new numeric unit systems respectively.
34+
For typical conversions to G_new = 1 set --G_new 1.0 (default).
35+
36+
- Optional: --infile/--outfile to convert a 7-column particle file with columns: m x y z u v w
37+
38+
- Optional: --precision N prints s_* with N decimal digits; --sci prints scientific notation.
39+
40+
Presets:
41+
--preset gadget : old_length=1 (kpc), old_mass=1e10 (Msun), G_old=43007.1
42+
--preset bonsai : old_length=1 (kpc), old_mass=1.0 (Msun), G_old=4.5e-06
43+
44+
Examples:
45+
46+
# Using gadget preset and target new units (L_new = 300 kpc, M_new = 1e12 Msun, want G_new=1)
47+
python convert_units_general.py --preset gadget --new-length 300 --new-mass 1e12 --G_new 1 --print-scales
48+
49+
# Provide explicit old/new units (length in kpc, mass in Msun, velocity in km/s):
50+
python convert_units_general.py --old-length 1 --new-length 300 --old-mass 1 --new-mass 1e12 --G_old 4.5e-06 --G_new 1 --print-scales
51+
52+
# Convert a particle file (7 cols) using the scales computed above:
53+
python convert_units_general.py --preset bonsai --new-length 300 --new-mass 1e12 --G_new 1 --infile particles.txt --outfile particles_g1.txt
54+
55+
56+
Notes and guidance:
57+
58+
- Units must be given in the same physical system for old/new comparisons.
59+
60+
Example:
61+
* If you treat lengths in kpc, give both --old-length and --new-length in kpc.
62+
63+
* If you use Msun for mass give both --old-mass and --new-mass in Msun.
64+
65+
- If you use a preset (--preset gadget or --preset bonsai) the preset sets
66+
old-length, old-mass, and G_old unless you override them on the command line.
67+
68+
- The script is defensive:
69+
70+
* You must supply at least two old/new pairs (so the third scale can be
71+
calculated from the G relation)
72+
73+
* Or supply all three and they must be consistent with the requested G_new.
74+
75+
- For conversions to the usual G_new = 1 choose --G_new 1.0 (default).
76+
77+
- The printed s_L, s_V, s_M are "old-unit-per-1-new-unit". To convert
78+
numeric particle values: r' = r_old / s_L, v' = v_old / s_V, m' = m_old / s_M
79+
80+
- The particle-file conversion expects 7 whitespace-separated columns per row:
81+
mass x y z u v w.
82+
83+
"""
84+
85+
import argparse
86+
import sys
87+
import numpy as np
88+
import math
89+
90+
PRESETS = {
91+
'gadget': {
92+
'old_length': 1.0, # kpc
93+
'old_mass': 1.0e10, # Msun
94+
'G_old': 43007.1
95+
},
96+
'bonsai': {
97+
'old_length': 1.0, # kpc
98+
'old_mass': 1.0, # Msun
99+
'G_old': 4.5e-06
100+
}
101+
}
102+
103+
def compute_scales(G_old, G_new, s_L, s_V, s_M):
104+
"""
105+
Given G_old, G_new and any two of (s_L, s_V, s_M) compute the third.
106+
s_L, s_V, s_M are either floats or None.
107+
Returns (s_L, s_V, s_M). Raises ValueError if insufficient or inconsistent.
108+
"""
109+
known = [(s_L is not None), (s_V is not None), (s_M is not None)]
110+
n_known = sum(known)
111+
112+
# If all three provided, check consistency with G_old/G_new
113+
if n_known == 3:
114+
G_check = (s_L * s_V**2) / (G_old * s_M)
115+
if not math.isfinite(G_check):
116+
raise ValueError("Computed G_check is not finite.")
117+
if abs((G_check - G_new) / (abs(G_new) if G_new != 0 else 1.0)) > 1e-9:
118+
raise ValueError(f"Inconsistent scales: G_new requested {G_new} but scales give {G_check}.")
119+
return s_L, s_V, s_M
120+
121+
if n_known < 2:
122+
raise ValueError("Insufficient inputs: need at least two of s_L, s_V, s_M (i.e., two old/new pairs).")
123+
124+
# If s_L and s_V known -> s_M
125+
if s_L is not None and s_V is not None:
126+
s_M_calc = s_L * s_V**2 * G_new / G_old
127+
return s_L, s_V, s_M_calc
128+
129+
# If s_L and s_M known -> s_V
130+
if s_L is not None and s_M is not None:
131+
denom = s_L * G_new / G_old
132+
if denom <= 0:
133+
raise ValueError("Non-positive denominator in s_V calculation.")
134+
s_V_calc = math.sqrt(s_M * G_old / (s_L * G_new))
135+
return s_L, s_V_calc, s_M
136+
137+
# If s_V and s_M known -> s_L
138+
if s_V is not None and s_M is not None:
139+
denom = s_V**2 * G_new / G_old
140+
if denom <= 0:
141+
raise ValueError("Non-positive denominator in s_L calculation.")
142+
s_L_calc = s_M * G_old / (s_V**2 * G_new)
143+
return s_L_calc, s_V, s_M
144+
145+
raise ValueError("Unhandled case in compute_scales.")
146+
147+
def parse_args():
148+
p = argparse.ArgumentParser(description="Compute unit-scale factors and optionally convert a 7-column particle file.")
149+
# presets
150+
p.add_argument("--preset", choices=PRESETS.keys(), help="Optional preset for old units (gadget, bonsai).")
151+
152+
# old units (user-specified)
153+
p.add_argument("--old-length", type=float, default=None, help="Old length unit expressed in some physical length units (e.g. kpc).")
154+
p.add_argument("--old-mass", type=float, default=None, help="Old mass unit expressed in some physical mass units (e.g. Msun).")
155+
p.add_argument("--old-velocity", type=float, default=None, help="Old velocity unit expressed in some physical velocity units (e.g. km/s).")
156+
157+
# new units (target)
158+
p.add_argument("--new-length", type=float, default=None, help="New length unit in same physical units as --old-length (e.g. 300 for 300 kpc).")
159+
p.add_argument("--new-mass", type=float, default=None, help="New mass unit in same physical units as --old-mass (e.g. 1e12 for 1e12 Msun).")
160+
p.add_argument("--new-velocity", type=float, default=None, help="New velocity unit in same physical units as --old-velocity (e.g. 1 for 1 km/s).")
161+
162+
# numeric G values
163+
p.add_argument("--G_old", type=float, default=None, help="Numeric gravitational constant in OLD numeric units.")
164+
p.add_argument("--G_new", type=float, default=1.0, help="Numeric gravitational constant in NEW numeric units (default 1.0).")
165+
166+
# files for conversion
167+
p.add_argument("--infile", type=str, default=None, help="Optional input particle file (7 columns: m x y z u v w).")
168+
p.add_argument("--outfile", type=str, default=None, help="If --infile given, write converted particles to this file.")
169+
170+
# print options
171+
p.add_argument("--print-scales", action="store_true", help="Print computed scale factors.")
172+
p.add_argument("--precision", type=int, default=None, help="Number of decimal digits when printing scales (overrides --sci).")
173+
p.add_argument("--sci", action="store_true", help="Print scales in scientific notation (default formatting if --precision is not set).")
174+
p.add_argument("--fmt", type=str, default="%.6e %.6e %.6e %.6e %.6e %.6e %.6e", help="Output format string for converted particle file.")
175+
p.add_argument("--skiprows", type=int, default=0, help="Rows to skip when reading input particle file (e.g. header lines).")
176+
return p.parse_args()
177+
178+
def resolve_old_new_from_preset(args):
179+
# Fill args.old-length and args.old-mass and G_old from preset if present
180+
if args.preset:
181+
preset = PRESETS[args.preset]
182+
if args.old_length is None:
183+
args.old_length = preset['old_length']
184+
if args.old_mass is None:
185+
args.old_mass = preset['old_mass']
186+
if args.G_old is None:
187+
args.G_old = preset['G_old']
188+
189+
def compute_and_maybe_convert(args):
190+
# Resolve preset values
191+
resolve_old_new_from_preset(args)
192+
193+
# Validate G_old provided
194+
if args.G_old is None:
195+
raise SystemExit("G_old must be specified either explicitly (--G_old) or via a preset (--preset).")
196+
197+
# Compute simple scale candidates from provided old/new unit values
198+
s_L = None
199+
s_V = None
200+
s_M = None
201+
202+
if args.old_length is not None and args.new_length is not None:
203+
if args.old_length == 0:
204+
raise SystemExit("old-length cannot be zero.")
205+
s_L = args.new_length / args.old_length
206+
207+
if args.old_mass is not None and args.new_mass is not None:
208+
if args.old_mass == 0:
209+
raise SystemExit("old-mass cannot be zero.")
210+
s_M = args.new_mass / args.old_mass
211+
212+
if args.old_velocity is not None and args.new_velocity is not None:
213+
if args.old_velocity == 0:
214+
raise SystemExit("old-velocity cannot be zero.")
215+
s_V = args.new_velocity / args.old_velocity
216+
217+
# Compute missing scale(s)
218+
try:
219+
s_L, s_V, s_M = compute_scales(args.G_old, args.G_new, s_L, s_V, s_M)
220+
except ValueError as e:
221+
raise SystemExit("Error computing scales: " + str(e))
222+
223+
# Optionally print scales
224+
if args.print_scales:
225+
if args.precision is not None:
226+
fmt = "{:." + str(args.precision) + "f}"
227+
print("s_L =", fmt.format(s_L))
228+
print("s_V =", fmt.format(s_V))
229+
print("s_M =", fmt.format(s_M))
230+
elif args.sci:
231+
print("s_L = {:.6e}".format(s_L))
232+
print("s_V = {:.6e}".format(s_V))
233+
print("s_M = {:.6e}".format(s_M))
234+
else:
235+
print("s_L =", s_L)
236+
print("s_V =", s_V)
237+
print("s_M =", s_M)
238+
239+
# Print check of G_new
240+
G_new_check = (s_L * s_V**2) / (s_M * args.G_old)
241+
print("Check: G_new = (s_L s_V^2) / (G_old * s_M) = {:.12g} (requested G_new = {})".format(G_new_check, args.G_new))
242+
243+
# If infile specified, convert and write
244+
if args.infile:
245+
if not args.outfile:
246+
raise SystemExit("When --infile is provided you must supply --outfile.")
247+
try:
248+
data = np.loadtxt(args.infile, comments="#", skiprows=args.skiprows)
249+
except Exception as e:
250+
raise SystemExit("Failed reading infile: " + str(e))
251+
252+
if data.ndim == 1:
253+
if data.size == 7:
254+
data = data.reshape((1,7))
255+
else:
256+
raise SystemExit("Input file doesn't have 7 columns per row.")
257+
258+
if data.shape[1] != 7:
259+
raise SystemExit("Input file must have 7 columns (m x y z u v w). Found {}".format(data.shape[1]))
260+
261+
out = np.empty_like(data, dtype=float)
262+
out[:,0] = data[:,0] / s_M # mass
263+
out[:,1:4] = data[:,1:4] / s_L # positions
264+
out[:,4:7] = data[:,4:7] / s_V # velocities
265+
266+
try:
267+
np.savetxt(args.outfile, out, fmt=args.fmt)
268+
except Exception as e:
269+
raise SystemExit("Failed writing outfile: " + str(e))
270+
271+
print("Converted {} -> {} using s_L={:.6g}, s_V={:.6g}, s_M={:.6g}".format(args.infile, args.outfile, s_L, s_V, s_M))
272+
273+
return s_L, s_V, s_M
274+
275+
def main():
276+
args = parse_args()
277+
try:
278+
s_L, s_V, s_M = compute_and_maybe_convert(args)
279+
except SystemExit as e:
280+
# argparse or compute_and_maybe_convert can call SystemExit for user errors
281+
print(e, file=sys.stderr)
282+
sys.exit(1)
283+
284+
if __name__ == "__main__":
285+
main()
286+

0 commit comments

Comments
 (0)