|
22 | 22 | import astropy.units as u |
23 | 23 |
|
24 | 24 | from fink_utils.sso.utils import estimate_axes_ratio |
| 25 | +from fink_utils.sso.utils import get_opposition, split_dataframe_per_apparition |
25 | 26 | from fink_utils.tester import regular_unit_tests |
26 | 27 |
|
27 | 28 |
|
@@ -343,6 +344,51 @@ def func_sshg1g2(pha, h, g1, g2, alpha0, delta0, period, a_b, a_c, phi0): |
343 | 344 | return func1 + func2 |
344 | 345 |
|
345 | 346 |
|
| 347 | +def sfhg1g2_multiple(phas, g1, g2, *args): |
| 348 | + """HG1G2 model in the case of simultaneous fit |
| 349 | +
|
| 350 | + Parameters |
| 351 | + ---------- |
| 352 | + phas: np.array |
| 353 | + (N, M_o) array of phase angles. N is the number |
| 354 | + of opposition, M_o is the number of observations |
| 355 | + per opposition. Phase is radians. |
| 356 | + g1: float |
| 357 | + G1 parameter (no unit) |
| 358 | + g2: float |
| 359 | + G2 parameter (no unit) |
| 360 | + args: float |
| 361 | + List of Hs, one per opposition. |
| 362 | +
|
| 363 | + Returns |
| 364 | + ------- |
| 365 | + out: np.array |
| 366 | + Magnitude as predicted by `func_hg1g2`. |
| 367 | + """ |
| 368 | + fl = [] |
| 369 | + for alpha, h in zip(phas, args[0][:]): |
| 370 | + fl.append(func_hg1g2(alpha, h, g1, g2)) |
| 371 | + return np.concatenate(fl) |
| 372 | + |
| 373 | + |
| 374 | +def sfhg1g2_error_fun(params, phas, mags): |
| 375 | + """Difference between sfHG1G2 predictions and observations |
| 376 | +
|
| 377 | + Parameters |
| 378 | + ---------- |
| 379 | + params: list |
| 380 | + [G1, G2, *H_i], where H_i are the Hs, one per opposition |
| 381 | + phas: np.array |
| 382 | + (N, M_o) array of phase angles. N is the number |
| 383 | + of opposition, M_o is the number of observations |
| 384 | + per opposition. Must be sorted by time. Phase is radians. |
| 385 | + mags: np.array |
| 386 | + Reduced magnitude, that is m_obs - 5 * np.log10('Dobs' * 'Dhelio') |
| 387 | + Sorted by time. |
| 388 | + """ |
| 389 | + return sfhg1g2_multiple(phas, params[0], params[1], params[2:]) - mags |
| 390 | + |
| 391 | + |
346 | 392 | def color_correction_to_V(): # noqa: N802 |
347 | 393 | """Color correction from band to V |
348 | 394 |
|
@@ -864,12 +910,7 @@ def fit_legacy_models( |
864 | 910 |
|
865 | 911 | Returns |
866 | 912 | ------- |
867 | | - popt: list |
868 | | - Estimated parameters for `func` |
869 | | - perr: list |
870 | | - Error estimates on popt elements |
871 | | - chi2_red: float |
872 | | - Reduced chi2 |
| 913 | + outdic: dict |
873 | 914 | """ |
874 | 915 | if p0 is None: |
875 | 916 | p0 = [15, 0.15, 0.15] |
@@ -994,6 +1035,129 @@ def fit_legacy_models( |
994 | 1035 | return outdic |
995 | 1036 |
|
996 | 1037 |
|
| 1038 | +def fit_sfhg1g2( |
| 1039 | + ssnamenr, |
| 1040 | + magpsf_red, |
| 1041 | + sigmapsf, |
| 1042 | + jds, |
| 1043 | + phase, |
| 1044 | + filters, |
| 1045 | +): |
| 1046 | + """Fit for phase curve parameters for sfHG1G2 |
| 1047 | +
|
| 1048 | + Notes |
| 1049 | + ----- |
| 1050 | + Unlike other models, it returns less information, and |
| 1051 | + only per-band. |
| 1052 | +
|
| 1053 | + Parameters |
| 1054 | + ---------- |
| 1055 | + ssnamenr: str |
| 1056 | + SSO name/number |
| 1057 | + magpsf_red: array |
| 1058 | + Reduced magnitude, that is m_obs - 5 * np.log10('Dobs' * 'Dhelio') |
| 1059 | + sigmapsf: array |
| 1060 | + Error estimates on magpsf_red |
| 1061 | + jds: array |
| 1062 | + Julian Dates |
| 1063 | + phase: array |
| 1064 | + Phase angle [rad] |
| 1065 | + filters: array |
| 1066 | + Filter name for each measurement |
| 1067 | +
|
| 1068 | + Returns |
| 1069 | + ------- |
| 1070 | + outdic: dict |
| 1071 | + Dictionary containing reduced chi2, and estimated parameters and |
| 1072 | + error on each parameters. |
| 1073 | + """ |
| 1074 | + # exit if NaN values |
| 1075 | + if not np.all([i == i for i in magpsf_red]): |
| 1076 | + outdic = {"fit": 1, "status": -2} |
| 1077 | + return outdic |
| 1078 | + |
| 1079 | + pdf = pd.DataFrame({ |
| 1080 | + "i:magpsf_red": magpsf_red, |
| 1081 | + "i:sigmapsf": sigmapsf, |
| 1082 | + "Phase": phase, |
| 1083 | + "i:jd": jds, |
| 1084 | + "i:fid": filters, |
| 1085 | + }) |
| 1086 | + pdf = pdf.sort_values("i:jd") |
| 1087 | + |
| 1088 | + # Get oppositions |
| 1089 | + pdf[["elong", "elongFlag"]] = get_opposition(pdf["i:jd"].to_numpy(), ssnamenr) |
| 1090 | + |
| 1091 | + # loop over filters |
| 1092 | + ufilters = np.unique(pdf["i:fid"].to_numpy()) |
| 1093 | + outdics = {} |
| 1094 | + for filt in ufilters: |
| 1095 | + outdic = {} |
| 1096 | + |
| 1097 | + # Select data for a filter |
| 1098 | + sub = pdf[pdf["i:fid"] == filt].copy() |
| 1099 | + |
| 1100 | + # Compute apparitions |
| 1101 | + splitted = split_dataframe_per_apparition(sub, "elongFlag", "i:jd") |
| 1102 | + napparition = len(splitted) |
| 1103 | + |
| 1104 | + # H for all apparitions plus G1, G2 |
| 1105 | + params_ = ["G1", "G2", *["H{}".format(i) for i in range(napparition)]] |
| 1106 | + params = [i + "_{}".format(str(filt)) for i in params_] |
| 1107 | + |
| 1108 | + # Split phase |
| 1109 | + phase_list = [df["Phase"].to_numpy().tolist() for df in splitted] |
| 1110 | + |
| 1111 | + # Fit |
| 1112 | + res_lsq = least_squares( |
| 1113 | + sfhg1g2_error_fun, |
| 1114 | + x0=[0.15, 0.15] + [15] * napparition, |
| 1115 | + bounds=( |
| 1116 | + [0, 0] + [-3] * napparition, |
| 1117 | + [1, 1] + [30] * napparition, |
| 1118 | + ), |
| 1119 | + loss="huber", |
| 1120 | + method="trf", |
| 1121 | + args=[ |
| 1122 | + phase_list, |
| 1123 | + sub["i:magpsf_red"].to_numpy().tolist(), |
| 1124 | + ], |
| 1125 | + xtol=1e-20, |
| 1126 | + gtol=1e-17, |
| 1127 | + ) |
| 1128 | + |
| 1129 | + # Result |
| 1130 | + popt = res_lsq.x |
| 1131 | + |
| 1132 | + # estimate covariance matrix using the jacobian |
| 1133 | + try: |
| 1134 | + cov = linalg.inv(res_lsq.jac.T @ res_lsq.jac) |
| 1135 | + chi2dof = np.sum(res_lsq.fun**2) / (res_lsq.fun.size - res_lsq.x.size) |
| 1136 | + cov *= chi2dof |
| 1137 | + |
| 1138 | + # 1sigma uncertainty on fitted parameters |
| 1139 | + perr = np.sqrt(np.diag(cov)) |
| 1140 | + except np.linalg.LinAlgError: |
| 1141 | + # raised if jacobian is degenerated |
| 1142 | + outdic = {"fit": 4, "status": res_lsq.status} |
| 1143 | + return outdic |
| 1144 | + |
| 1145 | + chisq = np.sum((res_lsq.fun / sub["i:sigmapsf"]) ** 2) |
| 1146 | + chisq_red = chisq / (res_lsq.fun.size - res_lsq.x.size - 1) |
| 1147 | + outdic["chi2red_{}".format(filt)] = chisq_red |
| 1148 | + outdic["rms_{}".format(filt)] = np.sqrt(np.mean(res_lsq.fun**2)) |
| 1149 | + outdic["n_obs_{}".format(filt)] = len(sub) |
| 1150 | + outdic["n_app_{}".format(filt)] = napparition |
| 1151 | + |
| 1152 | + for i in range(len(params)): |
| 1153 | + outdic[params[i]] = popt[i] |
| 1154 | + outdic["err_" + params[i]] = perr[i] |
| 1155 | + |
| 1156 | + outdics.update(outdic) |
| 1157 | + |
| 1158 | + return outdics |
| 1159 | + |
| 1160 | + |
997 | 1161 | def fit_spin( |
998 | 1162 | magpsf_red, |
999 | 1163 | sigmapsf, |
|
0 commit comments