ocelot 0.3.1a0__py3-none-any.whl

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/__init__.py 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,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
@@ -0,0 +1,225 @@
1
+ # Todo: refactor to new structure (e.g. class-based, mirroring scipy.stats maybe?)
2
+
3
+ from typing import Union, Optional
4
+
5
+ import numpy as np
6
+
7
+
8
+ def king_surface_density(
9
+ r_values: Union[float, np.ndarray],
10
+ r_core: float,
11
+ r_tidal: float,
12
+ normalise: bool = False,
13
+ ):
14
+ """Computes the King surface density (King 1962, equation 14) given the three parameters. Can take vectorised input.
15
+ Will return the surface density per square unit expected at a distance r_values from the core of the cluster.
16
+
17
+ Valid only for:
18
+ 0 <= r < r_tidal (0 elsewhere - this is checked internally)
19
+ 0 < r_core < r_tidal (raises an error if not the case)
20
+
21
+ Args:
22
+ r_values (float or np.ndarray): r values to compute the surface density at.
23
+ r_core (float): the core radius of the cluster.
24
+ r_tidal (float): the tidal radius of the cluster.
25
+ normalise (bool): whether or not to return the normalised King surface density profile, calculated
26
+ numerically.
27
+ Default: False
28
+
29
+ Returns:
30
+ a float or array of floats of the surface density for the cluster.
31
+
32
+ """
33
+ _check_core_and_tidal_radii(r_core, r_tidal)
34
+ r_values = _check_r_values(r_values)
35
+
36
+ # Constants
37
+ rt_rc = r_tidal / r_core
38
+ a = (1 + rt_rc**2) ** -0.5
39
+
40
+ # Compute normalisation constant if desired
41
+ if normalise:
42
+ term_1 = r_core * np.arctan(rt_rc)
43
+ term_2 = -2 * a * r_core * np.log(rt_rc + 1 / a)
44
+ term_3 = r_tidal * a**2
45
+ normalisation_constant = 1 / (term_1 + term_2 + term_3)
46
+ else:
47
+ normalisation_constant = 1.0
48
+
49
+ # Work out where 0 <= r < r_tidal
50
+ r_valid = np.logical_and(r_values >= 0, r_values < r_tidal)
51
+ r_invalid = np.invert(r_valid)
52
+
53
+ # Compute result
54
+ reduced_r_values = (1 + (r_values[r_valid] / r_core) ** 2) ** (-0.5)
55
+ result = np.empty(r_values.shape)
56
+ result[r_valid] = normalisation_constant * (reduced_r_values - a) ** 2
57
+ result[r_invalid] = 0
58
+
59
+ return result
60
+
61
+
62
+ def _check_r_values(r_values):
63
+ # Convert to a np array that's at least 1d, and check that it isn't bad
64
+ r_values = np.atleast_1d(r_values)
65
+ if not np.isfinite(r_values).all():
66
+ raise ValueError("invalid r_values (e.g. nan or inf) are not allowed!")
67
+ if np.any(r_values < 0):
68
+ raise ValueError("all r_values must be positive or zero.")
69
+ return r_values
70
+
71
+
72
+ def _check_core_and_tidal_radii(r_core, r_tidal):
73
+ if not np.isfinite(r_core).all() or not np.isfinite(r_tidal).all():
74
+ raise ValueError("input parameters must be finite!")
75
+ if r_tidal < r_core or r_core < 0 or r_tidal < 0:
76
+ raise ValueError("parameters must satisfy 0 < r_core < r_tidal")
77
+
78
+
79
+ def king_surface_density_fast(r_values: np.ndarray, r_core: float, r_tidal: float):
80
+ """Computes the King surface density (King 1962, equation 14) given the three parameters. Can take vectorised input.
81
+ Fast version intended for use with random sampling - it has no checks and cannot be normalised! Be careful!
82
+
83
+ Valid only for:
84
+ 0 <= r < r_tidal (NOT CHECKED in this function!)
85
+ 0 < r_core < r_tidal (NOT CHECKED in this function!)
86
+
87
+ Args:
88
+ r_values (np.ndarray): r values to compute the surface density at. Must be a numpy array!
89
+ r_core (float): the core radius of the cluster.
90
+ r_tidal (float): the tidal radius of the cluster.
91
+
92
+ Returns:
93
+ a float or array of floats of the surface density for the cluster.
94
+
95
+ """
96
+ # Constants
97
+ rt_rc = r_tidal / r_core
98
+ a = (1 + rt_rc**2) ** -0.5
99
+
100
+ # Compute result
101
+ return ((1 + (r_values / r_core) ** 2) ** (-0.5) - a) ** 2
102
+
103
+
104
+ def sample_2d_king_profile(
105
+ r_core: float,
106
+ r_tidal: float,
107
+ n_samples: int,
108
+ seed=None,
109
+ oversampling_factor: float = 10,
110
+ return_generator: bool = False,
111
+ ):
112
+ """Samples a 2D King profile to return n_samples sample radii.
113
+
114
+ Valid only for:
115
+ 0 < r_core < r_tidal
116
+
117
+ Args:
118
+ r_core (float): the core radius of the cluster.
119
+ r_tidal (float): the tidal radius of the cluster.
120
+ n_samples (int): the number of samples to generate.
121
+ seed (int, optional): the seed of the random number generator. Default: None.
122
+ oversampling_factor (float): how many times n_samples to generate each step, which helps to make sure that
123
+ enough samples are quickly generated in just one or two loops. Default: 10.
124
+ return_generator (bool): whether or not to return the numpy random number generator created. Default: False
125
+
126
+ Returns:
127
+ an array of sample radii of size n_samples, plus the random generator if return_generator==True.
128
+ """
129
+ _check_core_and_tidal_radii(r_core, r_tidal)
130
+
131
+ r_samples = np.empty(n_samples, dtype=float)
132
+ generator = np.random.default_rng(seed=seed)
133
+ completed_samples = 0
134
+
135
+ max_value = king_surface_density_fast(np.zeros(1), r_core, r_tidal)[0]
136
+
137
+ while completed_samples < n_samples:
138
+ remaining_samples = n_samples - completed_samples
139
+ samples_to_generate = int(
140
+ np.clip(remaining_samples * oversampling_factor, 10, np.inf)
141
+ )
142
+
143
+ # Generate some initial radius samples
144
+ test_r_values = generator.uniform(high=r_tidal, size=samples_to_generate)
145
+ test_king_values = king_surface_density_fast(test_r_values, r_core, r_tidal)
146
+ test_mcmc_values = generator.uniform(size=samples_to_generate, high=max_value)
147
+
148
+ # See which & how many are valid and save them!
149
+ valid_test_samples = test_king_values < test_mcmc_values
150
+ n_valid_test_samples = np.count_nonzero(valid_test_samples)
151
+
152
+ if n_valid_test_samples > remaining_samples:
153
+ r_samples[completed_samples:] = test_r_values[valid_test_samples][
154
+ :remaining_samples
155
+ ]
156
+ break
157
+
158
+ r_samples[
159
+ completed_samples : completed_samples + n_valid_test_samples
160
+ ] = test_r_values[valid_test_samples]
161
+ completed_samples += n_valid_test_samples
162
+
163
+ if return_generator:
164
+ return r_samples, generator
165
+ else:
166
+ return r_samples
167
+
168
+
169
+ def sample_1d_king_profile(
170
+ r_core: float,
171
+ r_tidal: float,
172
+ n_samples: int,
173
+ seed: Optional[int] = None,
174
+ oversampling_factor: float = 10,
175
+ ):
176
+ """Samples a 1D King profile (e.g. useful to get line of sight distances from the center of a cluster.) Uses a
177
+ little trick - assumes that we're looking at the cluster side-on and removes the not-line-of-sight axis as if we
178
+ were looking at it from the front. (This is because I cba to work out a 1D profile and more to the point, I couldn't
179
+ fucking find one)
180
+
181
+ Todo: change this to using the strip density g(x), which I *think* could do this better...
182
+
183
+ Valid only for:
184
+ 0 < r_core < r_tidal
185
+
186
+ Args:
187
+ r_core (float): the core radius of the cluster.
188
+ r_tidal (float): the tidal radius of the cluster.
189
+ n_samples (int): the number of samples to generate.
190
+ seed (int, optional): the seed of the random number generator. Default: None.
191
+ oversampling_factor (float): how many times n_samples to generate each step, which helps to make sure that
192
+ enough samples are quickly generated in just one or two loops. Default: 10.
193
+
194
+ Returns:
195
+ an array of sample radii of size n_samples
196
+ """
197
+ r_samples, generator = sample_2d_king_profile(
198
+ r_core,
199
+ r_tidal,
200
+ n_samples,
201
+ seed=seed,
202
+ oversampling_factor=oversampling_factor,
203
+ return_generator=True,
204
+ )
205
+
206
+ # Now, deproject this to 1D by giving every value an angle and then finding the 1D radius with the cosine
207
+ random_angles = generator.uniform(high=2 * np.pi, size=n_samples)
208
+
209
+ return r_samples * np.cos(random_angles)
210
+
211
+
212
+ def king_number_density(r, r_core, r_tidal, k=1):
213
+ """Calculates the King1962 number density (eqn 18 in the paper.)
214
+
215
+ Unnormalised by default (i.e. k=1.)
216
+ """
217
+ x = (r / r_core)**2
218
+ x_t = (r_tidal / r_core)**2
219
+
220
+ term_1 = np.log(1 + x)
221
+ term_2 = - 4 * ((1 + x)**(0.5) - 1) / (1 + x_t)**(0.5)
222
+ term_3 = x / (1 + x_t)
223
+
224
+ return np.pi * r_core**2 * k * (term_1 + term_2 + term_3)
225
+
@@ -0,0 +1,11 @@
1
+ from . import epsilon
2
+ from .nearest_neighbor import precalculate_nn_distances
3
+ from .preprocess import cut_dataset, rescale_dataset, recenter_dataset
4
+ from .resample import generate_gaia_covariance_matrix, resample_gaia_astrometry
5
+ import warnings
6
+
7
+ warnings.warn(
8
+ "ocelot.cluster API will change soon. Most objects will move, change, or be "
9
+ "deprecated.",
10
+ DeprecationWarning,
11
+ )