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.
- ocelot-0.3.1a0/LICENSE +21 -0
- ocelot-0.3.1a0/PKG-INFO +95 -0
- ocelot-0.3.1a0/README.md +32 -0
- ocelot-0.3.1a0/pyproject.toml +62 -0
- ocelot-0.3.1a0/setup.cfg +4 -0
- ocelot-0.3.1a0/src/ocelot/__init__.py +1 -0
- ocelot-0.3.1a0/src/ocelot/calculate/__init__.py +1 -0
- ocelot-0.3.1a0/src/ocelot/calculate/calculate.py +207 -0
- ocelot-0.3.1a0/src/ocelot/calculate/distance.py +1 -0
- ocelot-0.3.1a0/src/ocelot/calculate/generic.py +97 -0
- ocelot-0.3.1a0/src/ocelot/calculate/motion.py +11 -0
- ocelot-0.3.1a0/src/ocelot/calculate/position.py +64 -0
- ocelot-0.3.1a0/src/ocelot/calculate/profile.py +225 -0
- ocelot-0.3.1a0/src/ocelot/cluster/__init__.py +11 -0
- ocelot-0.3.1a0/src/ocelot/cluster/epsilon.py +726 -0
- ocelot-0.3.1a0/src/ocelot/cluster/nearest_neighbor.py +66 -0
- ocelot-0.3.1a0/src/ocelot/cluster/preprocess.py +370 -0
- ocelot-0.3.1a0/src/ocelot/cluster/resample.py +192 -0
- ocelot-0.3.1a0/src/ocelot/crossmatch/__init__.py +7 -0
- ocelot-0.3.1a0/src/ocelot/crossmatch/_catalogue.py +467 -0
- ocelot-0.3.1a0/src/ocelot/isochrone/__init__.py +4 -0
- ocelot-0.3.1a0/src/ocelot/isochrone/interpolate.py +588 -0
- ocelot-0.3.1a0/src/ocelot/isochrone/io.py +148 -0
- ocelot-0.3.1a0/src/ocelot/plot/__init__.py +13 -0
- ocelot-0.3.1a0/src/ocelot/plot/axis/__init__.py +1 -0
- ocelot-0.3.1a0/src/ocelot/plot/axis/cluster.py +468 -0
- ocelot-0.3.1a0/src/ocelot/plot/axis/nn_statistics.py +184 -0
- ocelot-0.3.1a0/src/ocelot/plot/gaia_explorer.py +436 -0
- ocelot-0.3.1a0/src/ocelot/plot/plot_figure.py +391 -0
- ocelot-0.3.1a0/src/ocelot/plot/process.py +73 -0
- ocelot-0.3.1a0/src/ocelot/plot/utilities.py +168 -0
- ocelot-0.3.1a0/src/ocelot/util/__init__.py +0 -0
- ocelot-0.3.1a0/src/ocelot/util/check.py +15 -0
- ocelot-0.3.1a0/src/ocelot/util/random.py +37 -0
- ocelot-0.3.1a0/src/ocelot/verify/__init__.py +2 -0
- ocelot-0.3.1a0/src/ocelot/verify/find.py +269 -0
- ocelot-0.3.1a0/src/ocelot/verify/significance.py +396 -0
- ocelot-0.3.1a0/src/ocelot/verify/stats.py +261 -0
- ocelot-0.3.1a0/src/ocelot.egg-info/PKG-INFO +95 -0
- ocelot-0.3.1a0/src/ocelot.egg-info/SOURCES.txt +47 -0
- ocelot-0.3.1a0/src/ocelot.egg-info/dependency_links.txt +1 -0
- ocelot-0.3.1a0/src/ocelot.egg-info/requires.txt +20 -0
- ocelot-0.3.1a0/src/ocelot.egg-info/top_level.txt +1 -0
- ocelot-0.3.1a0/tests/test_calculate.py +252 -0
- ocelot-0.3.1a0/tests/test_cluster.py +695 -0
- ocelot-0.3.1a0/tests/test_crossmatch.py +107 -0
- ocelot-0.3.1a0/tests/test_isochrone.py +278 -0
- ocelot-0.3.1a0/tests/test_plot.py +244 -0
- 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.
|
ocelot-0.3.1a0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://ocelot-docs.org)
|
|
65
|
+
[](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`.
|
ocelot-0.3.1a0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[](https://ocelot-docs.org)
|
|
2
|
+
[](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
|
ocelot-0.3.1a0/setup.cfg
ADDED
|
@@ -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,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
|