asteroid_spinprops 1.1.1__tar.gz → 1.2.0__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.1
3
+ Version: 1.2.0
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
@@ -2,23 +2,27 @@ 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,
8
9
  func_hg1g2_with_spin,
9
10
  )
10
- from asteroid_spinprops.ssolib.periodest import get_period_estimate, get_multiterm_period_estimate, perform_residual_resampling
11
+ from asteroid_spinprops.ssolib.periodest import (
12
+ get_multiterm_period_estimate,
13
+ )
11
14
 
12
15
 
13
16
  def get_fit_params(
14
17
  data,
15
18
  flavor,
16
19
  shg1g2_constrained=True,
17
- blind_scan=True,
20
+ period_blind=True,
21
+ pole_blind=True,
18
22
  p0=None,
19
- survey_filter=None,
20
23
  alt_spin=False,
21
24
  period_in=None,
25
+ period_quality_flag=False,
22
26
  terminator=False,
23
27
  ):
24
28
  """
@@ -44,12 +48,12 @@ def get_fit_params(
44
48
  Model type to fit. Must be 'SHG1G2' or 'SOCCA'.
45
49
  shg1g2_constrained : bool, optional
46
50
  Whether to constrain the SOCCA fit using a prior SHG1G2 solution. Default True.
47
- blind_scan : bool, optional
48
- If True, perform a small grid search over initial pole positions and periods. Default True.
51
+ period_blind : bool, optional
52
+ If True, perform a small grid search over initial periods. Default True.
53
+ pole_blind : bool, optional
54
+ If True, perform a grid search over initial poles. Default True.
49
55
  p0 : list, optional
50
56
  Initial guess parameters for the fit. Required if `shg1g2_constrained=False`.
51
- survey_filter : str or None, optional
52
- If 'ZTF' or 'ATLAS', only data from that survey are used. Default None uses all data.
53
57
  alt_spin : bool, optional
54
58
  For SOCCA constrained fits, use the antipodal spin solution. Default False.
55
59
  period_in : float, optional
@@ -80,36 +84,26 @@ def get_fit_params(
80
84
  If `flavor` is not 'SHG1G2' or 'SOCCA'.
81
85
  """
82
86
 
83
- if survey_filter is None:
84
- filter_mask = np.array(data["cfid"].values[0]) >= 0
85
- if survey_filter == "ZTF":
86
- filter_mask = (np.array(data["cfid"].values[0]) == 1) | (
87
- np.array(data["cfid"].values[0]) == 2
88
- )
89
- if survey_filter == "ATLAS":
90
- filter_mask = (np.array(data["cfid"].values[0]) == 3) | (
91
- np.array(data["cfid"].values[0]) == 4
92
- )
93
87
  if flavor == "SHG1G2":
94
88
  if p0 is None:
95
89
  Afit = estimate_sso_params(
96
- magpsf_red=data["cmred"].values[0][filter_mask],
97
- sigmapsf=data["csigmapsf"].values[0][filter_mask],
98
- phase=np.radians(data["Phase"].values[0][filter_mask]),
99
- filters=data["cfid"].values[0][filter_mask],
100
- ra=np.radians(data["ra"].values[0][filter_mask]),
101
- dec=np.radians(data["dec"].values[0][filter_mask]),
90
+ magpsf_red=data["cmred"].values[0],
91
+ sigmapsf=data["csigmapsf"].values[0],
92
+ phase=np.radians(data["Phase"].values[0]),
93
+ filters=data["cfid"].values[0],
94
+ ra=np.radians(data["ra"].values[0]),
95
+ dec=np.radians(data["dec"].values[0]),
102
96
  model="SHG1G2",
103
97
  )
104
98
 
105
99
  if p0 is not None:
106
100
  Afit = estimate_sso_params(
107
- magpsf_red=data["cmred"].values[0][filter_mask],
108
- sigmapsf=data["csigmapsf"].values[0][filter_mask],
109
- phase=np.radians(data["Phase"].values[0][filter_mask]),
110
- filters=data["cfid"].values[0][filter_mask],
111
- ra=np.radians(data["ra"].values[0][filter_mask]),
112
- dec=np.radians(data["dec"].values[0][filter_mask]),
101
+ magpsf_red=data["cmred"].values[0],
102
+ sigmapsf=data["csigmapsf"].values[0],
103
+ phase=np.radians(data["Phase"].values[0]),
104
+ filters=data["cfid"].values[0],
105
+ ra=np.radians(data["ra"].values[0]),
106
+ dec=np.radians(data["dec"].values[0]),
113
107
  model="SHG1G2",
114
108
  p0=p0,
115
109
  )
@@ -118,11 +112,15 @@ def get_fit_params(
118
112
  if flavor == "SOCCA":
119
113
  if shg1g2_constrained is True:
120
114
  shg1g2_params = get_fit_params(
121
- data=data, flavor="SHG1G2", survey_filter=survey_filter
122
- )
123
- residuals_dataframe = make_residuals_df(
124
- data, model_parameters=shg1g2_params
115
+ data=data, flavor="SHG1G2"
125
116
  )
117
+ try:
118
+ residuals_dataframe = make_residuals_df(
119
+ data, model_parameters=shg1g2_params
120
+ )
121
+ except Exception:
122
+ SOCCA_opt = {"Failed at period search preliminary steps": 3}
123
+ return SOCCA_opt
126
124
  if period_in is None:
127
125
  # Period search boundaries (in days)
128
126
  pmin, pmax = 5e-2, 1e4
@@ -132,20 +130,31 @@ def get_fit_params(
132
130
  residuals_dataframe, p_min=pmin, p_max=pmax, k_free=True
133
131
  )
134
132
  )
133
+ if period_quality_flag:
134
+ _, Nbs = periodest.perform_residual_resampling(
135
+ resid_df=residuals_dataframe, p_min=pmin, p_max=pmax, k=int(k_val)
136
+ )
135
137
  except KeyError:
136
138
  # If more than 10 terms are required switch to fast rotator:
137
139
  pmin, pmax = 5e-3, 5e-2
138
-
139
- p_in, k_val, p_rms, signal_peaks, window_peaks = (
140
- get_multiterm_period_estimate(
141
- residuals_dataframe, p_min=pmin, p_max=pmax, k_free=True
140
+ try:
141
+ p_in, k_val, p_rms, signal_peaks, window_peaks = (
142
+ get_multiterm_period_estimate(
143
+ residuals_dataframe, p_min=pmin, p_max=pmax, k_free=True
144
+ )
142
145
  )
143
- )
146
+ if period_quality_flag:
147
+ _, Nbs = periodest.perform_residual_resampling(
148
+ resid_df=residuals_dataframe, p_min=pmin, p_max=pmax, k=int(k_val)
149
+ )
150
+ except Exception:
151
+ SOCCA_opt = {"Failed at period search after": 4}
152
+ return SOCCA_opt
144
153
  period_sy = p_in
145
154
  else:
146
155
  period_sy = period_in
147
156
 
148
- if blind_scan is True:
157
+ if period_blind is True:
149
158
  rms = []
150
159
  model = []
151
160
 
@@ -155,9 +164,54 @@ def get_fit_params(
155
164
 
156
165
  ra0, dec0 = shg1g2_params["alpha0"], shg1g2_params["delta0"]
157
166
 
158
- ra_init, dec_init = utils.generate_initial_points(
159
- ra0, dec0, dec_shift=45
160
- )
167
+ if pole_blind is True:
168
+ ra_init, dec_init = utils.generate_initial_points(
169
+ ra0, dec0, dec_shift=45
170
+ )
171
+
172
+ else:
173
+ rarange = np.arange(0, 360, 10)
174
+ decrange = np.arange(-90, 90, 5)
175
+ rms_landscape = np.ones(shape=(len(rarange), len(decrange)))
176
+
177
+ for i, ra0 in enumerate(rarange):
178
+ for j, dec0 in enumerate(decrange):
179
+
180
+ all_residuals = []
181
+
182
+ for ff in np.unique(data["cfid"].values[0]):
183
+ cond_ff = data["cfid"].values[0] == ff
184
+
185
+ pha = [np.radians(data["Phase"].values[0][cond_ff]),
186
+ np.radians(data["ra"].values[0][cond_ff]),
187
+ np.radians(data["dec"].values[0][cond_ff])]
188
+
189
+ H = shg1g2_params[f"H_{ff}"]
190
+ G1 = shg1g2_params[f"G1_{ff}"]
191
+ G2 = shg1g2_params[f"G2_{ff}"]
192
+ R = shg1g2_params["R"]
193
+
194
+ C = func_hg1g2_with_spin(pha, H, G1, G2, R,
195
+ np.radians(ra0), np.radians(dec0))
196
+
197
+ O = data["cmred"].values[0][cond_ff]
198
+
199
+ all_residuals.append(O - C)
200
+
201
+ all_residuals = np.concatenate(all_residuals)
202
+ rms_landscape[j, i] = np.sqrt(np.mean(all_residuals**2))
203
+
204
+ interp_vals = utils.gaussian_interpolate(rms_landscape, factor=4, sigma=1.0)
205
+ ny, nx = interp_vals.shape
206
+ [rarange[0], rarange[-1], decrange[0], decrange[-1]]
207
+ ra_vals = np.linspace(rarange.min(), rarange.max(), nx)
208
+ dec_vals = np.linspace(decrange.min(), decrange.max(), ny)
209
+ ys, xs = utils.detect_local_minima(interp_vals)
210
+ ra_minima = ra_vals[xs]
211
+ dec_minima = dec_vals[ys]
212
+
213
+ ra_init = ra_minima
214
+ dec_init = dec_minima
161
215
 
162
216
  H_key = next(
163
217
  (f"H_{i}" for i in range(1, 7) if f"H_{i}" in shg1g2_params),
@@ -190,9 +244,26 @@ def get_fit_params(
190
244
  model.append(SOCCA)
191
245
  except Exception:
192
246
  continue
193
- rms = np.array(rms)
194
- SOCCA_opt = model[rms.argmin()]
195
-
247
+ try:
248
+ rms = np.array(rms)
249
+ SOCCA_opt = model[rms.argmin()]
250
+ if period_quality_flag:
251
+ try:
252
+ DeltaF1 = signal_peaks[1] - signal_peaks[2]
253
+ f_obs = 2 / period_sy
254
+ y_trumpet = utils.trumpet(
255
+ DeltaF1, 1, f_obs
256
+ )
257
+ alias_flag = (DeltaF1 - y_trumpet) * 100
258
+ if alias_flag < 1:
259
+ SOCCA_opt["Period_class"] = "true"
260
+ else:
261
+ SOCCA_opt["Period_class"] = "alias"
262
+ SOCCA_opt["Nbs"] = Nbs
263
+ except Exception:
264
+ SOCCA_opt["Period_class"] = "class_error"
265
+ except Exception:
266
+ SOCCA_opt = {"Failed at SOCCA inversion": 5}
196
267
  return SOCCA_opt
197
268
  else:
198
269
  period_si_t, alt_period_si_t, _ = utils.estimate_sidereal_period(
@@ -254,14 +325,14 @@ def get_fit_params(
254
325
 
255
326
  # Constrained Fit
256
327
  Afit = estimate_sso_params(
257
- data["cmred"].values[0][filter_mask],
258
- data["csigmapsf"].values[0][filter_mask],
259
- np.radians(data["Phase"].values[0][filter_mask]),
260
- data["cfid"].values[0][filter_mask],
261
- ra=np.radians(data["ra"].values[0][filter_mask]),
262
- dec=np.radians(data["dec"].values[0][filter_mask]),
263
- jd=data["cjd"].values[0][filter_mask],
264
- model="SSHG1G2", # We should call this SOCCA
328
+ data["cmred"].values[0],
329
+ data["csigmapsf"].values[0],
330
+ np.radians(data["Phase"].values[0]),
331
+ data["cfid"].values[0],
332
+ ra=np.radians(data["ra"].values[0]),
333
+ dec=np.radians(data["dec"].values[0]),
334
+ jd=data["cjd"].values[0],
335
+ model="SSHG1G2", # We should call this SOCCA
265
336
  p0=p0,
266
337
  )
267
338
  return Afit
@@ -272,28 +343,28 @@ def get_fit_params(
272
343
  if p0 is not None:
273
344
  if terminator:
274
345
  Afit = estimate_sso_params(
275
- data["cmred"].values[0][filter_mask],
276
- data["csigmapsf"].values[0][filter_mask],
277
- np.radians(data["Phase"].values[0][filter_mask]),
278
- data["cfid"].values[0][filter_mask],
279
- ra=np.radians(data["ra"].values[0][filter_mask]),
280
- dec=np.radians(data["dec"].values[0][filter_mask]),
281
- jd=data["cjd"].values[0][filter_mask],
346
+ data["cmred"].values[0],
347
+ data["csigmapsf"].values[0],
348
+ np.radians(data["Phase"].values[0]),
349
+ data["cfid"].values[0],
350
+ ra=np.radians(data["ra"].values[0]),
351
+ dec=np.radians(data["dec"].values[0]),
352
+ jd=data["cjd"].values[0],
282
353
  model="SSHG1G2",
283
354
  p0=p0,
284
355
  terminator=terminator,
285
- ra_s=np.radians(data["ra_s"].values[0][filter_mask]),
286
- dec_s=np.radians(data["dec_s"].values[0][filter_mask]),
356
+ ra_s=np.radians(data["ra_s"].values[0]),
357
+ dec_s=np.radians(data["dec_s"].values[0]),
287
358
  )
288
359
  else:
289
360
  Afit = estimate_sso_params(
290
- data["cmred"].values[0][filter_mask],
291
- data["csigmapsf"].values[0][filter_mask],
292
- np.radians(data["Phase"].values[0][filter_mask]),
293
- data["cfid"].values[0][filter_mask],
294
- ra=np.radians(data["ra"].values[0][filter_mask]),
295
- dec=np.radians(data["dec"].values[0][filter_mask]),
296
- jd=data["cjd"].values[0][filter_mask],
361
+ data["cmred"].values[0],
362
+ data["csigmapsf"].values[0],
363
+ np.radians(data["Phase"].values[0]),
364
+ data["cfid"].values[0],
365
+ ra=np.radians(data["ra"].values[0]),
366
+ dec=np.radians(data["dec"].values[0]),
367
+ jd=data["cjd"].values[0],
297
368
  model="SSHG1G2",
298
369
  p0=p0,
299
370
  terminator=terminator,
@@ -418,3 +418,57 @@ 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
+ if peak_diff_1 > 0 and f_obs > f_feat:
463
+ return 2 * f_feat
464
+
465
+ elif peak_diff_1 < 0 and f_obs < f_feat:
466
+ return -2 * f_obs
467
+
468
+ elif peak_diff_1 > 0 and f_obs < f_feat:
469
+ return 2 * f_obs
470
+
471
+ elif peak_diff_1 < 0 and f_obs > f_feat:
472
+ return -2 * f_feat
473
+
474
+ return 0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asteroid_spinprops"
3
- version = "1.1.1"
3
+ version = "1.2.0"
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"}