From 305096d4b15efffaa3205d4210639067dee16501 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:33:13 -0300 Subject: [PATCH 01/29] new develop cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28c94be9..b78cfaf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2" +version = "0.5.2-dev" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From 98242f768c08de26904a404fbdd2644c431e3e23 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:37:19 -0300 Subject: [PATCH 02/29] revert to re-compile docs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b78cfaf3..28c94be9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2-dev" +version = "0.5.2" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From ff131be067b1546f626e642ede63d255fb86f8d1 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:38:29 -0300 Subject: [PATCH 03/29] also fix date in docs --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 7904b9e9..c6332494 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,7 @@ extinction, distance, metallicity, age, binarity, mass, etc.. .. important:: - Version |ProjectVersion| released on the 10th of May, 2024. See :ref:`changelog` + Version |ProjectVersion| released on the 7th of May, 2024. See :ref:`changelog` for a detailed list of the changes implemented. From e1ab0b1850c37895282c1a1d2a23b3c4df98708c Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:51:30 -0300 Subject: [PATCH 04/29] fix date in changelog --- docs/CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 571434c4..69b96974 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -1,6 +1,6 @@ .. :changelog: -`[v0.5.2] `__ - 2024-05-XX +`[v0.5.2] `__ - 2024-05-07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Re-wrote isochrones reading module. See :ref:`isochronesload`. From 34d8a6280d86421054f7b7dc88eb6b53dec70451 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:53:11 -0300 Subject: [PATCH 05/29] new develop cycle... again --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28c94be9..b78cfaf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2" +version = "0.5.2-dev" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From 315c285d5357aa4a6bd584a96fada353dd7c581a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 16:56:16 -0300 Subject: [PATCH 06/29] ruff fixes --- docs/_static/IMF_plot.py | 2 +- docs/_static/asteca_icon.py | 6 +++--- docs/_static/binary_distr.py | 2 +- docs/_static/q_distr_plot.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/_static/IMF_plot.py b/docs/_static/IMF_plot.py index fdc6e32f..8118d286 100644 --- a/docs/_static/IMF_plot.py +++ b/docs/_static/IMF_plot.py @@ -3,7 +3,7 @@ def main(): - fig = plt.figure(figsize=(8, 4)) + plt.figure(figsize=(8, 4)) x_vals = np.array( list(np.arange(0.02, 1, 0.01)) + [1] + [1, 2, 3, 5, 10, 15, 20, 50, 100] diff --git a/docs/_static/asteca_icon.py b/docs/_static/asteca_icon.py index b6213f19..fcfff12d 100644 --- a/docs/_static/asteca_icon.py +++ b/docs/_static/asteca_icon.py @@ -1,12 +1,12 @@ import numpy as np import matplotlib.pyplot as plt -from matplotlib.patches import Ellipse +# from matplotlib.patches import Ellipse cmd_xy = [] with open("asteca_icon.dat", "r") as f: for line in f: - l = line.split() - cmd_xy.append([float(l[1]), float(l[0])]) + line_split = line.split() + cmd_xy.append([float(line_split[1]), float(line_split[0])]) cmd_xy = np.array(cmd_xy).T # fig = plt.figure(figsize=(25, 25)) diff --git a/docs/_static/binary_distr.py b/docs/_static/binary_distr.py index 4c1d7fe9..96a01565 100644 --- a/docs/_static/binary_distr.py +++ b/docs/_static/binary_distr.py @@ -57,7 +57,7 @@ def main(): x = np.linspace(0.029, 32, 100) y = 0.09 + 0.94 * (x / (1.4 + x)) - fig = plt.figure(figsize=(8, 4)) + plt.figure(figsize=(8, 4)) plt.scatter( mass_OF, multfreq_OF, diff --git a/docs/_static/q_distr_plot.py b/docs/_static/q_distr_plot.py index 7f583baa..4ea07140 100644 --- a/docs/_static/q_distr_plot.py +++ b/docs/_static/q_distr_plot.py @@ -132,7 +132,7 @@ def qunif_plot(N_m=50000): # for g in gamma: # y = f_gamma(q, g) # plt.plot(q, y, label=r"$\gamma=${:.2f}".format(g)) - fig = plt.figure(figsize=(8, 3)) + plt.figure(figsize=(8, 3)) gamma = -0.25 print(gamma) From 6f13d7a919322836c95824c803d4126472895449 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 17:24:03 -0300 Subject: [PATCH 07/29] testing job blocking code --- .readthedocs.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ba957bf8..d27d13c4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,6 +13,19 @@ build: # nodejs: "20" # rust: "1.70" # golang: "1.20" + jobs: + post_checkout: + # Cancel building pull requests when there aren't changes in the docs directory or YAML file. + # You can add any other files or directories that you'd like here as well, + # like your docs requirements file, or other files that will change your docs build. + # + # If there are no changes (git diff exits with 0) we force the command to return with 183. + # This is a special exit code on Read the Docs that will cancel the build immediately. + - | + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/develop -- docs/ .readthedocs.yaml; + then + exit 183; + fi # Build documentation in the "docs/" directory with Sphinx sphinx: From bec6b38d87562b8860b1ad251233992150988e55 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 17:36:13 -0300 Subject: [PATCH 08/29] minor --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 25431e6f..7c1fcc1c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ # and ignore these folders /docs/build/* /docs/autoapi/* +/docs/apidocs/* # Jupyter Notebook .ipynb_checkpoints \ No newline at end of file From 354abf3a8bc42ba5cb88e707c7d1914640ba8ff2 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 17:46:14 -0300 Subject: [PATCH 09/29] test job blocking in RDT once again --- .readthedocs.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d27d13c4..f006eafe 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -21,11 +21,12 @@ build: # # If there are no changes (git diff exits with 0) we force the command to return with 183. # This is a special exit code on Read the Docs that will cancel the build immediately. - - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/develop -- docs/ .readthedocs.yaml; - then - exit 183; - fi + - | + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && [ "$(git diff --quiet origin/develop -- docs/ .readthedocs.yml; echo $?)" -eq 0 ]; + then + echo "No changes to docs/ - exiting the build."; + exit 183; + fi # Build documentation in the "docs/" directory with Sphinx sphinx: From 883b726b85c0f10db13969a77e888300b754cf0a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 17:48:45 -0300 Subject: [PATCH 10/29] testing fix docs version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b78cfaf3..28c94be9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2-dev" +version = "0.5.2" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From a9253ee30e804f20d7fb2451264d0c80e5ada723 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 18:04:11 -0300 Subject: [PATCH 11/29] disabled webhook, reset --- .readthedocs.yaml | 11 +++++------ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f006eafe..d27d13c4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -21,12 +21,11 @@ build: # # If there are no changes (git diff exits with 0) we force the command to return with 183. # This is a special exit code on Read the Docs that will cancel the build immediately. - - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && [ "$(git diff --quiet origin/develop -- docs/ .readthedocs.yml; echo $?)" -eq 0 ]; - then - echo "No changes to docs/ - exiting the build."; - exit 183; - fi + - | + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/develop -- docs/ .readthedocs.yaml; + then + exit 183; + fi # Build documentation in the "docs/" directory with Sphinx sphinx: diff --git a/pyproject.toml b/pyproject.toml index 28c94be9..b78cfaf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2" +version = "0.5.2-dev" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From 6295842e8c67cc7a99ac42bab62ebb693409e324 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 18:06:25 -0300 Subject: [PATCH 12/29] minor, testing --- docs/conf.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 82132a8d..bd21ec87 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,6 @@ # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: +# For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- @@ -15,6 +14,12 @@ # import asteca # __version__ = asteca.__version__ + +# -- Project information ----------------------------------------------------- +project = "ASteCA" +copyright = "2024, Gabriel I Perren" +# author = 'Gabriel I Perren' + # Read version from pyproject.toml with open("../pyproject.toml", encoding="utf-8") as pyproject_toml: __version__ = ( @@ -23,13 +28,6 @@ .strip("'\"\n ") ) - -# -- Project information ----------------------------------------------------- - -project = "ASteCA" -copyright = "2024, Gabriel I Perren" -# author = 'Gabriel I Perren' - # The full version, including alpha/beta/rc tags version = __version__ release = __version__ @@ -97,16 +95,8 @@ def setup(sphinx): "dollarmath", ] -# # Don't re-order methods alphabetically -# autodoc_member_order = "bysource" - -# Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +# exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- @@ -133,7 +123,6 @@ def setup(sphinx): }, } - # Hide parent class name in right sidebar TOC for methods toc_object_entries_show_parents = "hide" From a691d3139a5bac5771d13dffb00859173407583b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 18:27:35 -0300 Subject: [PATCH 13/29] testing RTD block --- asteca/modules/cluster_priv.py | 212 +++++++++ asteca/modules/membership.py | 842 +++++++++++++++++++++++++++++++++ 2 files changed, 1054 insertions(+) create mode 100644 asteca/modules/cluster_priv.py create mode 100644 asteca/modules/membership.py diff --git a/asteca/modules/cluster_priv.py b/asteca/modules/cluster_priv.py new file mode 100644 index 00000000..eaaeaefb --- /dev/null +++ b/asteca/modules/cluster_priv.py @@ -0,0 +1,212 @@ +import warnings +import numpy as np +from scipy import spatial +import astropy.units as u +from astropy.coordinates import SkyCoord + + +def radec2lonlat(ra, dec): + gc = SkyCoord(ra=ra * u.degree, dec=dec * u.degree) + lb = gc.transform_to("galactic") + lon, lat = lb.l.value, lb.b.value + return lon, lat + + +def lonlat2radec(lon, lat): + gc = SkyCoord(l=lon * u.degree, b=lat * u.degree, frame='galactic') + ra, dec = gc.fk5.ra.value, gc.fk5.dec.value + return ra, dec + + +def reject_nans(data): + """Remove nans in 'data'""" + msk_all = [] + # Process each dimension separately + for arr in data: + # Identify non-nan data + msk = ~np.isnan(arr) + # Keep this non-nan data + msk_all.append(msk.data) + # Combine into a single mask + msk_accpt = np.logical_and.reduce(msk_all) + + # Indexes that survived + idx_clean = np.arange(data.shape[1])[msk_accpt] + + return idx_clean, data.T[msk_accpt].T + + +def get_5D_center( + lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cluster, N_clust_min, N_cent=500 +): + """ + Estimate the 5-dimensional center of a cluster. Steps: + + 1. Estimate the center in PMs (the value can be given as input) + 2. Using this center with any other given center, obtain the + 'N_cent' stars closest to the combined center. + 3. Estimate the 5-dimensional final center using KDE + + N_cent: how many stars are used to estimate the KDE center. + + """ + # Re-write if this parameter is given + if N_cluster is not None: + N_cent = N_cluster + + # Get filtered stars close to given xy+Plx centers (if any) to use + # in the PMs center estimation + pmRA_i, pmDE_i = filter_pms_stars(xy_c, plx_c, lon, lat, pmRA, pmDE, plx, N_cent) + + # (Re)estimate VPD center + vpd_c = get_pms_center(vpd_c, N_clust_min, pmRA_i, pmDE_i) + + # Get N_cent stars closest to vpd_c and given xy and/or plx centers + lon_i, lat_i, pmRA_i, pmDE_i, plx_i = get_stars_close_center( + lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cent + ) + + # kNN center + x_c, y_c, pmra_c, pmde_c, plx_c = get_kNN_center( + N_clust_min, np.array([lon_i, lat_i, pmRA_i, pmDE_i, plx_i]).T + ) + + return x_c, y_c, pmra_c, pmde_c, plx_c + + +def get_Nd_dists(cents, data, dists_flag=False): + """Obtain indexes and distances of stars to the given center""" + # Distances to center + dist_Nd = spatial.distance.cdist(data, cents).T[0] + if dists_flag: + # Return the distances + return dist_Nd + + # Indexes that sort the distances + d_idxs = dist_Nd.argsort() + # Return the indexes that sort the distances + return d_idxs + + +def filter_pms_stars(xy_c, plx_c, lon, lat, pmRA, pmDE, plx, N_cent): + """ """ + # Distances to xy_c+plx_c centers, if any was given + if xy_c is None and plx_c is None: + return pmRA, pmDE + + if xy_c is None and plx_c is not None: + cent = np.array([[plx_c]]) + data = np.array([plx]).T + elif xy_c is not None and plx_c is None: + cent = np.array([xy_c]) + data = np.array([lon, lat]).T + elif xy_c is not None and plx_c is not None: + cent = np.array([list(xy_c) + [plx_c]]) + data = np.array([lon, lat, plx]).T + + # Closest stars to the selected center + idx = get_Nd_dists(cent, data)[:N_cent] + pmRA_i, pmDE_i = pmRA[idx], pmDE[idx] + + return pmRA_i, pmDE_i + + +def get_pms_center(vpd_c, N_clust_min, pmRA, pmDE, N_bins=50, zoom_f=4, N_zoom=10): + """ """ + # vpd_mc = self.vpd_c # TODO is this necessary? + + vpd = np.array([pmRA, pmDE]).T + + # Center in PMs space + cx, cxm = None, None + for _ in range(N_zoom): + N_stars = vpd.shape[0] + if N_stars < N_clust_min: + break + + # Find center coordinates as max density + x, y = vpd.T + H, edgx, edgy = np.histogram2d(x, y, bins=N_bins) + flat_idx = H.argmax() + cbx, cby = np.unravel_index(flat_idx, H.shape) + cx = (edgx[cbx + 1] + edgx[cbx]) / 2.0 + cy = (edgy[cby + 1] + edgy[cby]) / 2.0 + + # If a manual center was set, use it + if vpd_c is not None: + # Store the auto center for later + cxm, cym = cx, cy + # Use the manual to zoom in + cx, cy = vpd_c + + # Zoom in + rx, ry = edgx[1] - edgx[0], edgy[1] - edgy[0] + msk = ( + (x < (cx + zoom_f * rx)) + & (x > (cx - zoom_f * rx)) + & (y < (cy + zoom_f * ry)) + & (y > (cy - zoom_f * ry)) + ) + vpd = vpd[msk] + + # If a manual center was set + if vpd_c is not None: + # If a better center value could be estimated + if cxm is not None: + cx, cy = cxm, cym + else: + cx, cy = vpd_c + warnings.warn("Could not estimate a better PMs center value") + else: + if cx is None: + cx, cy = vpd_c + raise Exception("Could not estimate the PMs center value") + + return [cx, cy] + + +def get_stars_close_center(lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cent): + """ + Distances to centers using the vpd_c and other available data + """ + if xy_c is None and plx_c is None: + cent = np.array([vpd_c]) + data = np.array([pmRA, pmDE]).T + elif xy_c is None and plx_c is not None: + cent = np.array([vpd_c + [plx_c]]) + data = np.array([pmRA, pmDE, plx]).T + elif xy_c is not None and plx_c is None: + cent = np.array([list(xy_c) + vpd_c]) + data = np.array([lon, lat, pmRA, pmDE]).T + elif xy_c is not None and plx_c is not None: + cent = np.array([list(xy_c) + vpd_c + [plx_c]]) + data = np.array([lon, lat, pmRA, pmDE, plx]).T + + # Closest stars to the selected center + idx = get_Nd_dists(cent, data)[:N_cent] + + return lon[idx], lat[idx], pmRA[idx], pmDE[idx], plx[idx] + + +def get_kNN_center(N_clust_min, data): + """ + Estimate 5D center with kNN. Better results are obtained not using + the parallax data + """ + data_noplx = data[:, :4] + + tree = spatial.cKDTree(data_noplx) + inx = tree.query(data_noplx, k=N_clust_min + 1) + NN_dist = inx[0].max(1) + # Convert to densities + dens = 1.0 / NN_dist + # Sort by largest density + idxs = np.argsort(-dens) + + # Use the star with the largest density + # cent = np.array([data[idxs[0]]])[0] + # # Use median of stars with largest densities + cent = np.median(data[idxs[:N_clust_min]], 0) + + x_c, y_c, pmra_c, pmde_c, plx_c = cent + return x_c, y_c, pmra_c, pmde_c, plx_c diff --git a/asteca/modules/membership.py b/asteca/modules/membership.py new file mode 100644 index 00000000..8e99b123 --- /dev/null +++ b/asteca/modules/membership.py @@ -0,0 +1,842 @@ +import warnings +import numpy as np +from astropy.stats import RipleysKEstimator +from scipy import spatial +# from scipy.stats import gaussian_kde # NEEDS TEST, 05/24 +from . import cluster_priv as cp + + +def fastMP( + X, + xy_c, + vpd_c, + plx_c, + fixed_centers, + N_cluster, + N_clust_min, + N_clust_max, + centers_ex, + N_resample, +): + """ """ + print("\nRunning fastMP...") + print(f"fixed_centers : {fixed_centers}") + print(f"N_cluster : {N_cluster}") + print(f"N_clust_min : {N_clust_min}") + print(f"N_clust_max : {N_clust_max}") + centers_ex_flag = False + if centers_ex is not None: + centers_ex_flag = True + print(f"centers_ex : {centers_ex_flag}") + print(f"N_resample : {N_resample}") + + # HARDCODED + N_break = max(50, int(N_resample * 0.05)) + # HARDCODED + + # Prepare dictionary of parameters for extra clusters in frame (if any) + extra_cls_dict, dims_msk = prep_extra_cl_dict(centers_ex) + + # Remove 'nan' values + N_all = X.shape[1] + idx_clean, X_no_nan = cp.reject_nans(X) + # Unpack input data with no 'nans' + lon, lat, pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx = X_no_nan + + # Estimate initial center + xy_c, vpd_c, plx_c = get_center( + xy_c, + vpd_c, + plx_c, + fixed_centers, + N_cluster, + N_clust_min, + lon, + lat, + pmRA, + pmDE, + plx, + ) + cents_init = [xy_c, vpd_c, plx_c] + + # Remove the most obvious field stars to speed up the process + idx_clean, lon, lat, pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx = first_filter( + N_clust_max, + idx_clean, + vpd_c, + plx_c, + lon, + lat, + pmRA, + pmDE, + plx, + e_pmRA, + e_pmDE, + e_plx, + ) + + # Prepare Ripley's K data + rads, Kest, C_thresh_N = init_ripley(lon, lat) + + # Initiate here as None, will be estimated after the members estimation + # using a selection of probable members + dims_norm = None + + # Estimate the number of members + if N_cluster is None: + N_survived, st_idx = estimate_nmembs( + N_clust_min, + N_clust_max, + extra_cls_dict, + dims_msk, + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + C_thresh_N, + rads, + Kest, + dims_norm, + ) + else: + N_survived = int(N_cluster) + st_idx = None + + cents_3d = np.array([list(vpd_c) + [plx_c]]) + data_3d = np.array([pmRA, pmDE, plx]).T + # Ordered indexes according to smallest distances to 'cents_3d' + d_pm_plx_idxs = cp.get_Nd_dists(cents_3d, data_3d) + st_idx = d_pm_plx_idxs[:N_survived] + + # Define here 'dims_norm' value used for data normalization + data_5d = np.array([lon, lat, pmRA, pmDE, plx]).T + cents_5d = np.array([xy_c + vpd_c + [plx_c]]) + data_mvd = data_5d - cents_5d + if st_idx is not None: + dims_norm = 2 * np.nanmedian(abs(data_mvd[st_idx]), 0) + else: + dims_norm = 2 * np.nanmedian(abs(data_mvd), 0) + + idx_selected = [] + N_runs, N_05_old, prob_old, break_check = 0, 0, 1, 0 + for _ in range(N_resample + 1): + # Sample data + s_pmRA, s_pmDE, s_plx = data_sample(pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx) + + # Data normalization + data_5d, cents_5d = get_dims_norm( + N_clust_min, + dims_norm, + lon, + lat, + s_pmRA, + s_pmDE, + s_plx, + xy_c, + vpd_c, + plx_c, + st_idx, + ) + + # Indexes of the sorted 5D distances to the estimated center + d_idxs = cp.get_Nd_dists(cents_5d, data_5d) + + # Star selection + st_idx = d_idxs[:N_survived] + + # Filter extra clusters in frame (if any) + st_idx = filter_cls_in_frame( + lon[st_idx], + lat[st_idx], + pmRA[st_idx], + pmDE[st_idx], + plx[st_idx], + xy_c, + vpd_c, + plx_c, + st_idx, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, + ) + + # Re-estimate centers using the selected stars + if len(st_idx) > N_clust_min: + xy_c, vpd_c, plx_c = get_center( + xy_c, + vpd_c, + plx_c, + fixed_centers, + N_cluster, + N_clust_min, + lon[st_idx], + lat[st_idx], + pmRA[st_idx], + pmDE[st_idx], + plx[st_idx], + ) + + idx_selected += list(st_idx) + N_runs += 1 + + # Convergence check + if idx_selected: + break_check, prob_old, N_05_old = get_break_check( + break_check, N_runs, idx_selected, prob_old, N_05_old + ) + if break_check > N_break: + break + + # Assign final probabilities + probs_final = assign_probs(N_all, idx_clean, idx_selected, N_runs) + # Change '0' probabilities using linear relation + probs_final = probs_0(N_clust_min, dims_norm, X, cents_init, probs_final) + + if break_check > N_break: + print(f"Convergence reached at {N_runs} runs.") + else: + print(f"Maximum number of runs reached: {N_resample}.") + print(f"Estimated number of members: {N_survived}") + + return probs_final + + +def get_break_check(break_check, N_runs, idx_selected, prob_old, N_05_old): + """ """ + counts = np.unique(idx_selected, return_counts=True)[1] + probs = counts / N_runs + msk = probs > 0.5 # HARDCODED + N_05 = msk.sum() + if N_05 > 2: + prob_mean = np.mean(probs[msk]) + delta_probs = abs(prob_mean - prob_old) + if N_05 == N_05_old or delta_probs < 0.001: # HARDCODED + break_check += 1 + else: + # Reset + break_check = 0 + prob_old, N_05_old = prob_mean, N_05 + + return break_check, prob_old, N_05_old + + +def prep_extra_cl_dict(centers_ex): + """ + The parameter 'centers_ex' must be a list of dictionaries, one + dictionary for each extra cluster in the frame. The dictionaries + must have at most three keys, 'xy', 'pms', 'plx', each with a list + containing the center value(s) in those dimensions. Example: + + centers_ex = [{'xy': [105.39, 0.9], 'plx': [1.3]}] + + centers_ex = [ + {'xy': [105.39, 0.9], 'pms': [3.5, -0.7]}, + {'xy': [0.82, -4.5], 'pms': [3.5, -0.7], 'plx': [3.5]} + ] + """ + extra_cls_dict = {"run_flag": False} + + if centers_ex is None: + return extra_cls_dict, None + + extra_cls_dict["run_flag"] = True + + # Set the distribution of dimensions + dims_msk = { + "xy": np.array([0, 1]), + "pms": np.array([2, 3]), + "plx": np.array([4]), + "xy_pms": np.array([0, 1, 2, 3]), + "xy_plx": np.array([0, 1, 4]), + "pms_plx": np.array([2, 3, 4]), + "xy_pms_plx": np.array([0, 1, 2, 3, 4]), + } + + # Read centers of the extra clusters in frame + dims = ["xy", "pms", "plx"] + dim_keys, cents = [], [] + for extra_cl in centers_ex: + dims_ex = extra_cl.keys() + dims_id, centers = "", [] + for d in dims: + if d in dims_ex: + center = extra_cl[d] + if center is not None: + dims_id += "_" + d + centers += center + # Store dimensions ids and centers for each extra cluster + dim_keys.append(dims_id[1:]) + cents.append(centers) + + extra_cls_dict["dim_keys"] = dim_keys + extra_cls_dict["centers"] = cents + + return extra_cls_dict, dims_msk + + +def get_center( + xy_c, + vpd_c, + plx_c, + fixed_centers, + N_cluster, + N_clust_min, + lon, + lat, + pmRA, + pmDE, + plx, +): + """Return 5-dimensional center values, within the given constrains""" + + # Skip process if all centers are fixed + if ( + fixed_centers is True + and xy_c is not None + and vpd_c is not None + and plx_c is not None + ): + x_c, y_c = xy_c + pmra_c, pmde_c = vpd_c + plx_c = plx_c + return [x_c, y_c], [pmra_c, pmde_c], plx_c + + # Estimate initial center + x_c, y_c, pmra_c, pmde_c, plx_c = cp.get_5D_center( + lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cluster, N_clust_min + ) + # Re-write values if necessary + if fixed_centers is True: + if xy_c is not None: + x_c, y_c = xy_c + if vpd_c is not None: + pmra_c, pmde_c = vpd_c + if plx_c is not None: + plx_c = plx_c + + return [x_c, y_c], [pmra_c, pmde_c], plx_c + + +def first_filter( + N_clust_max, + idx_all, + vpd_c, + plx_c, + lon, + lat, + pmRA, + pmDE, + plx, + e_pmRA, + e_pmDE, + e_plx, + plx_cut=0.5, + v_kms_max=5, + pm_max=3, + N_times=5, +): + """ + plx_cut: Parallax value that determines the cut in the filter rule + between using 'v_kms_max' or 'pm_max' + v_kms_max: + pm_max: + N_times: number of times used to multiply 'N_clust_max' to determine + how many stars to keep + """ + # Remove obvious field stars + pms = np.array([pmRA, pmDE]).T + pm_rad = spatial.distance.cdist(pms, np.array([vpd_c])).T[0] + msk1 = (plx > plx_cut) & (pm_rad / (abs(plx) + 0.0001) < v_kms_max) + msk2 = (plx <= plx_cut) & (pm_rad < pm_max) + # Stars to keep + msk = msk1 | msk2 + + # Update arrays + lon, lat, pmRA, pmDE, plx = lon[msk], lat[msk], pmRA[msk], pmDE[msk], plx[msk] + e_pmRA, e_pmDE, e_plx = e_pmRA[msk], e_pmDE[msk], e_plx[msk] + # Update indexes of surviving elements + idx_all = idx_all[msk] + + if msk.sum() < N_clust_max * N_times: + return idx_all, lon, lat, pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx + + # Sorted indexes of distances to pms+plx center + cents_3d = np.array([list(vpd_c) + [plx_c]]) + data_3d = np.array([pmRA, pmDE, plx]).T + d_pm_plx_idxs = cp.get_Nd_dists(cents_3d, data_3d) + # Indexes of stars to keep and reject based on their distance + idx_acpt = d_pm_plx_idxs[: int(N_clust_max * N_times)] + + # Update arrays + lon, lat, pmRA, pmDE, plx = ( + lon[idx_acpt], + lat[idx_acpt], + pmRA[idx_acpt], + pmDE[idx_acpt], + plx[idx_acpt], + ) + e_pmRA, e_pmDE, e_plx = e_pmRA[idx_acpt], e_pmDE[idx_acpt], e_plx[idx_acpt] + # Update indexes of surviving elements + idx_all = idx_all[idx_acpt] + + return idx_all, lon, lat, pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx + + +def init_ripley(lon, lat): + """ + https://rdrr.io/cran/spatstat/man/Kest.html + "For a rectangular window it is prudent to restrict the r values to a + maximum of 1/4 of the smaller side length of the rectangle + (Ripley, 1977, 1988; Diggle, 1983)" + """ + xmin, xmax = lon.min(), lon.max() + ymin, ymax = lat.min(), lat.max() + area = (xmax - xmin) * (ymax - ymin) + Kest = RipleysKEstimator(area=area, x_max=xmax, y_max=ymax, x_min=xmin, y_min=ymin) + + # Ripley's rule of thumb + thumb = 0.25 * min((xmax - xmin), (ymax - ymin)) + # Large sample rule + rho = len(lon) / area + large = np.sqrt(1000 / (np.pi * rho)) + + lmin = min(thumb, large) + rads = np.linspace(lmin * 0.01, lmin, 100) # HARDCODED + + C_thresh_N = 1.68 * np.sqrt(area) # HARDCODED + + return rads, Kest, C_thresh_N + + +def estimate_nmembs( + N_clust_min, + N_clust_max, + extra_cls_dict, + dims_msk, + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + C_thresh_N, + rads, + Kest, + dims_norm, + kde_prob_cut=0.25, + N_clust=50, + N_extra=5, + N_step=10, + N_break=5, +): + """ + Estimate the number of cluster members + """ + idx_survived = ripley_survive( + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + N_clust, + N_extra, + N_step, + N_break, + C_thresh_N, + rads, + Kest, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, + ) + N_survived = len(idx_survived) + + if N_survived < N_clust_min: + warnings.warn("The estimated number of cluster members is " + f"<{N_clust_min}") + return N_clust_min, None + + # Filter extra clusters in frame (if any) + msk = np.array(idx_survived) + idx_survived = filter_cls_in_frame( + lon[msk], + lat[msk], + pmRA[msk], + pmDE[msk], + plx[msk], + xy_c, + vpd_c, + plx_c, + msk, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, + ) + + # NOT SURE WHY I REMOVED THIS BLOCK, POOR PERFORMANCE MOST LIKELY. NEED TO TEST + # # Filter by (lon, lat) KDE + # kde_probs = self.kde_probs(lon, lat, idx_survived, msk) + # if kde_probs is not None: + # kde_prob_cut = np.percentile(kde_probs, 25) + # msk = kde_probs > kde_prob_cut + # idx_survived = idx_survived[msk] + + N_survived = len(idx_survived) + + if N_survived < N_clust_min: + warnings.warn("The estimated number of cluster members is " + f"<{N_clust_min}") + return N_clust_min, None + + if N_survived > N_clust_max: + warnings.warn("The estimated number of cluster members is " + f">{N_clust_max}") + # Select the maximum number of stars from those closest to the + # center + data_norm, cents_norm = get_dims_norm( + N_clust_min, + dims_norm, + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + idx_survived, + ) + d_idxs = cp.get_Nd_dists(cents_norm, data_norm[idx_survived]) + idx_survived = idx_survived[d_idxs][:N_clust_max] + return N_clust_max, idx_survived + + return N_survived, idx_survived + + +def ripley_survive( + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + N_clust, + N_extra, + N_step, + N_break, + C_thresh_N, + rads, + Kest, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, +): + """ + Process data to identify the indexes of stars that survive the + Ripley's K filter + """ + cents_3d = np.array([list(vpd_c) + [plx_c]]) + data_3d = np.array([pmRA, pmDE, plx]).T + # Ordered indexes according to smallest distances to 'cents_3d' + d_pm_plx_idxs = cp.get_Nd_dists(cents_3d, data_3d) + xy = np.array([lon, lat]).T + N_stars = xy.shape[0] + + # Identify stars associated to extra clusters in frame (if any) + msk = np.arange(0, len(lon)) + idx_survived_init = filter_cls_in_frame( + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + msk, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, + ) + + def core(N_clust_surv, idx_survived_init): + """ + This is the core function that estimates the number of members + based on Ripley's K-function. + """ + # Define K-function threshold + C_thresh = C_thresh_N / N_clust_surv + idx_survived = [] + N_break_count, step_old = 0, 0 + for step in np.arange(N_clust_surv, N_stars, N_clust_surv): + # Ring of stars around the VPD+Plx centers + msk_ring = d_pm_plx_idxs[step_old:step] + # Obtain their Ripely K estimator + C_s = rkfunc(xy[msk_ring], rads, Kest) + + # Remove stars associated to other clusters + msk_extra_cls = np.isin(msk_ring, idx_survived_init) + msk = msk_ring[msk_extra_cls] + # If only a small percentage survived, discard + if len(msk) < int(N_clust_surv * 0.25): # HARDCODED + C_s = 0 + + if not np.isnan(C_s): + # This group of stars survived + if C_s >= C_thresh: + idx_survived += list(msk) + else: + # Increase break condition + N_break_count += 1 + if N_break_count > N_break: + break + step_old = step + return idx_survived + + # Select those clusters where the stars are different enough from a + # random distribution + idx_survived = core(N_clust, idx_survived_init) + if not idx_survived: + # If the default clustering number did not work, try a few + # more values with an increasing number of cluster stars + for _ in range(N_extra): + N_clust_surv = int(N_clust + (_ + 1) * N_step) + idx_survived = core(N_clust_surv, idx_survived_init) + # Break out when (if) any value selected stars + if len(idx_survived) > 0: + break + + return idx_survived + + +def rkfunc(xy, rads, Kest): + """ + Test how similar this cluster's (x, y) distribution is compared + to a uniform random distribution using Ripley's K. + https://stats.stackexchange.com/a/122816/10416 + + Parameters + ---------- + xy : TYPE + Description + rads : TYPE + Description + Kest : TYPE + Description + + Returns + ------- + TYPE + Description + """ + # Avoid large memory consumption if the data array is too big + # if xy.shape[0] > 5000: + # mode = "none" + # else: + # mode = 'translation' + + # Hide RunTimeWarning + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + L_t = Kest.Lfunction(xy, rads, mode="translation") + + # Catch all-nans. Avoid 'RuntimeWarning: All-NaN slice encountered' + if np.isnan(L_t).all(): + C_s = np.nan + else: + C_s = np.nanmax(abs(L_t - rads)) + + return C_s + + +def data_sample(pmRA, pmDE, plx, e_pmRA, e_pmDE, e_plx): + """Gaussian random sample""" + data_3 = np.array([pmRA, pmDE, plx]) + grs = np.random.normal(0.0, 1.0, data_3.shape[1]) + data_err = np.array([e_pmRA, e_pmDE, e_plx]) + return data_3 + grs * data_err + + +def get_dims_norm( + N_clust_min, dims_norm, lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, msk +): + """ + Normalize dimensions using twice the median of the selected probable + members. + """ + data_5d = np.array([lon, lat, pmRA, pmDE, plx]).T + cents_5d = np.array([xy_c + vpd_c + [plx_c]]) + + if msk is None or len(msk) < N_clust_min: + return data_5d, cents_5d + + data_mvd = data_5d - cents_5d + + if dims_norm is None: + dims_norm = 2 * np.nanmedian(abs(data_mvd[msk]), 0) + data_norm = data_mvd / dims_norm + # from sklearn.preprocessing import RobustScaler + # data_norm = RobustScaler().fit(data_5d).transform(data_5d) + else: + data_norm = data_mvd / dims_norm + + cents_norm = np.array([[0.0, 0.0, 0.0, 0.0, 0.0]]) + + return data_norm, cents_norm + + +def filter_cls_in_frame( + lon, + lat, + pmRA, + pmDE, + plx, + xy_c, + vpd_c, + plx_c, + idx_survived, + extra_cls_dict, + dims_msk, + N_clust_min, + dims_norm, +): + """ + Filter extra clusters in frame (if any) + """ + # If there are no extra clusters to remove, skip + if extra_cls_dict["run_flag"] is False: + return idx_survived + + # Add centers to the end of these lists to normalize their values too + dims_plus_cents = [list(_) for _ in (lon, lat, pmRA, pmDE, plx)] + for cent in extra_cls_dict["centers"]: + # For each dimension, some won't be present + for i in range(5): + try: + dims_plus_cents[i].append(cent[i]) + except IndexError: + dims_plus_cents[i].append(np.nan) + lon2, lat2, pmRA2, pmDE2, plx2 = dims_plus_cents + + # Data normalization + msk = np.full(len(lon2), True) + data, cents = get_dims_norm( + N_clust_min, dims_norm, lon2, lat2, pmRA2, pmDE2, plx2, xy_c, vpd_c, plx_c, msk + ) + data = data.T + + # Extract normalized centers for extra clusters + new_cents = [] + for i in range(len(extra_cls_dict["centers"]), 0, -1): + new_cents.append([_ for _ in data[:, -i] if not np.isnan(_)]) + # Remove center values from the 'data' array + data = data[:, : -len(new_cents)] + + # Find the distances to the center for all the combinations of data + # dimensions in the extra clusters in frame + dists = {} + for dims in list(set(extra_cls_dict["dim_keys"])): + msk = dims_msk[dims] + dists[dims] = cp.get_Nd_dists(cents[:, msk], data[msk, :].T, True) + + # Identify stars closer to the cluster's center than the centers of + # extra clusters + msk_d = np.full(len(idx_survived), True) + for i, cent_ex in enumerate(new_cents): + dims_ex = extra_cls_dict["dim_keys"][i] + msk = dims_msk[dims_ex] + # Distance to this extra cluster's center + dists_ex = cp.get_Nd_dists(np.array([cent_ex]), data[msk, :].T, True) + + # If the distance to the selected cluster center is smaller than + # the distance to this extra cluster's center, keep the star + msk_d = msk_d & (dists[dims_ex] <= dists_ex) + + # Never return less than N_clust_min stars + if msk_d.sum() < N_clust_min: + return idx_survived + + return idx_survived[msk_d] + + +def assign_probs(N_all, idx_clean, idx_selected, N_runs): + """Assign final probabilities for all stars + + N_all: Total number of stars in input frame + idx_clean: indexes of stars that survived the removal of 'nans' and + of stars that are clear field stars + """ + # Initial -1 probabilities for *all* stars + probs_final = np.zeros(N_all) - 1 + + # Number of processed stars (ie: not rejected as nans) + N_stars = len(idx_clean) + # Initial zero probabilities for the processed stars + probs_all = np.zeros(N_stars) + + if idx_selected: + # Estimate probabilities as averages of counts + values, counts = np.unique(idx_selected, return_counts=True) + probs = counts / N_runs + # Store probabilities for processed stars + probs_all[values] = probs + else: + warnings.warn("No stars were identified as possible members") + + # Assign the estimated probabilities to the processed stars + probs_final[idx_clean] = probs_all + + return probs_final + + +def probs_0(N_clust_min, dims_norm, X, cents_init, probs_final, p_min=0.1): + """ + To all stars with prob=0 assign a probability from 0 to p_min + that follows a linear relation associated to their 5D distance to the + initial defined center + """ + # Stars with '0' probabilities + msk_0 = probs_final == 0.0 + # If no stars with prob=0, nothing to do + if msk_0.sum() == 0: + return probs_final + + # Original full data + lon, lat, pmRA, pmDE, plx = X[:5] + xy_c, vpd_c, plx_c = cents_init + + # Data normalization for all the stars + msk = np.full((len(lon)), True) + data_5d, cents_5d = get_dims_norm( + N_clust_min, dims_norm, lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, msk + ) + # 5D distances to the estimated center + dists = cp.get_Nd_dists(cents_5d, data_5d, True) + + # Select 'p_min' as the minimum probability between (0., 0.5) + msk_0_5 = (probs_final > 0.0) & (probs_final < 0.5) + if msk_0_5.sum() > 1: + p_min = probs_final[msk_0_5].min() + + # Linear relation for: (0, d_max), (p_min, d_min) + d_min, d_max = dists[msk_0].min(), dists[msk_0].max() + m, h = (d_min - d_max) / p_min, d_max + probs_0_v = (dists[msk_0] - h) / m + + # Assign new probabilities to 'msk_0' stars + probs_final[msk_0] = probs_0_v + + return probs_final From c07051fe7416ed7b8283c5a3782a4125f98e0c5f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 18:41:55 -0300 Subject: [PATCH 14/29] testing --- .readthedocs.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d27d13c4..56098430 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -22,7 +22,7 @@ build: # If there are no changes (git diff exits with 0) we force the command to return with 183. # This is a special exit code on Read the Docs that will cancel the build immediately. - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/develop -- docs/ .readthedocs.yaml; + if git diff --quiet origin/develop -- docs/ .readthedocs.yaml; then exit 183; fi diff --git a/pyproject.toml b/pyproject.toml index b78cfaf3..28c94be9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2-dev" +version = "0.5.2" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From 566b1d84583da2c16899598a33c57e5f7a9571bf Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 19:24:18 -0300 Subject: [PATCH 15/29] rm not used faq --- docs/contents/faq.rst | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 docs/contents/faq.rst diff --git a/docs/contents/faq.rst b/docs/contents/faq.rst deleted file mode 100644 index 1194205b..00000000 --- a/docs/contents/faq.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _faq: - -FAQ -### - -xxx From 9264712fe551e967f68e975cea1524a8f537224e Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 23 May 2024 19:25:54 -0300 Subject: [PATCH 16/29] minor --- docs/_static/asteca_icon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_static/asteca_icon.py b/docs/_static/asteca_icon.py index fcfff12d..b60bcf22 100644 --- a/docs/_static/asteca_icon.py +++ b/docs/_static/asteca_icon.py @@ -1,5 +1,6 @@ import numpy as np import matplotlib.pyplot as plt + # from matplotlib.patches import Ellipse cmd_xy = [] From d1eb84fff67eee6561d936a08806f053764d6656 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 24 May 2024 10:30:53 -0300 Subject: [PATCH 17/29] cleaning up --- .readthedocs.yaml | 13 ------------- asteca/__init__.py | 2 +- asteca/modules/synth_cluster_priv.py | 9 +++++++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 56098430..ba957bf8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,19 +13,6 @@ build: # nodejs: "20" # rust: "1.70" # golang: "1.20" - jobs: - post_checkout: - # Cancel building pull requests when there aren't changes in the docs directory or YAML file. - # You can add any other files or directories that you'd like here as well, - # like your docs requirements file, or other files that will change your docs build. - # - # If there are no changes (git diff exits with 0) we force the command to return with 183. - # This is a special exit code on Read the Docs that will cancel the build immediately. - - | - if git diff --quiet origin/develop -- docs/ .readthedocs.yaml; - then - exit 183; - fi # Build documentation in the "docs/" directory with Sphinx sphinx: diff --git a/asteca/__init__.py b/asteca/__init__.py index 6d31311e..c62f960e 100644 --- a/asteca/__init__.py +++ b/asteca/__init__.py @@ -1,6 +1,6 @@ +from .cluster import cluster as cluster from .isochrones import isochrones as isochrones from .synthetic import synthetic as synthetic -from .cluster import cluster as cluster from .likelihood import likelihood as likelihood from contextlib import suppress diff --git a/asteca/modules/synth_cluster_priv.py b/asteca/modules/synth_cluster_priv.py index 0833cfa5..185f8288 100644 --- a/asteca/modules/synth_cluster_priv.py +++ b/asteca/modules/synth_cluster_priv.py @@ -209,11 +209,16 @@ def ccmo_ext_coeffs(self) -> list: # For colors. eff_wave1, eff_wave2 = self.isochs.color_effl - ext_coefs[1] = [ccmo_model(10000.0 / eff_wave1), ccmo_model(10000.0 / eff_wave2)] + ext_coefs[1] = [ + ccmo_model(10000.0 / eff_wave1), + ccmo_model(10000.0 / eff_wave2), + ] if self.isochs.color2_effl is not None: eff_wave1, eff_wave2 = self.isochs.color2_effl - ext_coefs += [[ccmo_model(10000.0 / eff_wave1), ccmo_model(10000.0 / eff_wave2)]] + ext_coefs += [ + [ccmo_model(10000.0 / eff_wave1), ccmo_model(10000.0 / eff_wave2)] + ] return ext_coefs From 89315066235389555404f30a2b5427241015954c Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:11:19 -0300 Subject: [PATCH 18/29] updt docs --- docs/conf.py | 66 ++++++++++++++++++++++++------------------- docs/contents/api.rst | 37 ++++++++++++++++++++++++ docs/index.rst | 5 +--- docs/requirements.txt | 2 +- 4 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 docs/contents/api.rst diff --git a/docs/conf.py b/docs/conf.py index bd21ec87..65fac49d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,13 +37,9 @@ ) # -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - # "sphinx.ext.autodoc", - "autoapi.extension", + "autodoc2", "sphinx.ext.napoleon", "sphinx.ext.githubpages", "myst_nb", @@ -52,29 +48,41 @@ "sphinx.ext.mathjax", ] -autoapi_dirs = ["../asteca"] -# autoapi_options = ["imported-members", "show-inheritance", "inherited-members"] -# autoapi_ignore = ["_*"] -autoapi_add_toctree_entry = False -autoapi_keep_files = True -# autoapi_own_page_level = "attribute" - - -# Hide private files -def skip_submodules(app, what, name, obj, skip, options): - if what in ("package", "function", "attribute"): - skip = True - if what == "method": # Skip private methods - if name.split(".")[-1].startswith("_"): - skip = True - # if skip is False: - # print(what, ",", name, ",", obj) - # # breakpoint() - return skip - - -def setup(sphinx): - sphinx.connect("autoapi-skip-member", skip_submodules) +autodoc2_packages = [ + { + "path": "../asteca", + "exclude_dirs": [ + "__pycache__", + "modules", + ], + } +] +autodoc2_hidden_objects = ["dunder", "private", "inherited"] + +################################################################ +# This block removes the attributes from the apidocs .rst files. +# I could not find a simpler way to do this 26/05/24 +# https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx-core-events +import os +def source_read_handler(app, docname, source): + path = "./apidocs/asteca/" + for file in os.listdir(path): + with open(path + file) as f: + lines = f.readlines() + idxs = [] + for i, line in enumerate(lines): + if ".. py:attribute:" in line: + idxs += list(range(i, i+6)) + for i in range(len(lines), 0, -1): + if i in idxs: + del lines[i] + with open(path + file, "w") as f: + for line in lines: + f.write(line) + +def setup(app): + app.connect('source-read', source_read_handler) +################################################################ # https://www.sympy.org/sphinx-math-dollar/ diff --git a/docs/contents/api.rst b/docs/contents/api.rst new file mode 100644 index 00000000..49fc5b97 --- /dev/null +++ b/docs/contents/api.rst @@ -0,0 +1,37 @@ +API Reference +============= + +.. toctree:: + :titlesonly: + :maxdepth: 2 + + /../apidocs/asteca/asteca.cluster + /../apidocs/asteca/asteca.membership + /../apidocs/asteca/asteca.isochrones + /../apidocs/asteca/asteca.synthetic + /../apidocs/asteca/asteca.likelihood + + +Classes +~~~~~~~ + +.. list-table:: + :class: autosummary longtable + :align: left + + * - :py:obj:`cluster ` + - .. autodoc2-docstring:: asteca.cluster.cluster + :summary: + * - :py:obj:`fastmp ` + - .. autodoc2-docstring:: asteca.membership.fastmp + :summary: + * - :py:obj:`isochrones ` + - .. autodoc2-docstring:: asteca.isochrones.isochrones + :summary: + * - :py:obj:`synthetic ` + - .. autodoc2-docstring:: asteca.synthetic.synthetic + :summary: + * - :py:obj:`likelihood ` + - .. autodoc2-docstring:: asteca.likelihood.likelihood + :summary: + diff --git a/docs/index.rst b/docs/index.rst index c6332494..5eb6ee87 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -76,6 +76,7 @@ TOC contents/installation contents/changelog + contents/api .. toctree:: :maxdepth: 1 @@ -92,8 +93,4 @@ TOC tutorials/quickstart -.. toctree:: - :maxdepth: 1 - :caption: API Reference - autoapi/asteca/index \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 6c22bb7c..f019f03c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ sphinx_book_theme myst-nb sphinx-math-dollar -sphinx-autoapi +sphinx-autodoc2 From 2244b78559808e231d7065f24b58553569c7bff9 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:17:13 -0300 Subject: [PATCH 19/29] minor --- docs/conf.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 65fac49d..c4cd09e9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,6 +2,7 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os # -- Path setup -------------------------------------------------------------- @@ -59,12 +60,13 @@ ] autodoc2_hidden_objects = ["dunder", "private", "inherited"] + ################################################################ # This block removes the attributes from the apidocs .rst files. # I could not find a simpler way to do this 26/05/24 # https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx-core-events -import os def source_read_handler(app, docname, source): + """'docname, source' not used but required""" path = "./apidocs/asteca/" for file in os.listdir(path): with open(path + file) as f: @@ -72,7 +74,7 @@ def source_read_handler(app, docname, source): idxs = [] for i, line in enumerate(lines): if ".. py:attribute:" in line: - idxs += list(range(i, i+6)) + idxs += list(range(i, i + 6)) for i in range(len(lines), 0, -1): if i in idxs: del lines[i] @@ -80,8 +82,11 @@ def source_read_handler(app, docname, source): for line in lines: f.write(line) + def setup(app): - app.connect('source-read', source_read_handler) + app.connect("source-read", source_read_handler) + + ################################################################ From 0d466327ef8fc3eb6444ea07b7666dc5792d9a1f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:17:40 -0300 Subject: [PATCH 20/29] add get_center method --- asteca/cluster.py | 224 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 169 insertions(+), 55 deletions(-) diff --git a/asteca/cluster.py b/asteca/cluster.py index 98a39469..34197dca 100644 --- a/asteca/cluster.py +++ b/asteca/cluster.py @@ -3,40 +3,45 @@ import numpy as np import pandas as pd import matplotlib.pyplot as plt +from .modules import cluster_priv as cp @dataclass class cluster: - r"""Define a ``cluster`` object. - - Parameters - ---------- - cluster_df : pd.DataFrame - `pandas DataFrame `_ - with the cluster's loaded data - magnitude : str - Name of the DataFrame column that contains the magnitude - e_mag : str - Name of the DataFrame column that contains the magnitude's uncertainty - color : str - Name of the DataFrame column that contains the color - e_color : str - Name of the DataFrame column that contains the color's uncertainty - ra: str, optional, default=None - Name of the DataFrame column that contains the right ascension (RA) - dec: str, optional, default=None - Name of the DataFrame column that contains the declination (DEC) - plx: str, optional, default=None - Name of the DataFrame column that contains the parallax - pmra: str, optional, default=None - Name of the DataFrame column that contains the RA proper motion - pmde: str, optional, default=None - Name of the DataFrame column that contains the DEC proper motion - color2: str, optional, default=None - Name of the DataFrame column that contains the second color - e_color2: str, optional, default=None - Name of the DataFrame column that contains the second color's uncertainty + """Define a :class:`cluster` object. + :param cluster_df: `pandas DataFrame `__ + with the cluster's loaded data. + :type cluster_df: pd.DataFrame + :param magnitude: Name of the DataFrame column that contains the magnitude + :type magnitude: str + :param e_mag: Name of the DataFrame column that contains the magnitude's uncertainty + :type e_mag: str + :param color: Name of the DataFrame column that contains the color + :type color: str + :param e_color: Name of the DataFrame column that contains the color's uncertainty + :type e_color: str + :param ra: Name of the DataFrame column that contains the right ascension (RA), + defaults to ``None`` + :type ra: str, optional + :param dec: Name of the DataFrame column that contains the declination (DEC), + defaults to ``None`` + :type dec: str, optional + :param plx: Name of the DataFrame column that contains the parallax, + defaults to ``None`` + :type plx: str, optional + :param pmra: Name of the DataFrame column that contains the RA proper motion, + defaults to ``None`` + :type pmra: str, optional + :param pmde: Name of the DataFrame column that contains the DEC proper motion, + defaults to ``None`` + :type pmde: str, optional + :param color2: Name of the DataFrame column that contains the second color, + defaults to ``None`` + :type color2: str, optional + :param e_color2: Name of the DataFrame column that contains the second color's + uncertainty, defaults to ``None`` + :type e_color2: str, optional """ cluster_df: pd.DataFrame @@ -49,17 +54,19 @@ class cluster: plx: Optional[str] = None pmra: Optional[str] = None pmde: Optional[str] = None + e_plx: Optional[str] = None + e_pmra: Optional[str] = None + e_pmde: Optional[str] = None color2: Optional[str] = None e_color2: Optional[str] = None def __post_init__(self): - # Load photometry + """Load photometry.""" self._load() def _load(self): - """ - The photometry is store with a '_p' to differentiate from the self.magnitude, - self.color, etc that are defined with the class is called. + """The photometry is store with a '_p' to differentiate from the + self.magnitude, self.color, etc that are defined with the class is called. """ print("Instantiating cluster...") @@ -73,10 +80,23 @@ def _load(self): if self.e_color2 is not None: self.e_colors_p.append(np.array(self.cluster_df[self.e_color2])) + self.ra_v, self.dec_v = None, None if self.ra is not None: self.ra_v = self.cluster_df[self.ra] self.dec_v = self.cluster_df[self.dec] + self.pmra_v, self.e_pmra_v, self.pmde_v, self.pmde_v = None, None, None, None + if self.pmra is not None: + self.pmra_v = self.cluster_df[self.pmra].values + self.pmde_v = self.cluster_df[self.pmde].values + self.e_pmra_v = self.cluster_df[self.e_pmra].values + self.e_pmde_v = self.cluster_df[self.e_pmde].values + + self.plx_v, self.e_plx_v = None, None + if self.plx is not None: + self.plx_v = self.cluster_df[self.plx].values + self.e_plx_v = self.cluster_df[self.e_plx].values + print(f"N_stars : {len(self.mag_p)}") print(f"Magnitude : {self.magnitude}") print(f"Color : {self.color}") @@ -85,13 +105,10 @@ def _load(self): print("Cluster object generated\n") def radecplot(self): - r"""Generate a (RA, DEC) plot. - - Returns - ------- - matplotlib.axis - Matplotlib axis object + """Generate a (RA, DEC) plot. + :return: Matplotlib axis object + :rtype: matplotlib.axis """ ra = self.ra_v # cluster_df[self.ra].values dec = self.dec_v # cluster_df[self.dec].values @@ -118,23 +135,18 @@ def radecplot(self): return ax def clustplot(self, ax, color_idx=0, binar_probs=None): - r"""Generate a color-magnitude plot. - - Parameters - ---------- - ax : matplotlib.axis, optional, default=None - Matplotlib axis where to draw the plot - color_idx : int, default=0 - Index of the color to plot. If ``0`` (default), plot the first color. If - ``1`` plot the second color. - binar_probs : numpy.array, optional, default=None - Array with probabilities of being a binary system for each observed star - - Returns - ------- - matplotlib.axis - Matplotlib axis object + """Generate a color-magnitude plot. + :param ax: Matplotlib axis where to draw the plot + :type ax: matplotlib.axis, optional, default=None + :param color_idx: Index of the color to plot. If ``0`` (default), plot the + first color. If ``1`` plot the second color. + :type color_idx: int, default=0 + :param binar_probs: Array with probabilities of being a binary system for each + observed star + :type binar_probs: numpy.array, optional, default=None + :return: Matplotlib axis object + :rtype: matplotlib.axis """ if color_idx > 1: raise ValueError( @@ -183,3 +195,105 @@ def clustplot(self, ax, color_idx=0, binar_probs=None): ax.legend() return ax + + def get_center( + self, + method="knn_5d", + xy_c=None, + vpd_c=None, + plx_c=None, + N_cluster=None, + N_clust_min=25, + ): + """Estimate center coordinates for the cluster + + Use the available data (ra, dec, pmra, pmde, plx) to estimate a cluster's + center coordinates as the point of maximum density. Methods: + + ``knn_5d``: Estimates the center value as the median position of the k + (k=N_clust_min) nearest stars to an estimate of the center in proper motions + and (ra, dec, plx), if given. All 5 dimensions of data must be available. + + :param method: Method used to estimate center values, one of (``knn_5d``), + defaults to ``knn_5d`` + :type method: str + :param xy_c: Estimated value for the (RA, DEC) center, defaults to ``None`` + :type xy_c: tuple(float, float), optional + :param vpd_c: Estimated value for the (pmRA, pmDE) center, defaults to ``None`` + :type vpd_c: tuple(float, float), optional + :param plx_c: Estimated value for the plx center, defaults to ``None`` + :type plx_c: float, optional + :param N_cluster: Estimated number of members, defaults to ``None`` + :type N_cluster: int, optional + :param N_clust_min: Minimum number of cluster members, defaults to ``25`` + :type N_clust_min: int, optional + + :return: Dictionary of center coordinates + :rtype: dict + """ + + if method == "knn_5d": + if any( + [ + _ is None + for _ in ( + self.pmra_v, + self.pmde_v, + self.plx_v, + self.e_pmra_v, + self.e_pmde_v, + self.e_plx_v, + ) + ] + ): + raise ValueError( + f"Method '{method}' requires (ra, dec, pmra, pmde, plx) data and " + + "their uncertainties" + ) + + # To galactic coordinates + glon, glat = cp.radec2lonlat(self.ra_v.values, self.dec_v.values) + X = np.array( + [ + glon, + glat, + self.pmra_v, + self.pmde_v, + self.plx_v, + self.e_pmra_v, + self.e_pmde_v, + self.e_plx_v, + ] + ) + # Reject nan values and extract clean data + _, X_no_nan = cp.reject_nans(X) + lon, lat, pmRA, pmDE, plx, _, _, _ = X_no_nan + + x_c, y_c, pmra_c, pmde_c, plx_c = cp.get_5D_center( + lon, + lat, + pmRA, + pmDE, + plx, + xy_c=xy_c, + vpd_c=vpd_c, + plx_c=plx_c, + N_cluster=N_cluster, + N_clust_min=N_clust_min, + ) + ra_c, dec_c = cp.lonlat2radec(x_c, y_c) + + print("\nCenter coordinates found:") + print("ra_c : {:.4f}".format(ra_c)) + print("dec_c : {:.4f}".format(dec_c)) + print("pmra_c : {:.3f}".format(pmra_c)) + print("pmde_c : {:.3f}".format(pmde_c)) + print("plx_c : {:.3f}".format(plx_c)) + + return { + "ra_c": ra_c, + "dec_c": dec_c, + "pmra_c": pmra_c, + "pmde_c": pmde_c, + "plx_c": plx_c, + } From 156aff91a98a8f577a938d30a70d715e35c677c9 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:17:54 -0300 Subject: [PATCH 21/29] update docstring --- asteca/isochrones.py | 110 ++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/asteca/isochrones.py b/asteca/isochrones.py index 172dcc02..fab2f069 100644 --- a/asteca/isochrones.py +++ b/asteca/isochrones.py @@ -7,63 +7,67 @@ @dataclass class isochrones: - r"""Define an ``isochrones`` object. + """Define an :class:`isochrones` object. This object contains the loaded theoretical isochrones used by the - :py:mod:`asteca.synthetic` class to generate synthetic clusters. + :py:class:`asteca.synthetic` class to generate synthetic clusters. See :ref:`isochronesload` for more details. - Parameters - ---------- - model : str, {"PARSEC", "MIST", "BASTI"} - The model must be one of the supported isochrone services: - `PARSEC `_, - `MIST `_, or - `BASTI `_. - isochs_path : str - Path to the folder that contains the files for the theoretical isochrones. - magnitude : str - Magnitude's filter name as defined in the theoretical isochrones. - Example for Gaia's ``G`` magnitude: ``"Gmag"``. - color : tuple - Tuple containing the filter names that generate the first color defined. - The correct format is: ``("filter1", "filter2")``, where ``filter1`` + :param model: The model must be one of the supported isochrone services: + `PARSEC `__, + `MIST `__, or + `BASTI `__. + :type model: str: ``PARSEC, MIST, BASTI`` + :param isochs_path: Path to the folder that contains the files for the theoretical + isochrones + :type isochs_path: str + :param magnitude: Magnitude's filter name as defined in the theoretical isochrones. + Example for Gaia's ``G`` magnitude: ``"Gmag"`` + :type magnitude: str + :param color: Tuple containing the filter names that generate the first color + defined. The correct format is: ``("filter1", "filter2")``, where ``filter1`` and ``filter2`` are the names of the filters that are combined to generate the color. The order is important because the color will be generated as: ``filter1-filter2``. Example for Gaia's 'BP-RP' color: - ``("G_BPmag", "G_RPmag")``. - color2 : tuple, optional, default=None - Optional second color to use in the analysis. Same format as that used by the - ``color`` parameter. - magnitude_effl : float, optional, default=None - Effective lambda (in Angstrom) for the magnitude filter. - color_effl : tuple, optional, default=None - Effective lambdas for the filters that make up the ``color`` defined in the - :py:mod:`asteca.isochrones` object. E.g.: ``(1111.11, 2222.22)`` where - ``1111.11`` and ``2222.22`` are the effective lambdas (in Angstrom) for each - filter, in the same order as ``color``. - color2_effl : tuple, optional, default=None - Same as ``color_effl`` but for a second (optional) color defined. - z_to_FeH : float, optional, default=None - If ``None``, the default ``z`` values in the isochrones will be used to - generate the synthetic clusters. If ``float``, it must represent the solar - metallicity for these isochrones. The metallicity values will then be converted - to ``[FeH]`` values, to be used by the :meth:`synthetic.generate()` method. - N_interp : int, default=2500 - Number of interpolation points used to ensure that all isochrones are the - same shape. - column_names : dict, optional, default=None - Column names for the initial mass, metallicity, and age for the photometric - system's isochrones files. Example: + ``("G_BPmag", "G_RPmag")`` + :type color: tuple + :param color2: Optional second color to use in the analysis. Same format as that + used by the ``color`` parameter, defaults to ``None`` + :type color2: tuple, optional + :param magnitude_effl: Effective lambda (in Angstrom) for the magnitude filter, + defaults to ``None`` + :type magnitude_effl: float, optional + :param color_effl: Effective lambdas for the filters that make up the ``color`` + defined in the :py:class:`asteca.isochrones` object. E.g.: + ``(1111.11, 2222.22)`` where ``1111.11`` and ``2222.22`` are the effective + lambdas (in Angstrom) for each filter, in the same order as ``color``, defaults + to ``None`` + :type color_effl: tuple, optional + :param color2_effl: Same as ``color_effl`` but for a second (optional) color + defined, defaults to ``None`` + :type color2_effl: tuple, optional + :param z_to_FeH: If ``None``, the default ``z`` values in the isochrones will be + used to generate the synthetic clusters. If ``float``, it must represent the + solar metallicity for these isochrones. The metallicity values will then be + converted to ``[FeH]`` values, to be used by the + :py:meth:`synthetic.generate() ` method, + defaults to ``None`` + :type z_to_FeH: float, optional + :param N_interp: Number of interpolation points used to ensure that all isochrones + are the same shape, defaults to ``2500`` + :type N_interp: int + :param column_names: Column names for the initial mass, metallicity, and age for + the photometric system's isochrones files. Example: ``{"mass_col": "Mini", "met_col": "Zini", "age_col": "logAge"}``. This dictionary is defined internally in `ASteCA` and should only be given by the user if the isochrone service changes its format and the `isochrones` - class fails to load the files. - parsec_rm_stage_9 : boll, optional, default=True - If the isochrones are PARSEC, this argument set to ``True`` will remove the - *post_AGB* stage (label=9) which are still - "`in preparation `_". - + class fails to load the files, defaults to ``None`` + :type column_names: dict, optional + :param parsec_rm_stage_9: If the isochrones are PARSEC, this argument set to + ``True`` will remove the *post_AGB* stage (label=9) which are still + "`in preparation `__", defaults + to ``True`` + :type parsec_rm_stage_9: bool, optional """ model: str @@ -130,7 +134,7 @@ def __post_init__(self): print("Isochrone object generated\n") def _func_z_to_FeH(self, z_to_FeH): - r"""Convert z to FeH""" + """Convert z to FeH""" feh = np.log10(self.met_age_dict["met"] / z_to_FeH) N_old = len(feh) round_n = 4 @@ -145,14 +149,12 @@ def _func_z_to_FeH(self, z_to_FeH): self.met_age_dict["met"] = feh_r def _min_max(self) -> tuple[float]: - r"""Return the minimum and maximum values for the metallicity and age defined + """Return the minimum and maximum values for the metallicity and age defined in the theoretical isochrones. - Returns - ------- - tuple[float] - Tuple of (minimum_metallicity, maximum_metallicity, minimum_age, maximum_age) - + :return: Tuple of (minimum_metallicity, maximum_metallicity, minimum_age, + maximum_age) + :rtype: tuple[float] """ zmin = self.met_age_dict["met"].min() zmax = self.met_age_dict["met"].max() From 83fb7904df1ecd88d6bd0ebf2973bdc3c9352308 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:18:11 -0300 Subject: [PATCH 22/29] update docstrings --- asteca/synthetic.py | 174 ++++++++++++++++++++------------------------ 1 file changed, 78 insertions(+), 96 deletions(-) diff --git a/asteca/synthetic.py b/asteca/synthetic.py index eb60e25b..821d18c3 100644 --- a/asteca/synthetic.py +++ b/asteca/synthetic.py @@ -11,36 +11,39 @@ @dataclass class synthetic: - r"""Define a ``synthetic`` object. + """Define a :class:`synthetic` object. - Use the isochrones loaded in the :py:mod:`asteca.isochrones` object to generate a - :py:mod:`asteca.synthetic` object. This object is used to generate synthetic clusters - given a :py:mod:`asteca.cluster` object and a set of input fundamental parameters - (metallicity, age, distance, extinction, etc.). + Use the isochrones loaded in the :py:class:`asteca.isochrones` object to generate a + :py:class:`asteca.synthetic` object. This object is used to generate synthetic + clusters given a :py:class:`asteca.cluster` object and a set of input fundamental + parameters (metallicity, age, distance, extinction, etc.). See the :ref:`synth_clusters` section for more details. - Parameters - ---------- - isochs : :class:`isochrones` - :py:mod:`asteca.isochrones` object with the loaded files for the theoretical isochrones. - ext_law : str, {"CCMO", "GAIADR3"}, default="CCMO" - Extinction law. If "*GAIADR3*" is selected, the magnitude and first color defined - in :class:`isochrones` and :class:`cluster` are assumed to be Gaia's - (E)DR3 **G** and **(BP-RP)** respectively. The second color (if defined) will - always be affected by the "*CCMO*" model. - DR_distribution : str, {"uniform", "normal"}, default="uniform" - Distribution function for the differential reddening. - IMF_name : str, {"salpeter_1955", "kroupa_2001", "chabrier_2014"}, default="chabrier_2014" - Name of the initial mass function used to populate the isochrones. - max_mass : int, default=100_000 - Maximum total initial mass. Should be large enough to allow generating as many - synthetic stars as observed stars. - gamma : str, float, {"D&K", "fisher_stepped", "fisher_peaked", "raghavan"}, default="D&K" - Distribution function for the mass ratio of the binary systems. - seed: int, optional, default=None - Random seed. If ``None`` a random integer will be generated and used. - + :param isochs: :py:class:`asteca.isochrones` object with the loaded files for the + theoretical isochrones + :type isochs: :class:`isochrones` + :param ext_law: Extinction law. if ``GAIADR3`` is selected, the magnitude and first + color defined in :py:class:`asteca.isochrones` and :py:class:`asteca.cluster` + are assumed to be Gaia's (E)DR3 **G** and **(BP-RP)** respectively. The second + color (if defined) will always be affected by the ``CCMO`` model, defaults to + ``CCMO`` + :type ext_law: str: ``CCMO, GAIADR3`` + :param DR_distribution: Distribution function for the differential reddening, + defaults to ``uniform`` + :type DR_distribution: str: ``uniform, normal`` + :param IMF_name: Name of the initial mass function used to populate the isochrones, + defaults to ``chabrier_2014`` + :type IMF_name: str: ``salpeter_1955, kroupa_2001, chabrier_2014`` + :param max_mass: Maximum total initial mass. Should be large enough to allow + generating as many synthetic stars as observed stars, defaults to ``100_000`` + :type max_mass: int + :param gamma: Distribution function for the mass ratio of the binary systems, + defaults to ``D&K`` + :type gamma: str: ``D&K, fisher_stepped, fisher_peaked, raghavan``, float + :param seed: Random seed. If ``None`` a random integer will be generated and used, + defaults to ``None`` + :type seed: int, optional """ isochs: isochrones @@ -126,25 +129,23 @@ def __post_init__(self): print("Synthetic clusters object generated\n") def calibrate(self, cluster, fix_params: dict = {}): - r"""Calibrate a :py:mod:`asteca.synthetic` object based on a - :py:mod:`asteca.cluster` object and a dictionary of fixed fundamental parameters - (``fix_params``). + """Calibrate a :py:class:`asteca.synthetic` object based on a + :py:class:`asteca.cluster` object and a dictionary of fixed fundamental + parameters (``fix_params``). Use the data obtained from your observed cluster stored in the - :py:mod:`asteca.cluster` object, to calibrate a :py:mod:`asteca.synthetic` + :py:class:`asteca.cluster` object, to calibrate a :py:class:`asteca.synthetic` object. Additionally, a dictionary of fixed fundamental parameters (metallicity, age, distance, extinction, etc.) can be passed. See the :ref:`synth_clusters` section for more details. - Parameters - ---------- - cluster : :class:`cluster` - :py:mod:`asteca.cluster` object with the processed data from your observed - cluster. - fix_params : dict, optional, default={} - Dictionary with the values for the fixed parameters (if any). - + :param cluster: :py:class:`asteca.cluster` object with the processed data from + your observed cluster + :type cluster: :py:class:`asteca.cluster` + :param fix_params: Dictionary with the values for the fixed parameters (if any), + defaults to ``{}`` + :type fix_params: dict, optional """ # Check that the number of colors match if self.isochs.color2_effl is not None and cluster.color2 is None: @@ -192,27 +193,21 @@ def calibrate(self, cluster, fix_params: dict = {}): # self._rm_low_masses(dm_min) def generate(self, fit_params: dict) -> np.ndarray: - r"""Generate a synthetic cluster. + """Generate a synthetic cluster. The synthetic cluster is generated according to the parameters given in the ``fit_params`` dictionary and the already calibrated - :py:mod:`asteca.synthetic` object. - - Parameters - ---------- - fit_params : dict - Dictionary with the values for the fundamental parameters that were **not** - included in the ``fix_params`` dictionary when the - :py:mod:`asteca.synthetic` object was calibrated - (:meth:`synthetic.calibrate()` method). + :py:class:`asteca.synthetic` object. - Returns - ------- - array[mag, c1, (c2)] - Return a ``np.array`` containing a synthetic cluster with the shape + :param fit_params: Dictionary with the values for the fundamental parameters + that were **not** included in the ``fix_params`` dictionary when the + :py:class:`asteca.synthetic` object was calibrated + (:meth:`synthetic.calibrate()` method). + :type fit_params: dict + :return: Return a ``np.array`` containing a synthetic cluster with the shape ``[mag, c1, (c2)]``, where ``mag`` is the magnitude dimension, and ``c1`` and ``c2`` (last one is optional) are the color dimension(s). - + :rtype: array[mag, c1, (c2)] """ # Return proper values for fixed parameters and parameters required @@ -283,31 +278,26 @@ def generate(self, fit_params: dict) -> np.ndarray: return synth_clust[: self.m_ini_idx] def synthplot(self, ax, fit_params, color_idx=0, isochplot=False): - r"""Generate a color-magnitude plot for a synthetic cluster. + """Generate a color-magnitude plot for a synthetic cluster. The synthetic cluster is generated using the fundamental parameter values given in the ``fit_params`` dictionary. - Parameters - ---------- - ax : matplotlib.axis, optional, default=None - Matplotlib axis where to draw the plot. - fit_params : dict - Dictionary with the values for the fundamental parameters that were **not** - included in the ``fix_params`` dictionary when the - :py:mod:`asteca.synthetic` object was calibrated + :param ax: Matplotlib axis where to draw the plot, defaults to ``None`` + :type ax: matplotlib.axis, optional + :param fit_params: Dictionary with the values for the fundamental parameters + that were **not** included in the ``fix_params`` dictionary when the + :py:class:`asteca.synthetic` object was calibrated (:meth:`synthetic.calibrate()` method). - color_idx : int, default=0 - Index of the color to plot. If ``0`` (default), plot the first color. If - ``1`` plot the second color. - isochplot : bool, default=False - If ``True``, the accompanying isochrone will be plotted. - - Returns - ------- - matplotlib.axis - Matplotlib axis object - + :type fit_params: dict + :param color_idx: Index of the color to plot. If ``0`` (default), plot the + first color. If ``1`` plot the second color. Defaults to ``0`` + :type color_idx: int + :param isochplot: If ``True``, the accompanying isochrone will be plotted, + defaults to ``False`` + :type isochplot: bool + :return: Matplotlib axis object + :rtype: matplotlib.axis """ if color_idx > 1: raise ValueError( @@ -371,28 +361,22 @@ def synthplot(self, ax, fit_params, color_idx=0, isochplot=False): return ax def masses_binary_probs(self, model, model_std): - r"""Estimate individual masses for the observed stars, along with their binary + """Estimate individual masses for the observed stars, along with their binary probabilities (if binarity was estimated). - Parameters - ---------- - model : dict - Dictionary with the values for the fundamental parameters that were **not** - included in the ``fix_params`` dictionary when the - :py:mod:`asteca.synthetic` object was calibrated - (:meth:`synthetic.calibrate()` method). - model_std : dict - Dictionary with the standard deviations for the fundamental parameters in - the ``model`` argument. - - Returns - ------- - pandas.DataFrame - Data frame containing per-star primary and secondary masses along with - their uncertainties, and their probability of being a binary system. - numpy.array - Distribution of total binary fraction values for the cluster. - + :param model: Dictionary with the values for the fundamental parameters that + were **not** included in the ``fix_params`` dictionary when the + :py:class:`asteca.synthetic` object was calibrated + (:py:meth:`synthetic.calibrate()` method) + :type model: dict + :param model_std: Dictionary with the standard deviations for the fundamental + parameters in the ``model`` argument + :type model_std: dict + :return: Data frame containing per-star primary and secondary masses along with + their uncertainties, and their probability of being a binary system + :rtype: pandas.DataFrame + :return: Distribution of total binary fraction values for the cluster + :rtype: numpy.array """ # Generate random models from the selected solution models = mb.ranModels(model, model_std, self.seed) @@ -458,9 +442,7 @@ def masses_binary_probs(self, model, model_std): return df, np.array(b_fr_all) def _get_masses(self, fit_params, model_std, ra_c, dec_c): - """ - Estimate the different total masses for the observed cluster - """ + """Estimate the different total masses for the observed cluster""" print("Estimating total initial and actual masses") # Generate random models from the selected solution models = mb.ranModels(fit_params, model_std, self.seed) From bed432029c3e5a0eebf8f588c490704fde4ac3e8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:18:22 -0300 Subject: [PATCH 23/29] update docstrings --- asteca/likelihood.py | 46 +++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/asteca/likelihood.py b/asteca/likelihood.py index 4029641a..e5c7809a 100644 --- a/asteca/likelihood.py +++ b/asteca/likelihood.py @@ -6,27 +6,25 @@ @dataclass class likelihood: - r"""Define a ``likelihood`` object. + """Define a :class:`likelihood` object. This object is used to assess how similar your observed cluster is, stored in a :py:class:`cluster` object, compared to a given synthetic cluster, generated by the :py:meth:`synthetic.generate()` method. - Parameters - ---------- - my_cluster : :class:`cluster` - :py:mod:`asteca.cluster` object with the loaded data for the observed cluster. - lkl_name : str, {"plr"}, default="plr" - Currently only the Poisson likelihood ratio defined in - `Tremmel et al. (2013) `_ - is accepted. - bin_method: str, {"knuth", "fixed", "bayes_blocks", "manual"}, default="knuth" - Bin method used to split the color-magnitude diagram into cells - (`Hess diagram `_). If ``manual`` + :param my_cluster: :py:class:`asteca.cluster` object with the loaded data for the + observed cluster + :type my_cluster: :class:`cluster` + :param lkl_name: Currently only the Poisson likelihood ratio defined in + `Tremmel et al. (2013) `__ + is accepted, defaults to ``plr`` + :type lkl_name: str + :param bin_method: Bin method used to split the color-magnitude diagram into cells + (`Hess diagram `__). If ``manual`` is selected, a list containing an array of edge values for the magnitude, followed by one or two arrays (depending on the number of colors defined) for - the color(s), also with edge values. - + the color(s), also with edge values, defaults to ``knuth`` + :type bin_method: str: ``knuth, fixed, bayes_blocks, manual`` """ my_cluster: cluster @@ -58,21 +56,17 @@ def __post_init__(self): print("Likelihood object generated\n") def get(self, synth_clust): - r"""Evaluate the selected likelihood function. + """Evaluate the selected likelihood function. - Parameters - ---------- - synth_clust : array - ``np.array`` containing the synthetic cluster. The shape of this array must - be: ``[magnitude, color1, (color2)]``, where ``magnitude`` and ``color`` - are arrays with the magnitude and color photometric data (``color2`` is the - optional second color defined). - Returns - ------- - float - Likelihood value. + :param synth_clust: Numpy array containing the synthetic cluster. The shape of + this array must be: ``[magnitude, color1, (color2)]``, where ``magnitude`` + and ``color`` are arrays with the magnitude and color photometric data + (``color2`` is the optional second color defined) + :type synth_clust: array + :return: Likelihood value + :rtype: float """ if self.lkl_name == "plr": return lpriv.tremmel(self, synth_clust) From f43347d416883127561644aee58ea4ae8daf939f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:18:43 -0300 Subject: [PATCH 24/29] minor ruff format --- asteca/modules/isochrones_priv.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/asteca/modules/isochrones_priv.py b/asteca/modules/isochrones_priv.py index 20ea536f..35af23f7 100644 --- a/asteca/modules/isochrones_priv.py +++ b/asteca/modules/isochrones_priv.py @@ -120,7 +120,6 @@ def read( isochrones = {} for file_path in f_paths: - # Extract columns names and full header col_names, full_header = get_header(file_path) @@ -136,7 +135,6 @@ def read( # Group by age age_blocks = met_df.groupby(age_col, sort=False) for _, df in age_blocks: - zinit = df[met_col].values[0] try: isochrones[zinit] @@ -163,7 +161,6 @@ def read( # Process age blocks for _, df in df_blocks: - age = df[age_col].values[0] # Interpolated isochrone isochrones = interp_df( From 72e5ef0410a094fc9a6024fcd18c453e071c6be4 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:19:10 -0300 Subject: [PATCH 25/29] add membership.fastmp method --- asteca/__init__.py | 1 + asteca/membership.py | 131 ++++++++++++++++++++ asteca/modules/cluster_priv.py | 71 ++++++----- asteca/modules/{membership.py => fastmp.py} | 16 +-- 4 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 asteca/membership.py rename asteca/modules/{membership.py => fastmp.py} (98%) diff --git a/asteca/__init__.py b/asteca/__init__.py index c62f960e..8d60f5f8 100644 --- a/asteca/__init__.py +++ b/asteca/__init__.py @@ -1,4 +1,5 @@ from .cluster import cluster as cluster +from . import membership as membership from .isochrones import isochrones as isochrones from .synthetic import synthetic as synthetic from .likelihood import likelihood as likelihood diff --git a/asteca/membership.py b/asteca/membership.py new file mode 100644 index 00000000..c2b37714 --- /dev/null +++ b/asteca/membership.py @@ -0,0 +1,131 @@ +import numpy as np +from .modules import cluster_priv as cp +from .modules.fastmp import fastMP + + +def fastmp( + cluster, + xy_c=None, + vpd_c=None, + plx_c=None, + fixed_centers=False, + N_cluster=None, + N_clust_min=25, + N_clust_max=5000, + centers_ex=None, + N_resample=1000, +): + """Assign membership probabilities. + + Estimate the probability of being a true cluster member for all observed + stars using the fastMP algorithm. The algorithm was described in detail in + the `article `__ + were we introduced the + `Unified Cluster Catalogue (UCC) `__. + + :param cluster: :py:class:`asteca.cluster` object with the loaded data for the + observed cluster + :type cluster: :py:class:`asteca.cluster` + :param xy_c: Estimated value for the (RA, DEC) center, defaults to ``None`` + :type xy_c: tuple(float, float), optional + :param vpd_c: Estimated value for the (pmRA, pmDE) center, defaults to ``None`` + :type vpd_c: tuple(float, float), optional + :param plx_c: Estimated value for the plx center, defaults to ``None`` + :type plx_c: float, optional + :param fixed_centers: If ``True`` any center estimate (xy_c, vpd_c, plx_c) + given will be kept fixed throughout the process, defaults to ``False`` + :type fixed_centers: bool + :param N_cluster: Estimated number of members, defaults to ``None`` + :type N_cluster: int, optional + :param N_clust_min: Minimum number of cluster members, defaults to ``25`` + :type N_clust_min: int, optional + :param N_clust_max: Maximum number of cluster members, defaults to ``5000`` + :type N_clust_max: int, optional + :param centers_ex: List of dictionaries, one dictionary for each object that + shares the frame with the cluster and that should be ignored. The + dictionaries must have at most three keys, 'xy', 'pms', 'plx', each with a + list containing the center value(s) in those dimensions. Defaults to + ``None``. Examples: + one object: + ``[{'xy': [105.39, 0.9], 'plx': [1.3]}]``, + two objects: + ``[{'xy': [105.39, 0.9], 'pms': [3.5, -0.7]}, + {'xy': [0.82, -4.5], 'pms': [3.5, -0.7], 'plx': [3.5]}]`` + :type centers_ex: list(dict), optional + :param N_resample: Maximum number of resamples, defaults to ``1000`` + :type N_resample: int, optional + + :return: Membership probabilities for all stars in the frame + :rtype: np.array + """ + if any( + [ + _ is None + for _ in ( + cluster.pmra_v, + cluster.pmde_v, + cluster.plx_v, + cluster.e_pmra_v, + cluster.e_pmde_v, + cluster.e_plx_v, + ) + ] + ): + raise ValueError( + "fastMP requires (ra, dec, pmra, pmde, plx) data and\n" + + "their uncertainties to be present in the 'cluster' object" + ) + for k, N in { + "N_cluster": N_cluster, + "N_clust_min": N_clust_min, + "N_clust_max": N_clust_max, + "N_resample": N_resample, + }.items(): + if N is not None and not isinstance(N, int): + raise ValueError(f"{k}={N}, must be either 'None' or an 'int'") + if fixed_centers is True and xy_c is None and vpd_c is None and plx_c is None: + raise ValueError("fixed_centers=True but no center values were given") + + # Convert (RA, DEC) to (lon, lat) + glon, glat = cp.radec2lonlat(cluster.ra_v.values, cluster.dec_v.values) + if xy_c is not None: + xy_c = cp.radec2lonlat(xy_c[0], xy_c[1]) + + # Generate input data array for fastMP + X = np.array( + [ + glon, + glat, + cluster.pmra_v, + cluster.pmde_v, + cluster.plx_v, + cluster.e_pmra_v, + cluster.e_pmde_v, + cluster.e_plx_v, + ] + ) + + print("\nRunning fastMP...") + print(f"fixed_centers : {fixed_centers}") + print(f"N_cluster : {N_cluster}") + print(f"N_clust_min : {N_clust_min}") + print(f"N_clust_max : {N_clust_max}") + centers_ex_flag = False + if centers_ex is not None: + centers_ex_flag = True + print(f"centers_ex : {centers_ex_flag}") + print(f"N_resample : {N_resample}") + probs = fastMP( + X, + xy_c, + vpd_c, + plx_c, + fixed_centers, + N_cluster, + N_clust_min, + N_clust_max, + centers_ex, + N_resample, + ) + + return probs diff --git a/asteca/modules/cluster_priv.py b/asteca/modules/cluster_priv.py index eaaeaefb..7b9e9e28 100644 --- a/asteca/modules/cluster_priv.py +++ b/asteca/modules/cluster_priv.py @@ -13,7 +13,7 @@ def radec2lonlat(ra, dec): def lonlat2radec(lon, lat): - gc = SkyCoord(l=lon * u.degree, b=lat * u.degree, frame='galactic') + gc = SkyCoord(l=lon * u.degree, b=lat * u.degree, frame="galactic") ra, dec = gc.fk5.ra.value, gc.fk5.dec.value return ra, dec @@ -36,18 +36,34 @@ def reject_nans(data): return idx_clean, data.T[msk_accpt].T +def get_Nd_dists(cents, data, dists_flag=False): + """Obtain indexes and distances of stars to the given center""" + # Distances to center + dist_Nd = spatial.distance.cdist(data, cents).T[0] + if dists_flag: + # Return the distances + return dist_Nd + + # Indexes that sort the distances + d_idxs = dist_Nd.argsort() + # Return the indexes that sort the distances + return d_idxs + + def get_5D_center( lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cluster, N_clust_min, N_cent=500 ): """ - Estimate the 5-dimensional center of a cluster. Steps: + Estimate the 5-dimensional center of a cluster. + + Steps: - 1. Estimate the center in PMs (the value can be given as input) - 2. Using this center with any other given center, obtain the - 'N_cent' stars closest to the combined center. - 3. Estimate the 5-dimensional final center using KDE + 1. Keep only 'N_cent' stars if xy_c or plx_c are given + 2. (Re)Estimate the center in PMs (the value can be given as input) + 3. Obtain the 'N_cent' stars closest to the available center values + 4. Estimate the 5-dimensional final center using kNN - N_cent: how many stars are used to estimate the KDE center. + N_cent: estimated number of members """ # Re-write if this parameter is given @@ -74,26 +90,16 @@ def get_5D_center( return x_c, y_c, pmra_c, pmde_c, plx_c -def get_Nd_dists(cents, data, dists_flag=False): - """Obtain indexes and distances of stars to the given center""" - # Distances to center - dist_Nd = spatial.distance.cdist(data, cents).T[0] - if dists_flag: - # Return the distances - return dist_Nd - - # Indexes that sort the distances - d_idxs = dist_Nd.argsort() - # Return the indexes that sort the distances - return d_idxs - - def filter_pms_stars(xy_c, plx_c, lon, lat, pmRA, pmDE, plx, N_cent): - """ """ - # Distances to xy_c+plx_c centers, if any was given + """If either xy_c or plx_c values are given, select the 'N_cent' stars + closest to this 1D/2D/3D center, and return their proper motions. + """ + + # Distances to xy_c+plx_c centers if xy_c is None and plx_c is None: return pmRA, pmDE + # Create arrays with required shape if xy_c is None and plx_c is not None: cent = np.array([[plx_c]]) data = np.array([plx]).T @@ -136,7 +142,7 @@ def get_pms_center(vpd_c, N_clust_min, pmRA, pmDE, N_bins=50, zoom_f=4, N_zoom=1 if vpd_c is not None: # Store the auto center for later cxm, cym = cx, cy - # Use the manual to zoom in + # Use the manual values to zoom in cx, cy = vpd_c # Zoom in @@ -189,11 +195,10 @@ def get_stars_close_center(lon, lat, pmRA, pmDE, plx, xy_c, vpd_c, plx_c, N_cent def get_kNN_center(N_clust_min, data): - """ - Estimate 5D center with kNN. Better results are obtained not using - the parallax data - """ - data_noplx = data[:, :4] + """Estimate 5D center with kNN.""" + + # Better results are obtained not using the parallax data? + data_noplx = data[:, :4] # <-- HARDCODED tree = spatial.cKDTree(data_noplx) inx = tree.query(data_noplx, k=N_clust_min + 1) @@ -203,10 +208,10 @@ def get_kNN_center(N_clust_min, data): # Sort by largest density idxs = np.argsort(-dens) - # Use the star with the largest density + # # Use the single star with the largest density # cent = np.array([data[idxs[0]]])[0] - # # Use median of stars with largest densities + + # Use median of 'N_clust_min' stars with largest densities cent = np.median(data[idxs[:N_clust_min]], 0) - x_c, y_c, pmra_c, pmde_c, plx_c = cent - return x_c, y_c, pmra_c, pmde_c, plx_c + return cent diff --git a/asteca/modules/membership.py b/asteca/modules/fastmp.py similarity index 98% rename from asteca/modules/membership.py rename to asteca/modules/fastmp.py index 8e99b123..49e005c6 100644 --- a/asteca/modules/membership.py +++ b/asteca/modules/fastmp.py @@ -2,6 +2,7 @@ import numpy as np from astropy.stats import RipleysKEstimator from scipy import spatial + # from scipy.stats import gaussian_kde # NEEDS TEST, 05/24 from . import cluster_priv as cp @@ -19,17 +20,6 @@ def fastMP( N_resample, ): """ """ - print("\nRunning fastMP...") - print(f"fixed_centers : {fixed_centers}") - print(f"N_cluster : {N_cluster}") - print(f"N_clust_min : {N_clust_min}") - print(f"N_clust_max : {N_clust_max}") - centers_ex_flag = False - if centers_ex is not None: - centers_ex_flag = True - print(f"centers_ex : {centers_ex_flag}") - print(f"N_resample : {N_resample}") - # HARDCODED N_break = max(50, int(N_resample * 0.05)) # HARDCODED @@ -230,7 +220,9 @@ def prep_extra_cl_dict(centers_ex): The parameter 'centers_ex' must be a list of dictionaries, one dictionary for each extra cluster in the frame. The dictionaries must have at most three keys, 'xy', 'pms', 'plx', each with a list - containing the center value(s) in those dimensions. Example: + containing the center value(s) in those dimensions. + + Examples: centers_ex = [{'xy': [105.39, 0.9], 'plx': [1.3]}] From fa07885209016a0f583320bae6e6fc639db4c163 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:20:38 -0300 Subject: [PATCH 26/29] update changelog --- docs/CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 69b96974..f10995c7 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -1,5 +1,13 @@ .. :changelog: +`[v0.5.3] `__ - 2024-05-26 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +- Added the :py:meth:`asteca.membership.fastmp` method +- Added the :py:meth:`asteca.cluster.cluster.get_center` method + + + `[v0.5.2] `__ - 2024-05-07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ From 5aadb766950a389640a9f500eb5f97b2c4453a68 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:21:39 -0300 Subject: [PATCH 27/29] updt date and version number --- docs/index.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 5eb6ee87..b927fa50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,7 @@ extinction, distance, metallicity, age, binarity, mass, etc.. .. important:: - Version |ProjectVersion| released on the 7th of May, 2024. See :ref:`changelog` + Version |ProjectVersion| released on the 26th of May, 2024. See :ref:`changelog` for a detailed list of the changes implemented. diff --git a/pyproject.toml b/pyproject.toml index 28c94be9..ca3a0756 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.2" +version = "0.5.3" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md" From 270aa5ed178cd328cd0cc3704759e09fd8cdf189 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:49:35 -0300 Subject: [PATCH 28/29] minor --- docs/contents/api.rst | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/contents/api.rst b/docs/contents/api.rst index 49fc5b97..7270d8a2 100644 --- a/docs/contents/api.rst +++ b/docs/contents/api.rst @@ -1,20 +1,6 @@ API Reference ============= -.. toctree:: - :titlesonly: - :maxdepth: 2 - - /../apidocs/asteca/asteca.cluster - /../apidocs/asteca/asteca.membership - /../apidocs/asteca/asteca.isochrones - /../apidocs/asteca/asteca.synthetic - /../apidocs/asteca/asteca.likelihood - - -Classes -~~~~~~~ - .. list-table:: :class: autosummary longtable :align: left @@ -35,3 +21,18 @@ Classes - .. autodoc2-docstring:: asteca.likelihood.likelihood :summary: + +Submodules +---------- + +.. toctree:: + :titlesonly: + :maxdepth: 1 + + /../apidocs/asteca/asteca.cluster + /../apidocs/asteca/asteca.membership + /../apidocs/asteca/asteca.isochrones + /../apidocs/asteca/asteca.synthetic + /../apidocs/asteca/asteca.likelihood + + From ed6e96b0e07d7d850decdbc928a371a2392ef687 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 26 May 2024 11:57:16 -0300 Subject: [PATCH 29/29] forgot to add commits, new version to fix --- docs/CHANGELOG.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index f10995c7..3981f3ce 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -1,5 +1,12 @@ .. :changelog: +`[v0.5.4] `__ - 2024-05-26 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +- Fix small issue with documentation in v0.5.3 + + + `[v0.5.3] `__ - 2024-05-26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/pyproject.toml b/pyproject.toml index ca3a0756..75a6a623 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asteca" -version = "0.5.3" +version = "0.5.4" description = "Stellar cluster analysis package" authors = ["Gabriel I Perren "] readme = "README.md"