Skip to content

Commit c5b30bd

Browse files
author
jngaravitoc
committed
unifying make model and adding docstrings and tests
1 parent 2a2624d commit c5b30bd

File tree

2 files changed

+157
-76
lines changed

2 files changed

+157
-76
lines changed
Lines changed: 103 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,118 @@
11
import numpy as np
2-
import scipy
3-
from scipy.interpolate import interp1d, BSpline, splrep
42

5-
def makemodel(func, M, funcargs, rvals = 10.**np.linspace(-2.,4.,2000), pfile='', plabel = '',verbose=True):
6-
"""make an EXP-compatible spherical basis function table
7-
8-
inputs
9-
-------------
10-
func : (function) the callable functional form of the density
11-
M : (float) the total mass of the model, sets normalisations
12-
funcargs : (list) a list of arguments for the density function.
13-
rvals : (array of floats) radius values to evaluate the density function
14-
pfile : (string) the name of the output file. If '', will not print file
15-
plabel : (string) comment string
16-
verbose : (boolean)
17-
18-
outputs
19-
-------------
20-
R : (array of floats) the radius values
21-
D : (array of floats) the density
22-
M : (array of floats) the mass enclosed
23-
P : (array of floats) the potential
24-
3+
def _write_table(tablename, radius, density, mass, potential):
254
"""
26-
27-
R = np.nanmax(rvals)
28-
29-
# query out the density values
30-
if func == empirical_density_profile:
31-
rvals, dvals = func(rvals,*funcargs)
32-
else:
33-
dvals = func(rvals,*funcargs)
34-
35-
# make the mass and potential arrays
36-
mvals = np.zeros(dvals.size)
37-
pvals = np.zeros(dvals.size)
38-
pwvals = np.zeros(dvals.size)
39-
40-
# initialise the mass enclosed an potential energy
41-
mvals[0] = 1.e-15
5+
Write a table of radius, density, mass, and potential values to a text file.
6+
7+
Parameters
8+
----------
9+
tablename : str
10+
Name of the output file where the table will be written.
11+
radius : array-like
12+
Radius values.
13+
density : array-like
14+
Density values corresponding to radius.
15+
mass : array-like
16+
Mass values corresponding to radius.
17+
potential : array-like
18+
Potential values corresponding to radius.
19+
20+
Returns
21+
-------
22+
None
23+
"""
24+
with open(tablename, 'w') as f:
25+
print('! ', tablename, file=f)
26+
print('! R D M P', file=f)
27+
print(radius.size, file=f)
28+
for r, d, m, p in zip(radius, density, mass, potential):
29+
print(f'{r} {d} {m} {p}', file=f)
30+
31+
def makemodel(radius, density, Mtotal, output_filename='', physial_units=False, verbose=True):
32+
"""
33+
Generate an EXP-compatible spherical basis function table.
34+
35+
Parameters
36+
----------
37+
radius : array-like
38+
Radii at which the density values are evaluated.
39+
density : array-like
40+
Density values corresponding to radius.
41+
Mtotal : float
42+
Total mass of the model, used for normalization.
43+
output_filename : str, optional
44+
Name of the output file to save the table. If empty, no file is written.
45+
verbose : bool, optional
46+
If True, prints scaling information.
47+
48+
Returns
49+
-------
50+
radius_scaled : ndarray
51+
Scaled radius values.
52+
density_scaled : ndarray
53+
Scaled density values.
54+
mass_scaled : ndarray
55+
Scaled enclosed mass values.
56+
potential_scaled : ndarray
57+
Scaled potential values.
58+
"""
59+
Rmax = np.nanmax(radius)
60+
61+
mass = np.zeros_like(density)
62+
pwvals = np.zeros_like(density)
63+
64+
mass[0] = 1.e-15
4265
pwvals[0] = 0.
4366

44-
# evaluate mass enclosed and potential energy by recursion
45-
for indx in range(1,dvals.size):
46-
mvals[indx] = mvals[indx-1] +\
47-
2.0*np.pi*(rvals[indx-1]*rvals[indx-1]*dvals[indx-1] +\
48-
rvals[indx]*rvals[indx]*dvals[indx])*(rvals[indx] - rvals[indx-1]);
49-
pwvals[indx] = pwvals[indx-1] + \
50-
2.0*np.pi*(rvals[indx-1]*dvals[indx-1] + rvals[indx]*dvals[indx])*(rvals[indx] - rvals[indx-1]);
67+
#dr = radius[indx] - radius[indx - 1]
68+
dr = np.diff(radius) # differences between consecutive radii
69+
70+
# Midpoint mass contribution terms
71+
mass_contrib = 2.0 * np.pi * (
72+
radius[:-1]**2 * density[:-1] + radius[1:]**2 * density[1:]
73+
) * dr
74+
75+
pwvals_contrib = 2.0 * np.pi * (
76+
radius[:-1] * density[:-1] + radius[1:] * density[1:]
77+
) * dr
78+
79+
# Now cumulative sum to get the arrays
80+
mass = np.concatenate(([1e-15], 1e-15 + np.cumsum(mass_contrib)))
81+
pwvals = np.concatenate(([0.0], np.cumsum(pwvals_contrib)))
5182

52-
# evaluate potential (see theory document)
53-
pvals = -mvals/(rvals+1.e-10) - (pwvals[dvals.size-1] - pwvals)
83+
potential = -mass / (radius + 1.e-10) - (pwvals[-1] - pwvals)
5484

55-
# get the maximum mass and maximum radius
56-
M0 = mvals[dvals.size-1]
57-
R0 = rvals[dvals.size-1]
85+
M0 = mass[-1]
86+
R0 = radius[-1]
87+
88+
Beta = (Mtotal / M0) * (R0 / Rmax)
89+
Gamma = np.sqrt((M0 * R0) / (Mtotal * Rmax)) * (R0 / Rmax)
5890

59-
# compute scaling factors
60-
Beta = (M/M0) * (R0/R);
61-
Gamma = np.sqrt((M0*R0)/(M*R)) * (R0/R);
6291
if verbose:
63-
print("! Scaling: R=",R," M=",M)
92+
print(f"! Scaling: R = {Rmax} M = {Mtotal}")
6493

65-
rfac = np.power(Beta,-0.25) * np.power(Gamma,-0.5);
66-
dfac = np.power(Beta,1.5) * Gamma;
67-
mfac = np.power(Beta,0.75) * np.power(Gamma,-0.5);
68-
pfac = Beta;
94+
rfac = Beta**-0.25 * Gamma**-0.5
95+
dfac = Beta**1.5 * Gamma
96+
mfac = Beta**0.75 * Gamma**-0.5
97+
pfac = Beta
6998

70-
if verbose:
71-
print(rfac,dfac,mfac,pfac)
99+
if physical_units == True:
100+
rfac=1
101+
dfac=1
102+
mfac=1
103+
pfac=1
72104

73-
# save file if desired
74-
if pfile != '':
75-
f = open(pfile,'w')
76-
print('! ',plabel,file=f)
77-
print('! R D M P',file=f)
105+
if verbose:
106+
print(f"Scaling factors: rfac = {rfac}, dfac = {dfac}, mfac = {mfac}, pfac = {pfac}")
78107

79-
print(rvals.size,file=f)
108+
if output_filename:
109+
_write_table(
110+
output_filename,
111+
radius * rfac,
112+
density * dfac,
113+
mass * mfac,
114+
potential * pfac
115+
)
80116

81-
for indx in range(0,rvals.size):
82-
print('{0} {1} {2} {3}'.format( rfac*rvals[indx],\
83-
dfac*dvals[indx],\
84-
mfac*mvals[indx],\
85-
pfac*pvals[indx]),file=f)
86-
87-
f.close()
88-
89-
return rvals*rfac,dfac*dvals,mfac*mvals,pfac*pvals
117+
return radius * rfac, density * dfac, mass * mfac, potential * pfac
90118

tests/test_basis_utils.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,57 @@
11
import numpy as np
2+
import tempfile
3+
import os
24
from EXPtools import basis_builder
35

4-
# test integration between functions!
6+
def test_write_table():
7+
# Prepare dummy data
8+
rvals = np.array([1.0, 2.0, 3.0])
9+
dvals = np.array([10.0, 20.0, 30.0])
10+
mvals = np.array([0.1, 0.2, 0.3])
11+
pvals = np.array([-1.0, -2.0, -3.0])
12+
13+
# Create a temporary file
14+
with tempfile.NamedTemporaryFile(mode='r+', delete=False) as tmp:
15+
filename = tmp.name
16+
_write_table(filename, rvals, dvals, mvals, pvals)
17+
18+
# Read back the file and check contents
19+
with open(filename, 'r') as f:
20+
lines = f.readlines()
21+
22+
assert lines[0].startswith('!') # Header line
23+
assert lines[1].strip() == '! R D M P' # Column description
24+
assert lines[2].strip() == '3' # Number of entries
25+
assert len(lines[3:]) == 3 # 3 data lines
26+
27+
# Clean up
28+
os.remove(filename)
29+
30+
def test_makemodel():
31+
# Create simple synthetic density profile
32+
radius = np.logspace(-1, 1, 5) # 5 points from 0.1 to 10
33+
density = 1.0 / radius**2 # rho ~ r^-2 (common in astrophysics)
34+
35+
Mtotal = 10.0 # desired total mass
36+
37+
# Run model generation
38+
r_scaled, d_scaled, m_scaled, p_scaled = makemodel(radius, density, Mtotal, verbose=False)
39+
40+
# Assertions
41+
assert r_scaled.shape == radius.shape
42+
assert d_scaled.shape == density.shape
43+
assert m_scaled.shape == radius.shape
44+
assert p_scaled.shape == radius.shape
45+
46+
assert np.all(np.isfinite(r_scaled))
47+
assert np.all(np.isfinite(d_scaled))
48+
assert np.all(np.isfinite(m_scaled))
49+
assert np.all(np.isfinite(p_scaled))
50+
51+
assert np.all(r_scaled > 0)
52+
assert np.all(d_scaled > 0)
53+
assert np.all(m_scaled > 0)
54+
55+
# Check mass conservation approximately
56+
np.testing.assert_allclose(m_scaled[-1], Mtotal, rtol=1e-2)
57+

0 commit comments

Comments
 (0)