asteroid_spinprops 1.1.2__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asteroid_spinprops
3
- Version: 1.1.2
3
+ Version: 1.2.1
4
4
  Summary: Collection of tools used for fitting sHG1G2 and SOCCA photometric models to sparse asteroid photometry
5
5
  License: MIT
6
6
  Author: Odysseas
@@ -111,13 +111,13 @@ pdf["jd_ltc"] = pdf["cjd"] - pdf["Observer_SSO_distance_column"] / c_kmday # li
111
111
 
112
112
  Your input DataFrame must therefore include:
113
113
 
114
- - time of observation
115
- - PSF magnitude and uncertainty
116
- - filter ID
117
- - RA, Dec
118
- - phase angle
119
- - heliocentric distance (`Obj_Sun_LTC_km`)
120
- - observer-centric distance (`Range_LTC_km`)
114
+ - time of observation (JD)
115
+ - PSF magnitude and uncertainty
116
+ - filter ID
117
+ - RA, Dec (Degrees)
118
+ - phase angle
119
+ - heliocentric distance (AU)
120
+ - observer-centric distance (AU)
121
121
 
122
122
  The preprocessing step renames these fields to the Fink schema, computes reduced magnitudes, and applies the light-time correction to the observation timestamps.
123
123
 
@@ -138,10 +138,23 @@ numeric_filter = inv + 1
138
138
  pdf_s["cfid"].values[0] = numeric_filter
139
139
 
140
140
  # --- Data cleaning and filtering ---
141
- clean_data, errorbar_rejects = dataprep.errorbar_filtering(data=pdf_s, mlimit=0.7928)
141
+ clean_data, errorbar_rejects = dataprep.errorbar_filtering(data=pdf_s, mlimit=0.7928) # mag limit from the LCDB
142
142
  clean_data, projection_rejects = dataprep.projection_filtering(data=clean_data)
143
143
  clean_data, iterative_rejects = dataprep.iterative_filtering(data=clean_data)
144
144
 
145
+ # --- Fit SOCCA ---
146
+ SOCCA_params = modelfit.get_fit_params(
147
+ data=clean_data,
148
+ flavor="SOCCA",
149
+ shg1g2_constrained=True,
150
+ pole_blind=False,
151
+ period_blind=True,
152
+ period_in=None,
153
+ period_quality_flag=True
154
+ )
155
+
156
+
157
+ ## --- Or step-by-step --- ##
145
158
  # --- Fit SHG1G2 model ---
146
159
  shg1g2_params = modelfit.get_fit_params(
147
160
  data=clean_data,
@@ -167,13 +180,15 @@ _, Nbs = periodest.perform_residual_resampling(
167
180
  k=int(k_val)
168
181
  )
169
182
 
170
- # --- Fit SSHG1G2 (spin + multiband) model ---
183
+ # --- Fit SOCCA model ---
171
184
  SOCCA_params = modelfit.get_fit_params(
172
185
  data=clean_data,
173
186
  flavor="SSHG1G2",
174
187
  shg1g2_constrained=True,
175
- blind_scan=True,
188
+ period_blind=False,
189
+ pole_blind=False,
176
190
  period_in=p_in,
191
+ period_quality_flag=False
177
192
  )
178
193
  ```
179
194
 
@@ -83,13 +83,13 @@ pdf["jd_ltc"] = pdf["cjd"] - pdf["Observer_SSO_distance_column"] / c_kmday # li
83
83
 
84
84
  Your input DataFrame must therefore include:
85
85
 
86
- - time of observation
87
- - PSF magnitude and uncertainty
88
- - filter ID
89
- - RA, Dec
90
- - phase angle
91
- - heliocentric distance (`Obj_Sun_LTC_km`)
92
- - observer-centric distance (`Range_LTC_km`)
86
+ - time of observation (JD)
87
+ - PSF magnitude and uncertainty
88
+ - filter ID
89
+ - RA, Dec (Degrees)
90
+ - phase angle
91
+ - heliocentric distance (AU)
92
+ - observer-centric distance (AU)
93
93
 
94
94
  The preprocessing step renames these fields to the Fink schema, computes reduced magnitudes, and applies the light-time correction to the observation timestamps.
95
95
 
@@ -110,10 +110,23 @@ numeric_filter = inv + 1
110
110
  pdf_s["cfid"].values[0] = numeric_filter
111
111
 
112
112
  # --- Data cleaning and filtering ---
113
- clean_data, errorbar_rejects = dataprep.errorbar_filtering(data=pdf_s, mlimit=0.7928)
113
+ clean_data, errorbar_rejects = dataprep.errorbar_filtering(data=pdf_s, mlimit=0.7928) # mag limit from the LCDB
114
114
  clean_data, projection_rejects = dataprep.projection_filtering(data=clean_data)
115
115
  clean_data, iterative_rejects = dataprep.iterative_filtering(data=clean_data)
116
116
 
117
+ # --- Fit SOCCA ---
118
+ SOCCA_params = modelfit.get_fit_params(
119
+ data=clean_data,
120
+ flavor="SOCCA",
121
+ shg1g2_constrained=True,
122
+ pole_blind=False,
123
+ period_blind=True,
124
+ period_in=None,
125
+ period_quality_flag=True
126
+ )
127
+
128
+
129
+ ## --- Or step-by-step --- ##
117
130
  # --- Fit SHG1G2 model ---
118
131
  shg1g2_params = modelfit.get_fit_params(
119
132
  data=clean_data,
@@ -139,13 +152,15 @@ _, Nbs = periodest.perform_residual_resampling(
139
152
  k=int(k_val)
140
153
  )
141
154
 
142
- # --- Fit SSHG1G2 (spin + multiband) model ---
155
+ # --- Fit SOCCA model ---
143
156
  SOCCA_params = modelfit.get_fit_params(
144
157
  data=clean_data,
145
158
  flavor="SSHG1G2",
146
159
  shg1g2_constrained=True,
147
- blind_scan=True,
160
+ period_blind=False,
161
+ pole_blind=False,
148
162
  period_in=p_in,
163
+ period_quality_flag=False
149
164
  )
150
165
  ```
151
166
 
@@ -2,6 +2,7 @@ import numpy as np
2
2
  import pandas as pd
3
3
 
4
4
  import asteroid_spinprops.ssolib.utils as utils
5
+ import asteroid_spinprops.ssolib.periodest as periodest
5
6
 
6
7
  from fink_utils.sso.spins import (
7
8
  estimate_sso_params,
@@ -21,6 +22,7 @@ def get_fit_params(
21
22
  p0=None,
22
23
  alt_spin=False,
23
24
  period_in=None,
25
+ period_quality_flag=False,
24
26
  terminator=False,
25
27
  ):
26
28
  """
@@ -49,13 +51,16 @@ def get_fit_params(
49
51
  period_blind : bool, optional
50
52
  If True, perform a small grid search over initial periods. Default True.
51
53
  pole_blind : bool, optional
52
- If True, perform a grid search over initial poles. Default True.
54
+ If True, perform a grid search over 12 initial poles all over a sphere. Default True.
55
+ If False, produce the sHG1G2 rms error landscape and initialize SOCCA poles on its local minima
53
56
  p0 : list, optional
54
57
  Initial guess parameters for the fit. Required if `shg1g2_constrained=False`.
55
58
  alt_spin : bool, optional
56
59
  For SOCCA constrained fits, use the antipodal spin solution. Default False.
57
60
  period_in : float, optional
58
61
  Input synodic period (days) to override automatic estimation. Default None.
62
+ period_quality_flag : bool, optional
63
+ Provide bootstrap score, alias/true (0/1) flags and period fit rms for the period estimates
59
64
  terminator : bool, optional
60
65
  If True, include self-shading in the fit. Default False.
61
66
 
@@ -128,6 +133,10 @@ def get_fit_params(
128
133
  residuals_dataframe, p_min=pmin, p_max=pmax, k_free=True
129
134
  )
130
135
  )
136
+ if period_quality_flag:
137
+ _, Nbs = periodest.perform_residual_resampling(
138
+ resid_df=residuals_dataframe, p_min=pmin, p_max=pmax, k=int(k_val)
139
+ )
131
140
  except KeyError:
132
141
  # If more than 10 terms are required switch to fast rotator:
133
142
  pmin, pmax = 5e-3, 5e-2
@@ -137,6 +146,10 @@ def get_fit_params(
137
146
  residuals_dataframe, p_min=pmin, p_max=pmax, k_free=True
138
147
  )
139
148
  )
149
+ if period_quality_flag:
150
+ _, Nbs = periodest.perform_residual_resampling(
151
+ resid_df=residuals_dataframe, p_min=pmin, p_max=pmax, k=int(k_val)
152
+ )
140
153
  except Exception:
141
154
  SOCCA_opt = {"Failed at period search after": 4}
142
155
  return SOCCA_opt
@@ -153,16 +166,55 @@ def get_fit_params(
153
166
  )
154
167
 
155
168
  ra0, dec0 = shg1g2_params["alpha0"], shg1g2_params["delta0"]
156
-
169
+
157
170
  if pole_blind is True:
158
171
  ra_init, dec_init = utils.generate_initial_points(
159
172
  ra0, dec0, dec_shift=45
160
173
  )
161
174
 
162
175
  else:
163
- ra0_antipodal, dec0_antipodal = utils.flip_spin(ra0, dec0)
164
- ra_init = [ra0, ra0_antipodal]
165
- dec_init = [dec0, dec0_antipodal]
176
+ rarange = np.arange(0, 360, 10)
177
+ decrange = np.arange(-90, 90, 5)
178
+ rms_landscape = np.ones(shape=(len(rarange), len(decrange)))
179
+
180
+ for i, ra0 in enumerate(rarange):
181
+ for j, dec0 in enumerate(decrange):
182
+
183
+ all_residuals = []
184
+
185
+ for ff in np.unique(data["cfid"].values[0]):
186
+ cond_ff = data["cfid"].values[0] == ff
187
+
188
+ pha = [np.radians(data["Phase"].values[0][cond_ff]),
189
+ np.radians(data["ra"].values[0][cond_ff]),
190
+ np.radians(data["dec"].values[0][cond_ff])]
191
+
192
+ H = shg1g2_params[f"H_{ff}"]
193
+ G1 = shg1g2_params[f"G1_{ff}"]
194
+ G2 = shg1g2_params[f"G2_{ff}"]
195
+ R = shg1g2_params["R"]
196
+
197
+ C = func_hg1g2_with_spin(pha, H, G1, G2, R,
198
+ np.radians(ra0), np.radians(dec0))
199
+
200
+ Obs = data["cmred"].values[0][cond_ff]
201
+
202
+ all_residuals.append(Obs - C)
203
+
204
+ all_residuals = np.concatenate(all_residuals)
205
+ rms_landscape[j, i] = np.sqrt(np.mean(all_residuals**2))
206
+
207
+ interp_vals = utils.gaussian_interpolate(rms_landscape, factor=4, sigma=1.0)
208
+ ny, nx = interp_vals.shape
209
+ [rarange[0], rarange[-1], decrange[0], decrange[-1]]
210
+ ra_vals = np.linspace(rarange.min(), rarange.max(), nx)
211
+ dec_vals = np.linspace(decrange.min(), decrange.max(), ny)
212
+ ys, xs = utils.detect_local_minima(interp_vals)
213
+ ra_minima = ra_vals[xs]
214
+ dec_minima = dec_vals[ys]
215
+
216
+ ra_init = ra_minima
217
+ dec_init = dec_minima
166
218
 
167
219
  H_key = next(
168
220
  (f"H_{i}" for i in range(1, 7) if f"H_{i}" in shg1g2_params),
@@ -198,6 +250,23 @@ def get_fit_params(
198
250
  try:
199
251
  rms = np.array(rms)
200
252
  SOCCA_opt = model[rms.argmin()]
253
+ if period_quality_flag:
254
+ try:
255
+ DeltaF1 = signal_peaks[1] - signal_peaks[2]
256
+ f_obs = 2 / period_sy
257
+ y_trumpet = utils.trumpet(
258
+ DeltaF1, 1, f_obs
259
+ )
260
+ alias_flag = (DeltaF1 - y_trumpet) * 100
261
+ if alias_flag < 1:
262
+ SOCCA_opt["Period_class"] = 1 # True
263
+ else:
264
+ SOCCA_opt["Period_class"] = 0 # Alias
265
+ SOCCA_opt["Nbs"] = Nbs
266
+ except Exception:
267
+ SOCCA_opt["Period_class"] = -1 # Classification error
268
+ SOCCA_opt["prms"] = p_rms
269
+ SOCCA_opt["k_terms"] = k_val
201
270
  except Exception:
202
271
  SOCCA_opt = {"Failed at SOCCA inversion": 5}
203
272
  return SOCCA_opt
@@ -123,7 +123,7 @@ def get_period_estimate(residuals_dataframe, p_min=0.03, p_max=2):
123
123
 
124
124
 
125
125
  def get_multiterm_period_estimate(
126
- residuals_dataframe, p_min=0.03, p_max=2, k_free=True, k_val=None
126
+ residuals_dataframe, p_min=0.03, p_max=2, k_free=True, k_val=None, k_max=4
127
127
  ):
128
128
  """
129
129
  Estimate the period of a multiband time series using a multiband Lomb-Scargle model.
@@ -148,7 +148,8 @@ def get_multiterm_period_estimate(
148
148
  If True, automatically scan multiple base-term complexities to choose optimal model. Default True.
149
149
  k_val : int, optional
150
150
  Fixed number of base terms to use if `k_free=False`. Default None.
151
-
151
+ k_max : int
152
+ Maximum number of terms to use for the multiterm LS periodogram. Default 4
152
153
  Returns
153
154
  -------
154
155
  tuple
@@ -175,7 +176,7 @@ def get_multiterm_period_estimate(
175
176
  residuals = np.zeros(len(residuals_dataframe["filters"].values))
176
177
  bands = np.unique(residuals_dataframe["filters"].values)
177
178
  if k_free:
178
- for k in range(1, 11):
179
+ for k in range(1, k_max + 1):
179
180
  model = LombScargleMultiband(
180
181
  residuals_dataframe["jd"].values,
181
182
  residuals_dataframe["residuals"].values,
@@ -418,3 +418,73 @@ def generate_initial_points(ra, dec, dec_shift=45):
418
418
  ra_list.append(wrap_longitude(temp_ra + offset))
419
419
  dec_list.append(shifted_dec)
420
420
  return ra_list, dec_list
421
+
422
+ def gaussian_interpolate(data, factor=4, sigma=1.0):
423
+ """
424
+ Reproduce matplotlib's `interpolation="gaussian"` effect.
425
+ factor : upsampling factor
426
+ sigma : Gaussian smoothing strength
427
+ """
428
+ from scipy.ndimage import zoom, gaussian_filter
429
+
430
+ # Step 1: upsample (mimics interpolation grid)
431
+ up = zoom(data, factor, order=1) # bilinear before smoothing
432
+
433
+ # Step 2: apply gaussian smoothing
434
+ smoothed = gaussian_filter(up, sigma=sigma)
435
+
436
+ return smoothed
437
+
438
+ def detect_local_minima(arr):
439
+ import scipy.ndimage.filters as filters
440
+ import scipy.ndimage.morphology as morphology
441
+
442
+ # https://stackoverflow.com/questions/3684484/peak-detection-in-a-2d-array/3689710#3689710
443
+ # https://stackoverflow.com/questions/3986345/how-to-find-the-local-minima-of-a-smooth-multidimensional-array-in-numpy
444
+ """
445
+ Takes an array and detects the troughs using the local maximum filter.
446
+ Returns a boolean mask of the troughs (i.e. 1 when
447
+ the pixel's value is the neighborhood maximum, 0 otherwise)
448
+ """
449
+ neighborhood = morphology.generate_binary_structure(len(arr.shape),2)
450
+
451
+ footprint = np.ones((15, 15))
452
+ local_min = filters.minimum_filter(arr, footprint=footprint) == arr
453
+
454
+ background = (arr==0)
455
+ eroded_background = morphology.binary_erosion(
456
+ background, structure=neighborhood, border_value=1)
457
+
458
+ detected_minima = local_min ^ eroded_background
459
+ return np.where(detected_minima)
460
+
461
+ def trumpet(peak_diff_1, f_feat, f_obs):
462
+ """
463
+ Implementation of the alias/true flagging algorithm.
464
+
465
+ Parameters
466
+ ----------
467
+ peak_diff_1 : float
468
+ The difference between the 2nd and 3rd highest peaks of the periodogram
469
+ f_feat : float
470
+ The feature frequency.
471
+ f_obs : float
472
+ The 1st highest peak if the periodogram
473
+ Returns
474
+ -------
475
+ The assumed true frequency peak difference value if the peak is true,
476
+ otherwise zero.
477
+ """
478
+ if peak_diff_1 > 0 and f_obs > f_feat:
479
+ return 2 * f_feat
480
+
481
+ elif peak_diff_1 < 0 and f_obs < f_feat:
482
+ return -2 * f_obs
483
+
484
+ elif peak_diff_1 > 0 and f_obs < f_feat:
485
+ return 2 * f_obs
486
+
487
+ elif peak_diff_1 < 0 and f_obs > f_feat:
488
+ return -2 * f_feat
489
+
490
+ return 0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asteroid_spinprops"
3
- version = "1.1.2"
3
+ version = "1.2.1"
4
4
  description = "Collection of tools used for fitting sHG1G2 and SOCCA photometric models to sparse asteroid photometry"
5
5
  authors = [
6
6
  {name = "Odysseas",email = "odysseas.xenos@proton.me"}