asteroid_spinprops 0.2.32__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. asteroid_spinprops/ssolib/dataprep.py +0 -346
  2. asteroid_spinprops/ssolib/modelfit.py +143 -252
  3. asteroid_spinprops/ssolib/periodest.py +126 -158
  4. asteroid_spinprops/ssolib/utils.py +164 -211
  5. asteroid_spinprops-1.0.1.dist-info/METADATA +186 -0
  6. asteroid_spinprops-1.0.1.dist-info/RECORD +10 -0
  7. asteroid_spinprops/ssolib/.ruff_cache/.gitignore +0 -2
  8. asteroid_spinprops/ssolib/.ruff_cache/0.13.2/1980339045096230685 +0 -0
  9. asteroid_spinprops/ssolib/.ruff_cache/CACHEDIR.TAG +0 -1
  10. asteroid_spinprops/ssolib/pipetools.py +0 -167
  11. asteroid_spinprops/ssolib/testing/atlas_x_ztf_testing/test_pqfile_1.parquet +0 -0
  12. asteroid_spinprops/ssolib/testing/atlas_x_ztf_testing/test_pqfile_2.parquet +0 -0
  13. asteroid_spinprops/ssolib/testing/ephemeris_testing/2000 WL152 +0 -1702
  14. asteroid_spinprops/ssolib/testing/ephemeris_testing/2001 PC +0 -94
  15. asteroid_spinprops/ssolib/testing/ephemeris_testing/2001 SG276 +0 -111
  16. asteroid_spinprops/ssolib/testing/ephemeris_testing/2008 GX32 +0 -93
  17. asteroid_spinprops/ssolib/testing/ephemeris_testing/2009 BE185 +0 -130
  18. asteroid_spinprops/ssolib/testing/ephemeris_testing/2011 EY17 +0 -101
  19. asteroid_spinprops/ssolib/testing/ephemeris_testing/2134 T-1 +0 -352
  20. asteroid_spinprops/ssolib/testing/ephemeris_testing/Bellmore +0 -2657
  21. asteroid_spinprops/ssolib/testing/ephemeris_testing/Dermott +0 -2971
  22. asteroid_spinprops/ssolib/testing/ephemeris_testing/Duke +0 -2026
  23. asteroid_spinprops/ssolib/testing/ephemeris_testing/Izenberg +0 -2440
  24. asteroid_spinprops/ssolib/testing/ephemeris_testing/Lermontov +0 -2760
  25. asteroid_spinprops/ssolib/testing/ephemeris_testing/Poullain +0 -1272
  26. asteroid_spinprops/ssolib/testing/ephemeris_testing/Sonneberga +0 -2756
  27. asteroid_spinprops/ssolib/testing/testing_ssoname_keys.pkl +0 -0
  28. asteroid_spinprops-0.2.32.dist-info/METADATA +0 -77
  29. asteroid_spinprops-0.2.32.dist-info/RECORD +0 -31
  30. {asteroid_spinprops-0.2.32.dist-info → asteroid_spinprops-1.0.1.dist-info}/WHEEL +0 -0
@@ -1,16 +1,10 @@
1
1
  import numpy as np
2
2
  import pandas as pd
3
- import asteroid_spinprops.ssolib.ssptools as ssptools
4
-
5
- import matplotlib.pyplot as plt
6
-
7
- from mpl_toolkits.axes_grid1.inset_locator import inset_axes
8
3
 
9
4
  import asteroid_spinprops.ssolib.utils as utils
10
5
 
11
6
  from fink_utils.sso.spins import (
12
7
  estimate_sso_params,
13
- func_sshg1g2,
14
8
  func_hg1g2_with_spin,
15
9
  )
16
10
  from asteroid_spinprops.ssolib.periodest import get_period_estimate
@@ -26,8 +20,66 @@ def get_fit_params(
26
20
  alt_spin=False,
27
21
  period_in=None,
28
22
  terminator=False,
29
- pole_metadata=False,
30
23
  ):
24
+ """
25
+ Fit a small solar system object's photometric data using SHG1G2 or SOCCA models.
26
+
27
+ This function can perform either a standard SHG1G2 fit or a spin- and
28
+ shape-constrained SOCCA fit, optionally including blind scans over
29
+ initial pole positions and periods. It supports filtering data by survey.
30
+
31
+ Parameters
32
+ ----------
33
+ data : pandas.DataFrame
34
+ Input dataset containing photometry and geometry with columns:
35
+ - 'cmred': reduced magnitudes
36
+ - 'csigmapsf': uncertainties
37
+ - 'Phase': solar phase angles (deg)
38
+ - 'cfid': filter IDs
39
+ - 'ra', 'dec': coordinates (deg)
40
+ - 'cjd': observation times
41
+ Optional (for terminator fits):
42
+ - 'ra_s', 'dec_s': sub-solar point coordinates (deg)
43
+ flavor : str
44
+ Model type to fit. Must be 'SHG1G2' or 'SSHG1G2'.
45
+ shg1g2_constrained : bool, optional
46
+ Whether to constrain the SSHG1G2 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.
49
+ p0 : list, optional
50
+ 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
+ alt_spin : bool, optional
54
+ For SSHG1G2 constrained fits, use the antipodal spin solution. Default False.
55
+ period_in : float, optional
56
+ Input synodic period (days) to override automatic estimation. Default None.
57
+ terminator : bool, optional
58
+ If True, include self-shading in the fit. Default False.
59
+
60
+ Returns
61
+ -------
62
+ dict or tuple
63
+ If `flavor='SHG1G2'`:
64
+ dict
65
+ Best-fit SHG1G2 parameters.
66
+ If `flavor='SSHG1G2'`:
67
+ dict
68
+ Best-fit SOCCA parameters.
69
+
70
+ Notes
71
+ -----
72
+ - For SOCCA fits with `shg1g2_constrained=True`, the function first performs
73
+ a SHG1G2 fit to constrain H, G1, G2, and shape parameters.
74
+ - Blind scans systematically vary initial pole positions and period to find
75
+ the optimal fit when `blind_scan=True`.
76
+
77
+ Raises
78
+ ------
79
+ ValueError
80
+ If `flavor` is not 'SHG1G2' or 'SSHG1G2'.
81
+ """
82
+
31
83
  if survey_filter is None:
32
84
  filter_mask = np.array(data["cfid"].values[0]) >= 0
33
85
  if survey_filter == "ZTF":
@@ -86,20 +138,10 @@ def get_fit_params(
86
138
  )
87
139
 
88
140
  ra0, dec0 = shg1g2_params["alpha0"], shg1g2_params["delta0"]
89
- # ra0alt, dec0alt = utils.flip_spin(
90
- # shg1g2_params["alpha0"], shg1g2_params["delta0"]
91
- # )
141
+
92
142
  ra_init, dec_init = utils.generate_initial_points(
93
143
  ra0, dec0, dec_shift=45
94
144
  )
95
- if pole_metadata:
96
- pole_md = pd.DataFrame()
97
- pole_md["ra0"] = [ra0]
98
- pole_md["dec0"] = [dec0]
99
- pole_md["ra_init"] = [ra_init]
100
- pole_md["dec_init"] = [dec_init]
101
-
102
- ra_fin, dec_fin = [], []
103
145
 
104
146
  H_key = next(
105
147
  (f"H_{i}" for i in range(1, 7) if f"H_{i}" in shg1g2_params),
@@ -130,24 +172,12 @@ def get_fit_params(
130
172
  try:
131
173
  rms.append(sshg1g2["rms"])
132
174
  model.append(sshg1g2)
133
- if pole_metadata:
134
- ra_fin.append(sshg1g2["alpha0"])
135
- dec_fin.append(sshg1g2["dec0"])
136
175
  except Exception:
137
176
  continue
138
177
  rms = np.array(rms)
139
178
  sshg1g2_opt = model[rms.argmin()]
140
179
 
141
- if pole_metadata:
142
- ra_fin = np.array(ra_fin)
143
- dec_fin = np.array(dec_fin)
144
-
145
- pole_md["ra_fin"] = [ra_fin]
146
- pole_md["dec_fin"] = [dec_fin]
147
-
148
- return sshg1g2_opt, pole_md
149
- else:
150
- return sshg1g2_opt
180
+ return sshg1g2_opt
151
181
  else:
152
182
  period_si_t, alt_period_si_t, _ = utils.estimate_sidereal_period(
153
183
  data=data, model_parameters=shg1g2_params, synodic_period=period_sy
@@ -257,228 +287,36 @@ def get_fit_params(
257
287
  print("Model must either be SHG1G2 or SSHG1G2, not {}".format(flavor))
258
288
 
259
289
 
260
- def plot_model(
261
- data, flavor, model_params, x_axis="Date", resolution=400, filterout=False
262
- ):
263
- fink_colors = ["#15284F", "#F5622E", "#0E6B77", "#4A4A4A"]
264
-
265
- jd = np.linspace(
266
- np.min(data["cjd"].values[0]), np.max(data["cjd"].values[0]), resolution
267
- ).tolist()
268
- eph = ssptools.ephemcc(data["name"].values[0], jd, tcoor=1, observer="500")
269
- ra = eph["RA"].values
270
- dec = eph["DEC"].values
271
- ra, dec = utils.sexa_to_deg(ra, dec)
272
-
273
- params = model_params
274
-
275
- if x_axis == "Date":
276
- xvals_eph = "Date"
277
- xvals = "cjd"
278
- else:
279
- xvals = xvals_eph = "Phase"
280
-
281
- markers = ["<", ">", "^", "v"]
282
-
283
- label_sh = r"$\text{sHG}_1\text{G}_2$"
284
- label_ssh = r"$\text{ssHG}_1\text{G}_2$"
285
-
286
- if flavor == "SHG1G2":
287
- label_m = label_sh
288
- if flavor == "SSHG1G2":
289
- label_m = label_ssh
290
-
291
- filter_names = ["ZTF g", "ZTF r", "ATLAS orange", "ATLAS cyan"]
292
-
293
- if filterout is True:
294
- rdata = data.copy()
295
- ztfcond = (rdata["cfid"].values[0] == 1) | (rdata["cfid"].values[0] == 2)
296
- for col in rdata.columns:
297
- values = rdata.at[0, col]
298
- if isinstance(values, np.ndarray) and len(values) == len(ztfcond):
299
- rdata.at[0, col] = np.array(values)[ztfcond]
300
-
301
- data = rdata
302
- nfilts = len(np.unique(data["cfid"].values[0]))
303
-
304
- fig, ax = plt.subplots(
305
- int(nfilts + nfilts / 2),
306
- 1,
307
- figsize=(12, 8 * nfilts / 2),
308
- sharex=True,
309
- gridspec_kw={
310
- "top": 0.995,
311
- "left": 0.075,
312
- "right": 0.995,
313
- "bottom": 0.085,
314
- "hspace": 0.02,
315
- "height_ratios": [2, 2, 1] if nfilts == 2 else [2, 2, 1, 2, 2, 1],
316
- },
317
- )
318
-
319
- if x_axis == "Phase":
320
- alpha = 0.5
321
- else:
322
- alpha = 1
323
-
324
- for i, f in enumerate(np.unique(data["cfid"].values[0])):
325
- filter_mask = data["cfid"].values[0] == f
326
-
327
- if flavor == "SHG1G2":
328
- model_params = [
329
- params["H_{}".format(f)],
330
- params["G1_{}".format(f)],
331
- params["G2_{}".format(f)],
332
- params["R"],
333
- np.radians(params["alpha0"]),
334
- np.radians(params["delta0"]),
335
- ]
336
-
337
- model = func_hg1g2_with_spin(
338
- [np.radians(eph["Phase"]), np.radians(ra), np.radians(dec)],
339
- *model_params,
340
- )
341
-
342
- model_points = func_hg1g2_with_spin(
343
- [
344
- np.radians(data["Phase"].values[0][filter_mask]),
345
- np.radians(data["ra"].values[0][filter_mask]),
346
- np.radians(data["dec"].values[0][filter_mask]),
347
- ],
348
- *model_params,
349
- )
350
- if flavor == "SSHG1G2":
351
- jd_ltc = np.array(jd)
352
- jd_data_ltc = data["cjd"].values[0][filter_mask]
353
-
354
- model_params = [
355
- params["H_{}".format(f)],
356
- params["G1_{}".format(f)],
357
- params["G2_{}".format(f)],
358
- np.radians(params["alpha0"]),
359
- np.radians(params["delta0"]),
360
- params["period"],
361
- params["a_b"],
362
- params["a_c"],
363
- np.radians(params["phi0"]),
364
- ]
365
-
366
- model = func_sshg1g2(
367
- [np.radians(eph["Phase"]), np.radians(ra), np.radians(dec), jd_ltc],
368
- *model_params,
369
- )
370
-
371
- model_points = func_sshg1g2(
372
- [
373
- np.radians(data["Phase"].values[0][filter_mask]),
374
- np.radians(data["ra"].values[0][filter_mask]),
375
- np.radians(data["dec"].values[0][filter_mask]),
376
- jd_data_ltc,
377
- ],
378
- *model_params,
379
- )
380
- residuals = data["cmred"].values[0][filter_mask] - model_points
381
-
382
- if i > 1:
383
- ax[i + 1].plot(
384
- eph[xvals_eph].values,
385
- model,
386
- c="black",
387
- linestyle="--",
388
- linewidth=1.1,
389
- label=label_m,
390
- alpha=alpha,
391
- )
392
- ax[i + 1].scatter(
393
- data[xvals].values[0][filter_mask],
394
- data["cmred"].values[0][filter_mask],
395
- marker=markers[i],
396
- c=fink_colors[i],
397
- label=filter_names[i],
398
- zorder=1000,
399
- )
400
-
401
- inset_hist = inset_axes(ax[i + 1], width="30%", height="30%")
402
- inset_hist.hist(residuals, bins=40, color=fink_colors[i], density=True)
403
-
404
- ax[5].scatter(
405
- data[xvals].values[0][filter_mask],
406
- residuals,
407
- marker=markers[i],
408
- c=fink_colors[i],
409
- )
410
- yabs_max = abs(max(ax[5].get_ylim(), key=abs))
411
- ax[5].set_ylim(ymin=-yabs_max - 0.2, ymax=yabs_max + 0.2)
412
- ax[5].axhline(y=0, c="black", linestyle="--", zorder=-1000, alpha=0.5)
413
- ax[i + 1].legend(loc="lower left")
414
- if xvals == "Phase":
415
- ax[i + 1].invert_yaxis()
416
-
417
- else:
418
- ax[i].plot(
419
- eph[xvals_eph].values,
420
- model,
421
- c="black",
422
- linestyle="--",
423
- linewidth=1.1,
424
- label=label_m,
425
- alpha=alpha,
426
- )
427
- ax[i].scatter(
428
- data[xvals].values[0][filter_mask],
429
- data["cmred"].values[0][filter_mask],
430
- marker=markers[i],
431
- c=fink_colors[i],
432
- label=filter_names[i],
433
- zorder=1000,
434
- )
435
-
436
- inset_hist = inset_axes(ax[i], width="30%", height="30%")
437
- inset_hist.hist(residuals, bins=40, color=fink_colors[i], density=True)
438
-
439
- ax[2].scatter(
440
- data[xvals].values[0][filter_mask],
441
- residuals,
442
- marker=markers[i],
443
- c=fink_colors[i],
444
- )
445
- yabs_max = abs(max(ax[2].get_ylim(), key=abs))
446
- ax[2].set_ylim(ymin=-yabs_max - 0.1, ymax=yabs_max + 0.1)
447
- ax[2].axhline(y=0, c="black", linestyle="--", zorder=-1000, alpha=0.5)
448
- ax[i].legend(loc="lower left")
449
- if x_axis == "Phase":
450
- ax[i].invert_yaxis()
451
- if i > 2:
452
- if xvals == "cjd":
453
- ax[5].set_xlabel("Time (days)")
454
- else:
455
- ax[5].set_xlabel("Phase (degree)")
456
-
457
- ax[5].set_ylabel("Residuals")
458
-
459
- if xvals == "cjd":
460
- ax[2].set_xlabel("Time (days)")
461
- else:
462
- ax[2].set_xlabel("Phase (degree)")
463
-
464
- else:
465
- if xvals == "cjd":
466
- ax[2].set_xlabel("Time (days)")
467
- else:
468
- ax[2].set_xlabel("Phase (degree)")
469
-
470
- ax[2].set_ylabel("Residuals")
471
-
472
- fig.text(0.01, 0.81, "Reduced magnitude", va="center", rotation="vertical")
473
- fig.text(0.01, 0.35, "Reduced magnitude", va="center", rotation="vertical")
474
-
475
- plt.tight_layout()
476
-
477
- # if xvals == "Phase":
478
- # plt.gca().set_xlim(left=0)
479
-
480
-
481
290
  def get_model_points(data, params):
291
+ """
292
+ Compute modeled magnitudes for a dataset using SHG1G2.
293
+
294
+ For each unique filter in the data, this function applies the SHG1G2 model
295
+ to the corresponding subset of observations.
296
+
297
+ Parameters
298
+ ----------
299
+ data : pandas.DataFrame
300
+ Dataset containing at least the following columns:
301
+ - 'Phase' : solar phase angles (deg)
302
+ - 'ra' : right ascension (deg)
303
+ - 'dec' : declination (deg)
304
+ - 'cfid' : filter IDs (int)
305
+ params : dict
306
+ Model parameters containing keys:
307
+ - 'H_i', 'G1_i', 'G2_i' for each filter i
308
+ - 'R' : oblateness
309
+ - 'alpha0', 'delta0' : pole coordinates in degrees
310
+
311
+ Returns
312
+ -------
313
+ tuple of lists
314
+ - model_points_stack : list of numpy.ndarray
315
+ Modeled magnitudes for each filter.
316
+ - index_points_stack : list of numpy.ndarray
317
+ Indices of the original data points corresponding to each modeled subset.
318
+ """
319
+
482
320
  model_points_stack = []
483
321
  index_points_stack = []
484
322
  index = np.array([ind for ind in range(len(data["cfid"].values[0]))])
@@ -510,6 +348,29 @@ def get_model_points(data, params):
510
348
 
511
349
 
512
350
  def get_residuals(data, params):
351
+ """
352
+ Compute residuals between observed and modeled magnitudes for a dataset.
353
+
354
+ Parameters
355
+ ----------
356
+ data : pandas.DataFrame
357
+ Dataset containing at least the following columns:
358
+ - 'cmred' : observed reduced magnitudes
359
+ - 'Phase' : solar phase angles (deg)
360
+ - 'ra' : right ascension (deg)
361
+ - 'dec' : declination (deg)
362
+ - 'cfid' : filter IDs (int)
363
+ params : dict
364
+ Model parameters including H, G1, G2 for each filter, pole coordinates,
365
+ and oblateness. Keys should match those expected by `get_model_points`.
366
+
367
+ Returns
368
+ -------
369
+ numpy.ndarray
370
+ Residuals (observed - modeled magnitudes) for all data points,
371
+ ordered according to the original dataset.
372
+ """
373
+
513
374
  pstack, istack = get_model_points(data, params)
514
375
  fpstack, fistack = utils.flatten_list(pstack), utils.flatten_list(istack)
515
376
  df_to_sort = pd.DataFrame({"mpoints": fpstack}, index=fistack)
@@ -519,6 +380,36 @@ def get_residuals(data, params):
519
380
 
520
381
 
521
382
  def make_residuals_df(data, model_parameters):
383
+ """
384
+ Create a DataFrame of residuals between observed and modeled magnitudes.
385
+
386
+ Parameters
387
+ ----------
388
+ data : pandas.DataFrame
389
+ Dataset containing at least the following columns:
390
+ - 'cmred' : observed reduced magnitudes
391
+ - 'csigmapsf' : photometric uncertainties
392
+ - 'Phase' : solar phase angles (deg)
393
+ - 'ra' : right ascension (deg)
394
+ - 'dec' : declination (deg)
395
+ - 'cfid' : filter IDs (int)
396
+ - 'cjd' : observation times
397
+ model_parameters : dict
398
+ Model parameters including H, G1, G2 for each filter, pole coordinates,
399
+ and oblateness. Keys should match those expected by `get_model_points`.
400
+
401
+ Returns
402
+ -------
403
+ pandas.DataFrame
404
+ DataFrame indexed by observation index, with columns:
405
+ - 'mpoints' : modeled magnitudes
406
+ - 'mred' : observed reduced magnitudes
407
+ - 'sigma' : observational uncertainties
408
+ - 'filters' : filter IDs
409
+ - 'jd' : observation times
410
+ - 'residuals' : difference between observed and modeled magnitudes
411
+ (mred - mpoints)
412
+ """
522
413
  mpoints, indices = get_model_points(data=data, params=model_parameters)
523
414
  flat_mpoints, flat_index = utils.flatten_list(mpoints), utils.flatten_list(indices)
524
415