ocelot 0.3.1a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. ocelot-0.3.1a0/LICENSE +21 -0
  2. ocelot-0.3.1a0/PKG-INFO +95 -0
  3. ocelot-0.3.1a0/README.md +32 -0
  4. ocelot-0.3.1a0/pyproject.toml +62 -0
  5. ocelot-0.3.1a0/setup.cfg +4 -0
  6. ocelot-0.3.1a0/src/ocelot/__init__.py +1 -0
  7. ocelot-0.3.1a0/src/ocelot/calculate/__init__.py +1 -0
  8. ocelot-0.3.1a0/src/ocelot/calculate/calculate.py +207 -0
  9. ocelot-0.3.1a0/src/ocelot/calculate/distance.py +1 -0
  10. ocelot-0.3.1a0/src/ocelot/calculate/generic.py +97 -0
  11. ocelot-0.3.1a0/src/ocelot/calculate/motion.py +11 -0
  12. ocelot-0.3.1a0/src/ocelot/calculate/position.py +64 -0
  13. ocelot-0.3.1a0/src/ocelot/calculate/profile.py +225 -0
  14. ocelot-0.3.1a0/src/ocelot/cluster/__init__.py +11 -0
  15. ocelot-0.3.1a0/src/ocelot/cluster/epsilon.py +726 -0
  16. ocelot-0.3.1a0/src/ocelot/cluster/nearest_neighbor.py +66 -0
  17. ocelot-0.3.1a0/src/ocelot/cluster/preprocess.py +370 -0
  18. ocelot-0.3.1a0/src/ocelot/cluster/resample.py +192 -0
  19. ocelot-0.3.1a0/src/ocelot/crossmatch/__init__.py +7 -0
  20. ocelot-0.3.1a0/src/ocelot/crossmatch/_catalogue.py +467 -0
  21. ocelot-0.3.1a0/src/ocelot/isochrone/__init__.py +4 -0
  22. ocelot-0.3.1a0/src/ocelot/isochrone/interpolate.py +588 -0
  23. ocelot-0.3.1a0/src/ocelot/isochrone/io.py +148 -0
  24. ocelot-0.3.1a0/src/ocelot/plot/__init__.py +13 -0
  25. ocelot-0.3.1a0/src/ocelot/plot/axis/__init__.py +1 -0
  26. ocelot-0.3.1a0/src/ocelot/plot/axis/cluster.py +468 -0
  27. ocelot-0.3.1a0/src/ocelot/plot/axis/nn_statistics.py +184 -0
  28. ocelot-0.3.1a0/src/ocelot/plot/gaia_explorer.py +436 -0
  29. ocelot-0.3.1a0/src/ocelot/plot/plot_figure.py +391 -0
  30. ocelot-0.3.1a0/src/ocelot/plot/process.py +73 -0
  31. ocelot-0.3.1a0/src/ocelot/plot/utilities.py +168 -0
  32. ocelot-0.3.1a0/src/ocelot/util/__init__.py +0 -0
  33. ocelot-0.3.1a0/src/ocelot/util/check.py +15 -0
  34. ocelot-0.3.1a0/src/ocelot/util/random.py +37 -0
  35. ocelot-0.3.1a0/src/ocelot/verify/__init__.py +2 -0
  36. ocelot-0.3.1a0/src/ocelot/verify/find.py +269 -0
  37. ocelot-0.3.1a0/src/ocelot/verify/significance.py +396 -0
  38. ocelot-0.3.1a0/src/ocelot/verify/stats.py +261 -0
  39. ocelot-0.3.1a0/src/ocelot.egg-info/PKG-INFO +95 -0
  40. ocelot-0.3.1a0/src/ocelot.egg-info/SOURCES.txt +47 -0
  41. ocelot-0.3.1a0/src/ocelot.egg-info/dependency_links.txt +1 -0
  42. ocelot-0.3.1a0/src/ocelot.egg-info/requires.txt +20 -0
  43. ocelot-0.3.1a0/src/ocelot.egg-info/top_level.txt +1 -0
  44. ocelot-0.3.1a0/tests/test_calculate.py +252 -0
  45. ocelot-0.3.1a0/tests/test_cluster.py +695 -0
  46. ocelot-0.3.1a0/tests/test_crossmatch.py +107 -0
  47. ocelot-0.3.1a0/tests/test_isochrone.py +278 -0
  48. ocelot-0.3.1a0/tests/test_plot.py +244 -0
  49. ocelot-0.3.1a0/tests/test_verify.py +68 -0
ocelot-0.3.1a0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Emily Hunt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.1
2
+ Name: ocelot
3
+ Version: 0.3.1a0
4
+ Summary: A toolbox for working with observations of star clusters.
5
+ Author-email: Emily Hunt <emily.hunt.physics@gmail.com>
6
+ Maintainer-email: Emily Hunt <emily.hunt.physics@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2023 Emily Hunt
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://ocelot-docs.org
30
+ Project-URL: Bug Reports, https://github.com/emilyhunt/ocelot/issues
31
+ Project-URL: Source, https://github.com/emilyhunt/ocelot
32
+ Keywords: astronomy,star cluster,threading,development
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.8
38
+ Classifier: Programming Language :: Python :: 3.9
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: numpy<3.0,>1.21.0
47
+ Requires-Dist: matplotlib<4.0,>3.4.0
48
+ Requires-Dist: scikit-learn<2.0,>0.24.0
49
+ Requires-Dist: scipy<2.0,>1.0.0
50
+ Requires-Dist: pandas<3.0,>1.0.0
51
+ Requires-Dist: astropy<7.0,>4.0.0
52
+ Requires-Dist: healpy<2.0,>1.13.0
53
+ Provides-Extra: dev
54
+ Requires-Dist: check-manifest; extra == "dev"
55
+ Requires-Dist: pytest; extra == "dev"
56
+ Requires-Dist: mkdocs-material[imaging]; extra == "dev"
57
+ Requires-Dist: mkdocstrings[python]>=0.18; extra == "dev"
58
+ Provides-Extra: docs
59
+ Requires-Dist: mkdocs-material[imaging]; extra == "docs"
60
+ Requires-Dist: mkdocstrings[python]>=0.18; extra == "docs"
61
+ Provides-Extra: test
62
+ Requires-Dist: pytest; extra == "test"
63
+
64
+ [![docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://ocelot-docs.org)
65
+ [![Build Docs](https://github.com/emilyhunt/ocelot/actions/workflows/build-docs.yml/badge.svg)](https://ocelot-docs.org)
66
+
67
+ # ocelot
68
+
69
+ A toolbox for working with observations of star clusters.
70
+
71
+ In the [long-running tradition](https://arxiv.org/abs/1903.12180) of astronomy software, `ocelot` is _not_ a good acronym for this project. It's the **O**pen-source star **C**lust**E**r mu**L**ti-purp**O**se **T**oolkit. (We hope the results you get from this package are better than this acronym)
72
+
73
+ ## Current package status
74
+
75
+ ⚠️ ocelot is currently in **alpha** and is in active development. **Expect breaking API changes** ⚠️
76
+
77
+ For the time being, `ocelot` is a collection of code that [emilyhunt](https://github.com/emilyhunt) wrote during her PhD, but the eventual goal will be to make a package usable by the entire star cluster community. If you'd like to see a feature added, then please consider opening an issue and proposing it!
78
+
79
+ ## Installation
80
+
81
+ Install from PyPI with:
82
+
83
+ ```
84
+ pip install ocelot
85
+ ```
86
+
87
+ ## Development
88
+
89
+ If you'd like to contribute to the package, we recommend setting up a new virtual environment of your choice. Then, you can install the latest commit on the main branch in edit mode (`-e`) with all development dependencies (`[dev]`) with:
90
+
91
+ ```
92
+ pip install -e git+https://github.com/emilyhunt/ocelot[dev]
93
+ ```
94
+
95
+ After installing development dependencies, you can also make and view edits to the package's documentation. To view a local copy of the documentation, do `mkdocs serve`. You can do a test build with `mkdocs build`.
@@ -0,0 +1,32 @@
1
+ [![docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://ocelot-docs.org)
2
+ [![Build Docs](https://github.com/emilyhunt/ocelot/actions/workflows/build-docs.yml/badge.svg)](https://ocelot-docs.org)
3
+
4
+ # ocelot
5
+
6
+ A toolbox for working with observations of star clusters.
7
+
8
+ In the [long-running tradition](https://arxiv.org/abs/1903.12180) of astronomy software, `ocelot` is _not_ a good acronym for this project. It's the **O**pen-source star **C**lust**E**r mu**L**ti-purp**O**se **T**oolkit. (We hope the results you get from this package are better than this acronym)
9
+
10
+ ## Current package status
11
+
12
+ ⚠️ ocelot is currently in **alpha** and is in active development. **Expect breaking API changes** ⚠️
13
+
14
+ For the time being, `ocelot` is a collection of code that [emilyhunt](https://github.com/emilyhunt) wrote during her PhD, but the eventual goal will be to make a package usable by the entire star cluster community. If you'd like to see a feature added, then please consider opening an issue and proposing it!
15
+
16
+ ## Installation
17
+
18
+ Install from PyPI with:
19
+
20
+ ```
21
+ pip install ocelot
22
+ ```
23
+
24
+ ## Development
25
+
26
+ If you'd like to contribute to the package, we recommend setting up a new virtual environment of your choice. Then, you can install the latest commit on the main branch in edit mode (`-e`) with all development dependencies (`[dev]`) with:
27
+
28
+ ```
29
+ pip install -e git+https://github.com/emilyhunt/ocelot[dev]
30
+ ```
31
+
32
+ After installing development dependencies, you can also make and view edits to the package's documentation. To view a local copy of the documentation, do `mkdocs serve`. You can do a test build with `mkdocs build`.
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "ocelot" # Required
3
+ version = "0.3.1-alpha" # Required
4
+ description = "A toolbox for working with observations of star clusters." # Optional
5
+ readme = "README.md" # Optional
6
+ requires-python = ">=3.8"
7
+ license = {file = "LICENSE"}
8
+ keywords = ["astronomy", "star cluster", "threading", "development"] # Optional
9
+ authors = [
10
+ {name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" } # Optional
11
+ ]
12
+ maintainers = [
13
+ {name = "Emily Hunt", email = "emily.hunt.physics@gmail.com" } # Optional
14
+ ]
15
+ classifiers = [ # Optional
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ ]
27
+ dependencies = [
28
+ "numpy>1.21.0,<3.0",
29
+ "matplotlib>3.4.0,<4.0",
30
+ "scikit-learn>0.24.0,<2.0",
31
+ "scipy>1.0.0,<2.0",
32
+ "pandas>1.0.0,<3.0",
33
+ "astropy>4.0.0,<7.0",
34
+ "healpy>1.13.0,<2.0",
35
+ ]
36
+
37
+
38
+ [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
41
+ test = ["pytest"]
42
+
43
+ [project.urls] # Optional
44
+ "Homepage" = "https://ocelot-docs.org"
45
+ "Bug Reports" = "https://github.com/emilyhunt/ocelot/issues"
46
+ "Source" = "https://github.com/emilyhunt/ocelot"
47
+
48
+ # [tool.setuptools]
49
+ # package-data = {"sample" = ["*.dat"]}
50
+
51
+ [tool.pytest.ini_options]
52
+ pythonpath = "src"
53
+ addopts = [
54
+ "--import-mode=importlib",
55
+ ]
56
+
57
+ [build-system]
58
+ requires = ["setuptools>=43.0.0", "wheel"]
59
+ build-backend = "setuptools.build_meta"
60
+
61
+ [tool.ruff]
62
+ line-length = 88
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = '0.4.0'
@@ -0,0 +1 @@
1
+ # Todo decide what to make top-level here
@@ -0,0 +1,207 @@
1
+ """A set of functions for calculating typical cluster parameters.
2
+
3
+ Todo: error treatment here could be made more bayesian
4
+ """
5
+
6
+ from typing import Optional
7
+ from astropy.coordinates import SkyCoord
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from .constants import mas_per_yr_to_rad_per_s, pc_to_m, deg_to_rad
13
+
14
+
15
+ def _handle_ra_discontinuity(ra_data, middle_ras_raise_error=True):
16
+ """Tries to detect when the ras in a field cross the (0, 360) ra discontinuity and returns corrected results. Will
17
+ raise an error if ras are all over the place (which will happen e.g. at very high declinations) in which you
18
+ ought to instead switch to a method free of spherical distortions.
19
+
20
+ Args:
21
+ ra_data (pd.Series or np.ndarray): data on ras.
22
+ middle_ras_raise_error (bool): whether or not a cluster having right ascensions in all ranges [0, 90), [90, 270]
23
+ and (270, 360] raises an error. The error here indicates that this cluster has extreme spherical
24
+ discontinuities (e.g. it's near a coordinate pole) and that the mean ra and mean dec will be inaccurate.
25
+ Default: True
26
+
27
+ Returns:
28
+ ra_data but corrected for distortions. If values are both <90 and >270, the new ra data will be in the range
29
+ (-90, 90).
30
+
31
+ """
32
+ # Firstly, check that the ras are valid ras
33
+ if np.any(ra_data >= 360) or np.any(ra_data < 0):
34
+ raise ValueError(
35
+ "at least one input ra value was invalid! Ras must be in the range [0, 360)."
36
+ )
37
+
38
+ # Next, grab all the locations of dodgy friends and check that all three aren't ever in use at the same time
39
+ low_ra = ra_data < 90
40
+ high_ra = ra_data > 270
41
+ middle_ra = np.logical_not(np.logical_or(low_ra, high_ra))
42
+
43
+ # Proceed if we have both low and high ras
44
+ if np.any(low_ra) and np.any(high_ra):
45
+ # Stop if we have middle too (would imply stars everywhere or an extreme dec value)
46
+ if np.any(middle_ra) and middle_ras_raise_error:
47
+ raise ValueError(
48
+ "ra values are in all three ranges: [0, 90), [90, 270] and (270, 360). This cluster can't "
49
+ "be processed by this function! Spherical distortions must be removed first."
50
+ )
51
+
52
+ # Otherwise, apply the discontinuity removal
53
+ else:
54
+ # Make a copy so nothing weird happens
55
+ ra_data = ra_data.copy()
56
+
57
+ # And remove the distortion for all high numbers
58
+ ra_data[high_ra] = ra_data[high_ra] - 360
59
+
60
+ return ra_data
61
+
62
+
63
+ def mean_radius(
64
+ data_gaia: pd.DataFrame,
65
+ membership_probabilities: Optional[np.ndarray] = None,
66
+ already_inferred_parameters: Optional[dict] = None,
67
+ key_ra: str = "ra",
68
+ key_ra_error: str = "ra_error",
69
+ key_dec: str = "dec",
70
+ key_dec_error: str = "dec_error",
71
+ distance_to_use: str = "inverse_parallax",
72
+ middle_ras_raise_error: bool = True,
73
+ **kwargs,
74
+ ):
75
+ """Produces various radius statistics on a given cluster, finding its sky location and three radii: the core, tidal
76
+ and 50% radius.
77
+
78
+ Done in a very basic, frequentist way, whereby means are weighted based on the membership probabilities (if
79
+ specified).
80
+
81
+ N.B. unlike the above functions, errors do *not change the mean* as this would potentially bias the
82
+ estimates towards being dominated by large, centrally-located stars within clusters (that have generally lower
83
+ velocities.) Hence, estimates here will be less precise but hopefully more accurate.
84
+
85
+ Todo: add error estimation to this function (hard)
86
+
87
+ Todo: add galactic l, b to the output of this function
88
+
89
+ Args:
90
+ data_gaia (pd.DataFrame): Gaia data for the cluster in the standard format (e.g. as in DR2.)
91
+ membership_probabilities (optional, np.ndarray): membership probabilities for the cluster. When specified,
92
+ they can increase or decrease the effect of certain stars on the mean.
93
+ already_inferred_parameters (optional, dict): a parameter dictionary of the mean distance and proper motion.
94
+ Otherwise, this function calculates a version.
95
+ key_ra (str): Gaia parameter name.
96
+ key_ra_error (str): Gaia parameter name.
97
+ key_dec (str): Gaia parameter name.
98
+ key_dec_error (str): Gaia parameter name.
99
+ distance_to_use (str): which already inferred distance to use to convert angular radii to parsecs.
100
+ Default: "inverse_parallax"
101
+ middle_ras_raise_error (bool): whether or not a cluster having right ascensions in all ranges [0, 90), [90, 270]
102
+ and (270, 360] raises an error. The error here indicates that this cluster has extreme spherical
103
+ discontinuities (e.g. it's near a coordinate pole) and that the mean ra and mean dec will be inaccurate.
104
+ Default: True
105
+
106
+ Returns:
107
+ a dict, formatted with:
108
+ {
109
+ # Position
110
+ "ra": ra of the cluster
111
+ "ra_error": error on the above
112
+ "dec": dec of the cluster
113
+ "dec_error": error on the above
114
+
115
+ # Angular radii
116
+ "ang_radius_50": median ang. distance from the center, i.e.angular radius of the cluster with 50% of members
117
+ "ang_radius_50_error": error on the above
118
+ "ang_radius_c": angular King's core radius of the cluster
119
+ "ang_radius_c_error": error on the above
120
+ "ang_radius_t": maximum angular distance from the center, i.e. angular King's tidal radius of the cluster
121
+ "ang_radius_t_error": error on the above
122
+
123
+ # Parsec radii
124
+ "radius_50": median distance from the center, i.e.radius of the cluster with 50% of members
125
+ "radius_50_error": error on the above
126
+ "radius_c": King's core radius of the cluster
127
+ "radius_c_error": error on the above
128
+ "radius_t": maximum distance from the center, i.e. King's tidal radius of the cluster
129
+ "radius_t_error": error on the above
130
+ }
131
+
132
+ """
133
+ inferred_parameters = {}
134
+ sqrt_n_stars = np.sqrt(data_gaia.shape[0])
135
+
136
+ # Grab the distances if they aren't specified - we'll need them in a moment!
137
+ if already_inferred_parameters is None:
138
+ already_inferred_parameters = mean_distance(data_gaia, membership_probabilities)
139
+
140
+ # Estimate the ra, dec of the cluster as the weighted mean
141
+ ra_data = _handle_ra_discontinuity(
142
+ data_gaia[key_ra], middle_ras_raise_error=middle_ras_raise_error
143
+ )
144
+
145
+ inferred_parameters["ra"] = np.average(ra_data, weights=membership_probabilities)
146
+ inferred_parameters["ra_std"] = _weighted_standard_deviation(
147
+ ra_data, membership_probabilities
148
+ )
149
+ inferred_parameters["ra_error"] = inferred_parameters["ra_std"] / sqrt_n_stars
150
+
151
+ if inferred_parameters["ra"] < 0:
152
+ inferred_parameters["ra"] += 360
153
+
154
+ inferred_parameters["dec"] = np.average(
155
+ data_gaia[key_dec], weights=membership_probabilities
156
+ )
157
+ inferred_parameters["dec_std"] = _weighted_standard_deviation(
158
+ data_gaia[key_dec], membership_probabilities
159
+ )
160
+ inferred_parameters["dec_error"] = inferred_parameters["dec_std"] / sqrt_n_stars
161
+
162
+ inferred_parameters["ang_dispersion"] = np.sqrt(
163
+ inferred_parameters["ra_std"] ** 2 + inferred_parameters["dec_std"] ** 2
164
+ )
165
+
166
+ # Calculate how far every star in the cluster is from the central point
167
+ center_skycoord = SkyCoord(
168
+ ra=inferred_parameters["ra"], dec=inferred_parameters["dec"], unit="deg"
169
+ )
170
+ star_skycoords = SkyCoord(
171
+ ra=data_gaia[key_ra].to_numpy(), dec=data_gaia[key_dec].to_numpy(), unit="deg"
172
+ )
173
+
174
+ distances_from_center = center_skycoord.separation(star_skycoords).degree
175
+
176
+ # And say something about the radii in this case
177
+ inferred_parameters["ang_radius_50"] = np.median(distances_from_center)
178
+ inferred_parameters["ang_radius_50_error"] = np.nan
179
+
180
+ inferred_parameters["ang_radius_c"] = np.nan
181
+ inferred_parameters["ang_radius_c_error"] = np.nan
182
+
183
+ inferred_parameters["ang_radius_t"] = np.max(distances_from_center)
184
+ inferred_parameters["ang_radius_t_error"] = np.nan
185
+
186
+ # Convert the angular distances into parsecs
187
+ inferred_parameters["radius_50"] = (
188
+ np.tan(inferred_parameters["ang_radius_50"] * deg_to_rad)
189
+ * already_inferred_parameters[distance_to_use]
190
+ )
191
+ inferred_parameters["radius_50_error"] = np.nan
192
+ inferred_parameters["radius_c"] = np.nan
193
+ inferred_parameters["radius_c_error"] = np.nan
194
+ inferred_parameters["radius_t"] = (
195
+ np.tan(inferred_parameters["ang_radius_t"] * deg_to_rad)
196
+ * already_inferred_parameters[distance_to_use]
197
+ )
198
+ inferred_parameters["radius_t_error"] = np.nan
199
+
200
+ return inferred_parameters
201
+
202
+
203
+ def all_statistics():
204
+ """
205
+ """
206
+ # Todo refactor this (and other high-level calculation methods)
207
+ pass
@@ -0,0 +1 @@
1
+ """Functions for working with cluster distances."""
@@ -0,0 +1,97 @@
1
+ """Generic calculation utilities used across the module."""
2
+ import numpy as np
3
+ from numpy.typing import ArrayLike, NDArray
4
+ from typing import Optional, Union
5
+ from ocelot.util.check import _check_matching_lengths_of_non_nones
6
+ from scipy.stats import directional_stats
7
+
8
+
9
+ def _weighted_standard_deviation(x: ArrayLike, weights: Optional[ArrayLike] = None):
10
+ """Computes weighted standard deviation. Uses method from
11
+ https://stackoverflow.com/a/52655244/12709989.
12
+ """
13
+ # Todo: not sure that this deals with small numbers of points correctly!
14
+ # See: unit test fails when v. few points used
15
+ return np.sqrt(np.cov(x, aweights=weights))
16
+
17
+
18
+ def standard_error(
19
+ standard_deviation: Union[ArrayLike[Union[float, int]], float, int],
20
+ number_of_measurements: Union[ArrayLike[int], int],
21
+ ) -> float:
22
+ """Calculates the standard error on the mean of some parameter given the standard
23
+ deviation.
24
+
25
+ Parameters
26
+ ----------
27
+ standard_deviation : array-like, float, or int
28
+ Standard deviation(s)
29
+ number_of_measurements : array-like of ints, int
30
+ Number of measurements used to find standard deviation.
31
+
32
+ Returns
33
+ -------
34
+ standard_error : float
35
+ """
36
+ return standard_deviation / np.sqrt(number_of_measurements)
37
+
38
+
39
+ def mean_and_deviation(
40
+ values: ArrayLike,
41
+ weights: Optional[ArrayLike] = None,
42
+ ) -> tuple[float]:
43
+ """Calculates the mean and standard deviation of some set of values.
44
+
45
+ Parameters
46
+ ----------
47
+ values : array-like
48
+ Values to calculate mean and standard deviation of.
49
+ weights : array-like, optional
50
+ Array of weights to use to compute a weighted mean and average.
51
+
52
+ Returns
53
+ -------
54
+ mean : float
55
+ Mean of values.
56
+ std : float
57
+ Standard deviation of values.
58
+ """
59
+ _check_matching_lengths_of_non_nones(values, weights)
60
+
61
+ return (
62
+ np.average(values, weights=weights),
63
+ _weighted_standard_deviation(values, weights),
64
+ )
65
+
66
+
67
+ def lonlat_to_unitvec(longitudes: NDArray, latitudes: NDArray):
68
+ """Converts longitudes and latitudes to unit vectors on a unit sphere. Uses method
69
+ at https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates.
70
+ Assumes that latitudes is in the range [-pi / 2, pi / 2] as is common in
71
+ astronomical unit systems.
72
+ """
73
+ x = np.cos(latitudes) * np.cos(longitudes)
74
+ y = np.cos(latitudes) * np.sin(longitudes)
75
+ z = np.sin(latitudes)
76
+ return np.column_stack((x, y, z))
77
+
78
+
79
+ def unitvec_to_lonlat(unit_vectors: NDArray):
80
+ """Converts unit vectors on a unit sphere to longitudes and latitudes. See
81
+ `lonlat_to_unitvec` for more details.
82
+ """
83
+ x, y, z = [column.ravel() for column in np.hsplit(unit_vectors, 3)]
84
+ longitudes = np.arctan2(y, x)
85
+ latitudes = np.arcsin(z / np.sqrt(x**2 + y**2 + z**2))
86
+ return longitudes, latitudes
87
+
88
+
89
+ def spherical_mean(longitudes: ArrayLike, latitudes: ArrayLike):
90
+ """Calculates the spherical mean of angular positions."""
91
+ longitudes = np.asarray_chkfinite(longitudes)
92
+ latitudes = np.asarray_chkfinite(latitudes)
93
+
94
+ unit_vectors = lonlat_to_unitvec(longitudes, latitudes)
95
+ mean_unit_vector = directional_stats(unit_vectors).mean_direction
96
+ mean_lon, mean_lat = unitvec_to_lonlat(mean_unit_vector)
97
+ return mean_lon[0], mean_lat[0]
@@ -0,0 +1,11 @@
1
+ """Tools for calculating values based on proper motions and/or velocities."""
2
+
3
+
4
+ def radial_velocity(velocities, with_frame_correction=False):
5
+ # Todo
6
+ pass
7
+
8
+
9
+ def proper_motion(proper_motions, with_frame_correction=False):
10
+ # Todo
11
+ pass
@@ -0,0 +1,64 @@
1
+ """Different methods for calculating the center of a cluster in spherical coordinates.
2
+ (This is actually oddly difficult, thanks to how spheres work. Damn spheres.)
3
+ """
4
+ import numpy as np
5
+ from numpy.typing import ArrayLike
6
+
7
+ from ocelot.calculate.generic import spherical_mean
8
+
9
+
10
+ def mean_position(
11
+ longitudes: ArrayLike, latitudes: ArrayLike, degrees=True
12
+ ) -> tuple[float]:
13
+ """Calculates the spherical mean of angular positions, specified as longitudes and
14
+ latitudes. This uses directional statistics to do so in a way that is aware of
15
+ discontinuities, such as the fact that 0° = 360°.
16
+
17
+ Parameters
18
+ ----------
19
+ longitudes : array-like
20
+ Array of longitudinal positions of stars in your cluster (e.g. right ascensions
21
+ or galactic longitudes.) Assumed to be in the range [0°, 360°].
22
+ latitudes : array-like
23
+ Array of latitudinal positions of stars in your cluster (e.g. declinations or
24
+ galactic latitudes.) Assumed to be in the range [-90°, 90°].
25
+ degrees : bool
26
+ Whether longitudes and latitudes are in degrees, and whether to return an answer
27
+ in degrees. Defaults to True. If False, longitudes and latitudes are assumed to
28
+ be in radians, with ranges [0, 2π] and [-π/2, π/2] respectively.
29
+
30
+ Returns
31
+ -------
32
+ mean_longitude : float
33
+ mean_latitude : float
34
+
35
+ Notes
36
+ -----
37
+ This function explicitly assumes that your star cluster *has* a well-defined mean
38
+ position. Some configurations (such as points uniformly distributed in at least one
39
+ axis of a sphere) will not have a meaningful mean position.
40
+
41
+ Internally, this function uses `scipy.stats.directional_stats`, with a definition
42
+ taken from [1]. See [2] for more background.
43
+
44
+ References
45
+ ----------
46
+ [1] Mardia, Jupp. (2000). Directional Statistics (p. 163). Wiley.
47
+ [2] https://en.wikipedia.org/wiki/Directional_statistics
48
+ """
49
+ if degrees:
50
+ longitudes, latitudes = np.radians(longitudes), np.radians(latitudes)
51
+ mean_lon, mean_lat = spherical_mean(longitudes, latitudes)
52
+ if degrees:
53
+ return np.degrees(mean_lon), np.degrees(mean_lat)
54
+ return mean_lon, mean_lat
55
+
56
+
57
+ def mode_position():
58
+ """Attempts to find the mode of a star cluster's 2D on-sky distribution. This is a
59
+ better estimator than the mean position for clusters that are assymmetric, which is
60
+ often the case for clusters with assymetric tidal tails (e.g. due to one side being
61
+ more easily detected than the other.)
62
+ """
63
+ # Todo
64
+ pass