diff --git a/bolides/astro_utils.py b/bolides/astro_utils.py index 8461d0c..e1a9441 100644 --- a/bolides/astro_utils.py +++ b/bolides/astro_utils.py @@ -7,6 +7,8 @@ from astropy.coordinates import ICRS, SkyCoord from astropy.time import Time from scipy.special import comb +from pytz import timezone +utc = timezone('UTC') def get_phase(datetime): """Get lunar phase (0.01=new moon just happened, 0.99=new moon about to happen)""" diff --git a/bolides/bdf.py b/bolides/bdf.py index ae9be7b..e674d3a 100644 --- a/bolides/bdf.py +++ b/bolides/bdf.py @@ -530,6 +530,9 @@ def filter_shower(self, shower=None, years=None, padding=1, sdf=None, exclude=Fa `~BolideDataFrame` The filtered `~BolideDataFrame` """ + + if not pd.api.types.is_datetime64_any_dtype(self.datetime): + self.datetime = pd.to_datetime(self.datetime, errors='coerce') if years is None: years = list(range(min(self.datetime).year-1, max(self.datetime).year+1)) elif type(years) is int: @@ -542,7 +545,7 @@ def filter_shower(self, shower=None, years=None, padding=1, sdf=None, exclude=Fa from . import ShowerDataFrame sdf = ShowerDataFrame() self._showers = sdf - + dates = sdf.get_dates(shower, years).datetime date_padding = timedelta(days=padding) date_ranges = [[d-date_padding, d+date_padding] for d in dates] @@ -553,6 +556,53 @@ def filter_shower(self, shower=None, years=None, padding=1, sdf=None, exclude=Fa else: good_locs = counts == np.zeros(len(counts)) return self[good_locs] + + def filter_out_all_showers(self, shower=None, exclude=True, sdf=None): + """Filter out all bolides that are part of any meteor shower. + + Parameters + ---------- + sdf: ShowerDataFrame + Optionally provide a ShowerDataFrame to use for filtering. + If not provided, the ShowerDataFrame is pulled from the IAU Meteor Data Center. + shower: str + The meteor shower to filter by. If None, all showers are used. + Can enter either IAU number, IAU 3-letter code, or full shower name. + Refer to the IAU Meteor Data center at https://www.ta3.sk/IAUC22DB/MDC2022/. + exclude: bool + Whether or not to exclude bolides around the given showers or keep only bolides in showers. Default is to exclude. + + Returns + ------- + `~BolideDataFrame` + The filtered `~BolideDataFrame` + """ + years = list(range(min(self.datetime).year-1, max(self.datetime).year+1)) + if sdf is None: + if hasattr(self, '_showers'): + sdf = self._showers + else: + from . import ShowerDataFrame + sdf = ShowerDataFrame() + self._showers = sdf + + + start = sdf.get_start_dates(shower, years).datetime + end = sdf.get_end_dates(shower, years).datetime + date_ranges = [ + [date_start, date_end] + for date_start, date_end in zip(start, end) + if not (pd.isna(date_start) or pd.isna(date_end)) + ] + print(date_ranges) + counts = np.sum(np.array([list(self.datetime.between(d[0], d[1])) for d in date_ranges]), axis=0) + if not exclude: + + good_locs = counts != np.zeros(len(counts)) + else: + # keep bolides that are not part of any shower + good_locs = counts == np.zeros(len(counts)) + return self[good_locs] def plot_detections(self, category=None, *args, **kwargs): """Plot detections of bolides. diff --git a/bolides/plotting.py b/bolides/plotting.py index ff98a85..37b6e90 100644 --- a/bolides/plotting.py +++ b/bolides/plotting.py @@ -163,7 +163,7 @@ def plot_scatter( if s is not None and hasattr(s, '__getitem__'): kwargs['s'] = s[idx] ax.scatter(x[idx], y[idx], color=scalarMap.to_rgba(num), label=label, **kwargs) - plt.legend() + plt.legend(fontsize=20) if coastlines: ax.coastlines() # plot coastlines @@ -343,10 +343,8 @@ def plot_density( filled_c = ax.contourf(x, y, z*mask, levels=levels[1:], transform=ccrs.PlateCarree(), **kwargs) - # make lines invisible - for c in filled_c.collections: - c.set_edgecolor('none') - c.set_linewidth(0.000000000001) + filled_c.set_edgecolor('none') + filled_c.set_linewidth(0.000000000001) if coastlines: ax.coastlines() # plot coastlines @@ -356,8 +354,10 @@ def plot_density( if boundary: add_boundary(ax, boundary, boundary_style) - plt.colorbar(filled_c, alpha=kwargs['alpha'], - label='bolide density (km$^{-2}$)') + cbar = plt.colorbar(filled_c, alpha=kwargs['alpha']) + + cbar.ax.tick_params(labelsize=15) + cbar.set_label(label='bolide density (km$^{-2}$)', size=20, weight='bold') if title is not None: plt.title(title) diff --git a/bolides/sdf.py b/bolides/sdf.py index 5dafbae..c3f4eba 100644 --- a/bolides/sdf.py +++ b/bolides/sdf.py @@ -1,6 +1,7 @@ import pandas as pd import requests import numpy as np +import warnings from . import ROOT_PATH @@ -14,7 +15,7 @@ class ShowerDataFrame(pd.DataFrame): Specifies the source for the initialized. Can be: - ``'established', 'all', 'working'``: initialize from different sets - of data offered at the IAU Meteor Data Center, https://www.ta3.sk/IAUC22DB/MDC2007/ + of data offered at the IAU Meteor Data Center, https://www.ta3.sk/IAUC22DB/MDC2022/ - ``'csv'``: initialize from a .csv file file : str @@ -32,7 +33,7 @@ def __init__(self, *args, **kwargs): if source == 'csv': df = pd.read_csv(file, index_col=0, keep_default_na=False, na_values='') else: - url = 'https://www.ta3.sk/IAUC22DB/MDC2007/Etc/stream'+source+'data.txt' + url = 'https://www.ta3.sk/IAUC22DB/MDC2022/Etc/stream'+source+'data2022.txt' r = requests.get(url) start_line = 0 for num, line in enumerate(r.text.splitlines()): @@ -41,12 +42,13 @@ def __init__(self, *args, **kwargs): break column_line = r.text.splitlines()[start_line-3] import re - columns = re.split(r" {2,}", column_line)[1:-1] + columns = re.split(r" {2,}", column_line)[1:-3] data_lines = r.text.splitlines()[start_line:] data = '\n'.join(data_lines) import io csv_io = io.StringIO(data) df = pd.read_csv(csv_io, sep="|", header=None) + df = df.iloc[:, :len(columns)] df.columns = columns for col in df.columns: if df[col].dtype == 'O': @@ -135,7 +137,7 @@ def plot_orbits(self, date='2000-01-01T12:00:00', orb = Orbit.from_classical(Sun, a, ecc, inc, raan, argp, nu, plane=Planes.EARTH_ECLIPTIC) except ValueError: continue - plotter.plot(orb, label=row['shower name']) + plotter.plot(orb, label=row['shower name-designation']) if use_3d: fig = plotter._figure @@ -203,6 +205,24 @@ def plot_orbits(self, date='2000-01-01T12:00:00', return plotter def get_dates(self, showers, years): + """ + Get the dates from solar longitude. + + Parameters + ---------- + showers : str or list of str + The meteor shower(s) to get the dates for. + years : int or list of int + The year(s) to get the dates for. + + Returns + ------- + pd.DataFrame + A DataFrame with the shower Codes, names, IAUNos, and the corresponding dates + for the specified meteor showers and years. + """ + if showers is None: + showers = self['shower name-designation'].unique() if type(years) is int: years = [years] if type(showers) in [str, int]: @@ -214,28 +234,167 @@ def get_dates(self, showers, years): elif len(showers[0]) == 3: col = 'Code' else: - col = 'shower name' + col = 'shower name-designation' sdf = self[self[col].isin(showers)] import warnings if len(sdf) == 0: warnings.warn('No showers with '+col+'in'+str(showers)+'.') sdf = sdf[sdf.activity == 'annual'] - sdf = sdf.dropna(subset=['LaSun']) + sdf = sdf.dropna(subset=['LoS']) num = len(sdf) sdf = pd.concat([sdf]*len(years), ignore_index=True) years = np.repeat(years, num) - lons = sdf.LaSun + lons = sdf.LoS from .astro_utils import sol_lon_to_datetime dts = [] for lon, year in zip(lons, years): dts.append(sol_lon_to_datetime(lon, year)) return pd.DataFrame({'Code': sdf.Code, - 'shower name': sdf['shower name'], + 'shower name': sdf['shower name-designation'], + 'IAUNo': sdf.IAUNo, + 'datetime': dts}) + + def get_start_dates(self, showers, years): + """Get the start dates of the meteor showers from solar longitude. + + Parameters + ---------- + showers : str or list of str + The meteor shower(s) to get the start dates for. + years : int or list of int + The year(s) to get the start dates for. + + Returns + ------- + pd.Series + A series with the start dates of the meteor showers. + """ + if showers is None: + showers = self['shower name-designation'].unique() + if type(years) is int: + years = [years] + if type(showers) in [str, int]: + showers = [showers] + showers = [str(s) for s in showers] + if showers[0].isdigit(): + col = 'IAUNo' + showers = [int(s) for s in showers] + elif len(showers[0]) == 3: + col = 'Code' + else: + col = 'shower name-designation' + sdf = self[self[col].isin(showers)] + import warnings + if len(sdf) == 0: + warnings.warn('No showers with '+col+'in'+str(showers)+'.') + + sdf = sdf[sdf.activity == 'annual'] + sdf = sdf.dropna(subset=['LoSb']) + num = len(sdf) + sdf = pd.concat([sdf]*len(years), ignore_index=True) + years = np.repeat(years, num) + + lons = sdf.LoSb + from .astro_utils import sol_lon_to_datetime + dts = [] + for lon, year in zip(lons, years): + if lon=='' or lon is None or pd.isna(lon): + dts.append(np.nan) + else: + dts.append(sol_lon_to_datetime(float(lon), year)) + return pd.DataFrame({'Code': sdf.Code, + 'shower name': sdf['shower name-designation'], + 'IAUNo': sdf.IAUNo, + 'datetime': dts}) + + def get_end_dates(self, showers, years): + """Get the end dates of the meteor showers from solar longitude. + + Parameters + ---------- + showers : str or list of str + The meteor shower(s) to get the start dates for. + years : int or list of int + The year(s) to get the start dates for. + + Returns + ------- + pd.Series + A series with the start dates of the meteor showers. + """ + if showers is None: + showers = self['shower name-designation'].unique() + if type(years) is int: + years = [years] + if type(showers) in [str, int]: + showers = [showers] + showers = [str(s) for s in showers] + if showers[0].isdigit(): + col = 'IAUNo' + showers = [int(s) for s in showers] + elif len(showers[0]) == 3: + col = 'Code' + else: + col = 'shower name-designation' + sdf = self[self[col].isin(showers)] + import warnings + if len(sdf) == 0: + warnings.warn('No showers with '+col+'in'+str(showers)+'.') + + sdf = sdf[sdf.activity == 'annual'] + sdf = sdf.dropna(subset=['LoSe']) + num = len(sdf) + sdf = pd.concat([sdf]*len(years), ignore_index=True) + years = np.repeat(years, num) + + lons = pd.to_numeric(sdf.LoSe, errors='coerce') + from .astro_utils import sol_lon_to_datetime + dts = [] + for lon, year in zip(lons, years): + if lon=='' or lon is None or pd.isna(lon): + dts.append(np.nan) + else: + dts.append(sol_lon_to_datetime(float(lon), year)) + return pd.DataFrame({'Code': sdf.Code, + 'shower name': sdf['shower name-designation'], 'IAUNo': sdf.IAUNo, 'datetime': dts}) + def fastest_showers(self, n=5): + """ + Return the top n shower Codes by mean Vg. + + Parameters + ----------- + n : int + The number of top showers to return based on their mean Vg. Maximum is 122. + + Returns + ------- + pd.DataFrame + A DataFrame with the shower Codes and their mean Vg, sorted by mean Vg in descending order. + """ + # Ensure Vg is numeric, coerce errors to NaN + sdf = self.copy() + sdf['Vg'] = pd.to_numeric(sdf['Vg'], errors='coerce') + sdf_sorted = sdf.sort_values('Code') + + mean_vgs = [] + codes = [] + for code, group in sdf_sorted.groupby('Code'): + codes.append(code) + mean_vgs.append(group['Vg'].mean()) + + result = pd.DataFrame({'Code': codes, 'mean_Vg': mean_vgs}) + + if len(result) < n: + warnings.warn('Maximum number of showers is ' + str(len(result))) + + result = result.sort_values('mean_Vg', ascending=False).head(n).reset_index(drop=True) + return result + def __setattr__(self, attr, val): from warnings import filterwarnings filterwarnings("ignore", message="Pandas doesn't allow columns to be created via a new attribute name") diff --git a/notebooks/tutorial.ipynb b/notebooks/tutorial.ipynb index c633f4d..3ed84ad 100644 --- a/notebooks/tutorial.ipynb +++ b/notebooks/tutorial.ipynb @@ -4423,7 +4423,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.13.2" } }, "nbformat": 4,