Skip to content

Commit bc48cd6

Browse files
committed
Initial import of cluster simulation code
1 parent 99553c8 commit bc48cd6

17 files changed

+1426
-17
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*.png
44
.vscode/
55
*.code-workspace
6+
data/
67

78
## GITHUB PYTHON .gitignore FILE
89
# Byte-compiled / optimized / DLL files
@@ -92,7 +93,7 @@ ipython_config.py
9293
# pyenv
9394
# For a library or package, you might want to ignore these files since the code is
9495
# intended to run in multiple environments; otherwise, check them in:
95-
# .python-version
96+
.python-version
9697

9798
# pipenv
9899
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

docs/CNAME

Lines changed: 0 additions & 1 deletion
This file was deleted.

ocelot_data.zip

160 Bytes
Binary file not shown.

pyproject.toml

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
[project]
2-
name = "ocelot" # Required
3-
version = "0.3.1-alpha" # Required
4-
description = "A toolbox for working with observations of star clusters." # Optional
2+
name = "ocelot" # Required
3+
version = "0.3.1-alpha" # Required
4+
description = "A toolbox for working with observations of star clusters." # Optional
55
readme = "README.md" # Optional
66
requires-python = ">=3.8"
7-
license = {file = "LICENSE"}
8-
keywords = ["astronomy", "star cluster", "threading", "development"] # Optional
7+
license = { file = "LICENSE" }
8+
keywords = ["astronomy", "star cluster", "threading", "development"] # Optional
99
authors = [
10-
{name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" } # Optional
10+
{ name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" }, # Optional
1111
]
1212
maintainers = [
13-
{name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" } # Optional
13+
{ name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" }, # Optional
1414
]
15-
classifiers = [ # Optional
15+
classifiers = [ # Optional
1616
"Development Status :: 3 - Alpha",
1717
"Intended Audience :: Developers",
1818
"License :: OSI Approved :: MIT License",
@@ -26,21 +26,36 @@ classifiers = [ # Optional
2626
]
2727
dependencies = [
2828
"numpy>1.21.0,<3.0",
29+
"numba<1.0",
2930
"matplotlib>3.4.0,<4.0",
3031
"scikit-learn>0.24.0,<2.0",
3132
"scipy>1.0.0,<2.0",
3233
"pandas>1.0.0,<3.0",
34+
"pyarrow",
3335
"astropy>4.0.0,<7.0",
3436
"healpy>1.13.0,<2.0",
37+
"galpy>1.9,<2.0",
38+
"imf @ git+https://github.com/keflavich/imf",
39+
"dustmaps",
3540
]
3641

3742

3843
[project.optional-dependencies]
39-
dev = ["check-manifest", "pytest", "mkdocs-material[imaging]", "mkdocstrings[python]>=0.18"]
40-
docs = ["mkdocs-material[imaging]", "mkdocstrings[python]>=0.18"] # For building docs on GitHub
44+
dev = [
45+
"check-manifest",
46+
"pytest",
47+
"mkdocs-material[imaging]",
48+
"mkdocstrings[python]>=0.18",
49+
"ruff",
50+
"ezpadova @ git+https://github.com/mfouesneau/ezpadova",
51+
]
52+
docs = [
53+
"mkdocs-material[imaging]",
54+
"mkdocstrings[python]>=0.18",
55+
] # For building docs on GitHub
4156
test = ["pytest"]
4257

43-
[project.urls] # Optional
58+
[project.urls] # Optional
4459
"Homepage" = "https://ocelot-docs.org"
4560
"Bug Reports" = "https://github.com/emilyhunt/ocelot/issues"
4661
"Source" = "https://github.com/emilyhunt/ocelot"
@@ -50,9 +65,7 @@ test = ["pytest"]
5065

5166
[tool.pytest.ini_options]
5267
pythonpath = "src"
53-
addopts = [
54-
"--import-mode=importlib",
55-
]
68+
addopts = ["--import-mode=importlib"]
5669

5770
[build-system]
5871
requires = ["setuptools>=43.0.0", "wheel"]

scripts/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Scripts
2+
3+
The following scripts are intended for various development purposes.

scripts/download_parsec_isochrones.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Script to download isochrones from PARSEC with ezpadova. Intended for development
2+
use only. Downloads to ../data/.... .
3+
"""
4+
5+
import ezpadova
6+
import pandas as pd
7+
import numpy as np
8+
import time
9+
from pathlib import Path
10+
11+
12+
outdir = Path("../data/isochrones/PARSEC_v1.2S")
13+
outdir.mkdir(exist_ok=True, parents=True)
14+
15+
minimum_age = 5.0
16+
break_point = 8.0
17+
maximum_age = 10.15
18+
age_spacing = 0.01
19+
20+
minimum_metallicity = -2.2
21+
maximum_metallicity = 0.5
22+
metallicity_spacing = 0.05
23+
24+
photometric_system = "YBC_tab_mag_odfnew/tab_mag_gaiaEDR3.dat"
25+
26+
27+
metallicities = np.linspace(
28+
minimum_metallicity,
29+
maximum_metallicity,
30+
num=int((maximum_metallicity - minimum_metallicity) / metallicity_spacing + 1),
31+
)
32+
for a_metallicity in metallicities:
33+
print(f"Working on [M/H]={a_metallicity:.3f}")
34+
# Download in two batches to get around size limits
35+
isochrones_first_half = ezpadova.get_isochrones(
36+
logage=(minimum_age, break_point, age_spacing),
37+
MH=(a_metallicity, a_metallicity, 0.0),
38+
photsys_file=photometric_system,
39+
# track_parsec="parsec_CAF09_v1.2S",
40+
# track_colibri="parsec_CAF09_v1.2S_NOV13",
41+
)
42+
isochrones_second_half = ezpadova.get_isochrones(
43+
logage=(break_point + age_spacing, maximum_age, age_spacing),
44+
MH=(a_metallicity, a_metallicity, 0.0),
45+
photsys_file=photometric_system,
46+
# track_parsec="parsec_CAF09_v1.2S",
47+
# track_colibri="parsec_CAF09_v1.2S_NOV13",
48+
)
49+
50+
isochrones = pd.concat(
51+
[isochrones_first_half, isochrones_second_half], ignore_index=True
52+
)
53+
isochrones.to_parquet(outdir / f"mh={a_metallicity:.3f}.parquet")
54+
55+
print("Sleeping to avoid rate limit...")
56+
time.sleep(5)

src/ocelot/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
__version__ = '0.4.0'
1+
import importlib.util
2+
from pathlib import Path
3+
4+
__version__ = "0.4.0"
5+
6+
# Set a library path, helping other modules to find the data directory etc.
7+
_module_path = Path(importlib.util.find_spec("ocelot").origin).parent
8+
_data_path = _module_path / "data"
Binary file not shown.

src/ocelot/simulate/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""A package for simulating star clusters."""
2+
from .cluster import Cluster, ClusterParameters # noqa: F401

src/ocelot/simulate/astrometry.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Functions for dealing with astrometry-related things."""
2+
3+
from __future__ import annotations # Necessary to type hint without cyclic import
4+
import numpy as np
5+
from ocelot.simulate.uncertainties import apply_gaia_astrometric_uncertainties
6+
from ocelot.calculate.profile import sample_1d_king_profile
7+
from ocelot.util.random import points_on_sphere
8+
import ocelot.simulate.cluster
9+
from astropy.coordinates import SkyCoord, CartesianRepresentation, CartesianDifferential
10+
from astropy import units as u
11+
from scipy.stats import multivariate_normal
12+
13+
14+
def generate_star_positions(cluster: ocelot.simulate.cluster.Cluster):
15+
"""Generates positions of member stars in polar coordinates relative to the center
16+
of the cluster.
17+
"""
18+
radii = sample_1d_king_profile(
19+
cluster.parameters.r_core,
20+
cluster.parameters.r_tidal,
21+
cluster.stars,
22+
seed=cluster.random_seed,
23+
)
24+
phis, thetas = points_on_sphere(
25+
len(radii), phi_symmetric=False, seed=cluster.random_seed
26+
)
27+
x_values, y_values, z_values = spherical_to_cartesian(radii, thetas, phis)
28+
return CartesianRepresentation(x_values, y_values, z_values, unit=u.pc)
29+
30+
31+
def spherical_to_cartesian(radii, thetas, phis):
32+
"""Converts from spherical to Cartesian coordinates. See:
33+
https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
34+
35+
Assumes radii ∈ [0, inf), thetas ∈ [0, pi], phis ∈ [0, 2pi)
36+
"""
37+
x_values = radii * np.sin(thetas) * np.cos(phis)
38+
y_values = radii * np.sin(thetas) * np.sin(phis)
39+
z_values = radii * np.cos(thetas)
40+
return x_values, y_values, z_values
41+
42+
43+
def generate_star_velocities(cluster: ocelot.simulate.cluster.Cluster):
44+
"""Generates the velocities of stars in a cluster."""
45+
distribution = multivariate_normal(
46+
mean=np.zeros(3),
47+
cov=np.identity(3) * cluster.parameters.velocity_dispersion_1d,
48+
seed=cluster.random_generator,
49+
)
50+
v_x, v_y, v_z = distribution.rvs(cluster.stars).T.reshape(3, -1) # We also reshape to make sure a size-1 cluster is handled correctly
51+
return CartesianDifferential(d_x=v_x, d_y=v_y, d_z=v_z, unit=u.m / u.s)
52+
53+
54+
def generate_true_star_astrometry(cluster: ocelot.simulate.cluster.Cluster):
55+
"""Generates the true values of cluster astrometry (not affected by errors)."""
56+
positions = generate_star_positions(cluster)
57+
velocities = generate_star_velocities(cluster)
58+
59+
# Do coordinate frame stuff to get final values (dont look astropy devs, dont tell
60+
# me what I can and cant do, i live in a lawless realm, this works so it works)
61+
cluster_center = cluster.parameters.get_position_as_skycoord(
62+
frame="galactocentric", with_zeroed_proper_motions=True
63+
).cartesian
64+
cluster_differential = cluster_center.differentials["s"]
65+
66+
final_positions = positions + cluster_center
67+
final_velocities = velocities + cluster_differential
68+
69+
final_coords = SkyCoord(
70+
CartesianRepresentation(final_positions, differentials=final_velocities),
71+
frame="galactocentric",
72+
).transform_to("icrs")
73+
final_coords_galactic = final_coords.transform_to("galactic")
74+
75+
# Assign these values to cluster df
76+
cluster.cluster["ra"] = final_coords.ra.value
77+
cluster.cluster["dec"] = final_coords.dec.value
78+
cluster.cluster["l"] = final_coords_galactic.l.value
79+
cluster.cluster["b"] = final_coords_galactic.b.value
80+
cluster.cluster["pmra"] = np.nan # Gets set later
81+
cluster.cluster["pmdec"] = np.nan # Gets set later
82+
cluster.cluster["pmra_true"] = final_coords.pm_ra_cosdec.value
83+
cluster.cluster["pmdec_true"] = final_coords.pm_dec.value
84+
cluster.cluster["radial_velocity_true"] = final_coords.radial_velocity.value
85+
cluster.cluster["parallax_true"] = 1000 / final_coords.distance.value
86+
87+
88+
def generate_cluster_astrometry(cluster: ocelot.simulate.cluster.Cluster):
89+
"""Generates the astrometry of clusters."""
90+
generate_true_star_astrometry(cluster)
91+
apply_gaia_astrometric_uncertainties(cluster)

0 commit comments

Comments
 (0)