3@brief `main()` shows how to
4 [minimize()](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)
5 the [objective function()](#calibration.objective_function) for antenna phase center
12from pathlib
import Path
13from initialise
import load_pueoEvent_Dataset
20def generate_antenna_phase_center_pairs_with_placeholders(qrh_dot_dat: Path, offset_inSIM:Path |
None =
None, ant_type=
'MI') -> pl.DataFrame:
21 r"""!Creates [antenna pairs](#antenna_pairs.generate_MI_antenna_pairs) with placeholder
22 integers as antenna phase centers.
24@param[in] qrh_dot_dat Path to `qrh.dat`
25@retval nominal_phase_center_pairs
31$ A1_Rho (placeholder) <u16>
32$ A1_Phi (placeholder) <u16>
33$ A1_Z (placeholder) <u16>
39$ A2_Rho (placeholder) <u16>
40$ A2_Phi (placeholder) <u16>
41$ A2_Z (placeholder) <u16>
47 -# Calls #antenna_attributes.read_MI_antenna_geometry() to read `qrh.dat` to obtain
48 antenna face center positions
49 -# Calls #antenna_attributes.get_MI_nominal_phase_center() to convert face centers to
51 -# Adds three columns to store placeholder values of the phase centers; these integer placeholders
52 serve as dictionary keys.
55┌────────┬────────┬──────┬────────────────┬────────────────┬───────────────┬──────────┬───────────┬───────┐
56│ AntNum ┆ AntIdx ┆ Ring ┆ Rho ┆ Phi ┆ Z ┆ Rho ┆ Phi ┆ Z │
57│ --- ┆ --- ┆ --- ┆ (placeholder) ┆ (placeholder) ┆ (placeholder) ┆ --- ┆ --- ┆ --- │
58│ enum ┆ u32 ┆ u8 ┆ --- ┆ --- ┆ --- ┆ f64 ┆ f64 ┆ f64 │
59│ ┆ ┆ ┆ u16 ┆ u16 ┆ u16 ┆ ┆ ┆ │
60╞════════╪════════╪══════╪════════════════╪════════════════╪═══════════════╪══════════╪═══════════╪═══════╡
61│ 101 ┆ 0 ┆ 1 ┆ 0 ┆ 96 ┆ 192 ┆ 1.188 ┆ 0.0 ┆ 5.114 │
62│ 201 ┆ 1 ┆ 2 ┆ 1 ┆ 97 ┆ 193 ┆ 2.296 ┆ 0.0 ┆ 1.457 │
63│ 301 ┆ 2 ┆ 3 ┆ 2 ┆ 98 ┆ 194 ┆ 2.296 ┆ 0.0 ┆ 0.728 │
64│ 401 ┆ 3 ┆ 4 ┆ 3 ┆ 99 ┆ 195 ┆ 2.296 ┆ 0.0 ┆ 0.0 │
65│ 102 ┆ 4 ┆ 1 ┆ 4 ┆ 100 ┆ 196 ┆ 1.076821 ┆ 0.261538 ┆ 4.476 │
66│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
67│ 423 ┆ 91 ┆ 4 ┆ 91 ┆ 187 ┆ 283 ┆ 2.295825 ┆ -0.523638 ┆ 0.0 │
68│ 124 ┆ 92 ┆ 1 ┆ 92 ┆ 188 ┆ 284 ┆ 1.07708 ┆ -0.26224 ┆ 4.476 │
69│ 224 ┆ 93 ┆ 2 ┆ 93 ┆ 189 ┆ 285 ┆ 2.295502 ┆ -0.261893 ┆ 1.457 │
70│ 324 ┆ 94 ┆ 3 ┆ 94 ┆ 190 ┆ 286 ┆ 2.295502 ┆ -0.261893 ┆ 0.728 │
71│ 424 ┆ 95 ┆ 4 ┆ 95 ┆ 191 ┆ 287 ┆ 2.295502 ┆ -0.261893 ┆ 0.0 │
72└────────┴────────┴──────┴────────────────┴────────────────┴───────────────┴──────────┴───────────┴───────┘
74 -# Calls #antenna_pairs.generate_MI_antenna_pairs with the above dataframe as the input.
76 from antenna_pairs
import generate_MI_antenna_pairs, generate_LF_antenna_pairs
77 from antenna_attributes
import read_antenna_geometry, get_nominal_phase_center
79 fc: pl.DataFrame = read_antenna_geometry(qrh_dot_dat=qrh_dot_dat, offset_inSIM=offset_inSIM, ant_type=ant_type)
80 nominal_and_placeholders: pl.DataFrame = (
81 get_nominal_phase_center(face_centers=fc, coordinates=
"cylindrical", ant_type=ant_type)
83 pl.arange(pl.len()).alias(
"Rho (placeholder)").cast(pl.UInt16),
84 pl.arange(pl.len(), pl.len() * 2).alias(
"Phi (placeholder)").cast(pl.UInt16),
85 pl.arange(pl.len() * 2, pl.len() * 3).alias(
"Z (placeholder)").cast(pl.UInt16),
86 pl.arange(pl.len() * 3, pl.len() * 4).alias(
"CableDelay (placeholder)").cast(pl.UInt16),
87 pl.lit(0.0).alias(
"CableDelay"),
90 "AntNum",
"AntIdx",
"Ring",
"Rho (placeholder)",
"Phi (placeholder)",
"Z (placeholder)",
"CableDelay (placeholder)",
91 pl.col(
"Rho[m]").alias(
"Rho"), pl.col(
"Phi[rad]").alias(
"Phi"), pl.col(
"Z[m]").alias(
"Z"),
"CableDelay"
95 nominal_phase_center_pairs: pl.DataFrame = (
96 generate_MI_antenna_pairs(antennas=nominal_and_placeholders)
97 .select(pl.all().exclude(
r"^A[12]_AntIdx$"))
101 nominal_phase_center_pairs: pl.DataFrame = (
102 generate_LF_antenna_pairs(antennas=nominal_and_placeholders)
103 .select(pl.all().exclude(
r"^A[12]_AntIdx$"))
106 print(
"Ant type is not supported!")
108 return nominal_phase_center_pairs
111def __get_all_run_numbers(pueo_data: Path) -> [int]:
112 """! A temporary helper function of #load_dataset_and_measure_time_delays()
114 @param[in] pueo_data See the parameter of #initialise.load_pueoEvent_Dataset
116 * This function retrieves the run numbers (negative sign allowed) of the `run/` subdirectories
119 * The runs are returned as a list of integers.
121 * The function is here only for testing purposes,
122 as it makes little sense to use every single run for antenna calibration.
126 run_number_pattern_ = re.compile(
r'^run([+\-]?\d+)$')
127 return [int(run_number_pattern_.match(run_directories_.name).group(1))
128 for run_directories_
in pueo_data.rglob(
"run*")]
131def load_dataset_and_measure_time_delays(dataset: ROOT.pueo.Dataset,
132 phase_center_pairs: pl.DataFrame, pulser_station: str=
'taylor_dome', MC=
False, AntType=
'MI', IcefinalFileDir:str=
'', singlerun:int=
'', event_list:list =
None,UPSAMPLE=1) -> pl.DataFrame:
133 r"""! Uses [SciPy's correlate()](https://docs.scipy.org/doc//scipy-1.16.2/reference/generated/scipy.signal.correlate.html)
134 to "measure" the delays of [waveforms](#waveform_plots.load_waveforms) between antenna pairs
136 @param[in] dataset The output of #initialise.load_pueoEvent_Dataset
137 @param[in] phase_center_pairs The output of #generate_antenna_phase_center_pairs_with_placeholders
138 @param[in] pulser_station For data, which pulser station the data is from, taylor_dome, ldb, s200
139 @param[in] MC This is MC or not
140 @param[in] AntType This is for MI or LF
141 @param[in] IcefinalFileDir Temporary input, only for the LF + MC combination, the dataformat is not ready, so we read waveform from root file diretly. Path to the IceFinal Root file
142 @param[in] singlerun If specified, only use one run instead of all runs in the data directory
143 @param[in] UPSAMPPLE Upsample factor
144 @retval pulser_directions_and_measured_time_delays
146 * The input `dataset` should have been initialized with a (valid) run number,
147 but that is not the only run that gets loaded.
148 For now, [all runs](#__get_all_run_numbers) are used.
151$ measured time delays (ns) <f64>
152$ max correlation <f64>
155$ A1_Rho (placeholder) <u16>
156$ A1_Phi (placeholder) <u16>
157$ A1_Z (placeholder) <u16>
163$ A2_Rho (placeholder) <u16>
164$ A2_Phi (placeholder) <u16>
165$ A2_Z (placeholder) <u16>
169$ incoming pulse direction (Phi Theta) <array[f64, 2]>
171 * The `dataset` is also used to load the directions of the incoming pulses
172 (\f$^\circ\phi\f$ and \f$^\circ\theta\f$ in payload coordinates).
173 * `measured time delays (ns)` is computed by the helper function
174 #_scipy_cross_correlate_all_pairs_for_one_event().
178 from locate_signal
import _get_true_direction, _get_true_pulser_direction, _get_true_pulser_direction_tmp
179 from waveform_plots
import load_waveforms, upsample_waveforms, load_waveforms_from_root
182 pulser_directions_and_measured_time_delays = pl.DataFrame()
186 all_runs: [int] = __get_all_run_numbers(
188 dataset.getDataDir(ROOT.pueo.Dataset.PUEO_MC_DATA)
192 all_runs: [int] = __get_all_run_numbers(
194 dataset.getDataDir(ROOT.pueo.Dataset.PUEO_ROOT_DATA)
202 dataset.loadRun(run, ROOT.pueo.Dataset.PUEO_MC_DATA)
204 dataset.loadRun(run, ROOT.pueo.Dataset.PUEO_ROOT_DATA)
207 for i
in range(dataset.N()):
212 load_waveforms(run, event=i, anttype=AntType, upsample_factor=UPSAMPLE)
213 .filter(pl.col(
"Pol") ==
"V")
214 .select(
"AntNum",
"Pol",
"waveforms (volts)",
"step size (nanoseconds)")
218 IcefinalFile=IcefinalFileDir+f
'run{run}/IceFinal_{run}_allTree.root'
220 load_waveforms_from_root(IcefinalFile, ev=i)
221 .filter(pl.col(
"Pol") ==
"V")
222 .select(
"AntNum",
"Pol",
"waveforms (volts)",
"step size (nanoseconds)")
224 wf: pl.DataFrame = upsample_waveforms(waveforms=wf, upsample_factor=UPSAMPLE)
227 pulser_directions_and_measured_time_delays.vstack(
228 _scipy_cross_correlate_all_pairs_for_one_event(
229 phase_center_pairs=phase_center_pairs,
232 (pl.lit(_get_true_direction(dataset=dataset)) * np.pi / 180)
233 .cast(pl.Array(pl.Float64, 2))
234 .alias(
"incoming pulse direction (Phi Theta)")
240 for eventnum
in event_list:
241 dataset.getEvent(eventnum);
243 load_waveforms(run, eventNumber=eventnum, anttype=AntType, upsample_factor=UPSAMPLE)
244 .filter(pl.col(
"Pol") ==
"V")
245 .select(
"AntNum",
"Pol",
"waveforms (volts)",
"step size (nanoseconds)")
248 pulser_directions_and_measured_time_delays.vstack(
249 _scipy_cross_correlate_all_pairs_for_one_event(
250 phase_center_pairs=phase_center_pairs,
253 (pl.lit(_get_true_pulser_direction_tmp(dataset=dataset, pulser_station=pulser_station)) * np.pi / 180)
254 .cast(pl.Array(pl.Float64, 2))
255 .alias(
"incoming pulse direction (Phi Theta)")
260 return pulser_directions_and_measured_time_delays
263def _scipy_cross_correlate_all_pairs_for_one_event(phase_center_pairs: pl.DataFrame,
264 waveforms: pl.DataFrame
266 r"""! A helper function of #load_dataset_and_measure_time_delays()
268 @param[in] phase_center_pairs The output of #generate_antenna_phase_center_pairs_with_placeholders
269 @param[in] waveforms The output of #waveform_plots.load_waveforms
270 @retval measured_time_delays
274$ measured time delays (ns) <f64>
275$ max correlation <f64>
278$ A1_Rho (placeholder) <u16>
279$ A1_Phi (placeholder) <u16>
280$ A1_Z (placeholder) <u16>
286$ A2_Rho (placeholder) <u16>
287$ A2_Phi (placeholder) <u16>
288$ A2_Z (placeholder) <u16>
294 -# [Load the waveforms](#waveform_plots.load_waveforms) of both antennas in the antenna pair
295 -# Cross correlate the waveform pair
296 -# Figure out by how much one needs to shift the second waveform against the first
297 in order to achieve the maximum correlation.
298 -# The above is called `measured time delays (ns)` in the output dataframe.
299 -# For more details, see [Explanation](#scipy_corr_expl).
301 from scipy.signal
import correlate, correlation_lags
305 signal_length = len(waveforms[
"waveforms (volts)"][0])
306 CORRLAGS: np.ndarray = correlation_lags(signal_length, signal_length)
308 STEP_SIZE = waveforms[
"step size (nanoseconds)"][0]
310 assert np.mean(waveforms[
"step size (nanoseconds)"].to_numpy()) - STEP_SIZE < 1e-10
312 corr_func = zncc_pairwise_correlation
314 measured_time_delays = (
316 .join(waveforms, left_on=
"A1_AntNum", right_on=
"AntNum")
317 .join(waveforms, left_on=[
"A2_AntNum",
"Pol"], right_on=[
"AntNum",
"Pol"])
319 pl.struct(
"waveforms (volts)",
"waveforms (volts)_right")
320 .map_batches(corr_func).alias(
"correlation"),
323 pl.col(
"correlation").arr.max().alias(
"max correlation"),
324 pl.col(
"correlation").arr.arg_max().alias(
"max correlation idx"),
327 pl.col(
"max correlation idx").map_batches(
lambda idx: CORRLAGS[idx] * STEP_SIZE)
328 .alias(
"measured time delays (ns)")
331 "measured time delays (ns)",
"max correlation",
332 r"^A[12]_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$",
336 return measured_time_delays
338def zncc_pairwise_correlation(s):
339 from scipy.signal
import correlate
342 (wf1 - np.mean(wf1)) / np.std(wf1),
343 (wf2 - np.mean(wf2)) / np.std(wf2),
346 for wf2, wf1
in s.to_numpy()
349def retrieve_relevant_antennas_from_all_pairs(phase_center_pairs: pl.DataFrame):
350 r"""! Retrieves the (unique) antennas, given a bunch of antenna pairs.
352 @param[in] phase_center_pairs The output of #load_dataset_and_measure_time_delays
353 @retval relevant_antennas
355 * This step is not trivial, as explained below.
356 * It is conceivable that not all antennas receive signals clear enough for calibration purposes
357 (resulting in weak correlations).
358 * As such, the weakly correlated antenna pairs should be filtered out.
361 pulser_directions_and_pairwise_time_delays = (
362 load_dataset_and_measure_time_delays(dataset=ds, phase_center_pairs=nominal_pairs)
363 .filter(pl.col("max correlation") > 0.8)
366 * It is possible that some antennas end up getting filtered out.
367 * Sample output (note that only 76 out of 96 MI antennas survived the filter in this case):
370┌────────┬──────┬───────────────────┬───────────────────┬─────────────────┬──────────┬───────────┬───────┐
371│ AntNum ┆ Ring ┆ Rho (placeholder) ┆ Phi (placeholder) ┆ Z (placeholder) ┆ Rho ┆ Phi ┆ Z │
372│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
373│ enum ┆ u8 ┆ u16 ┆ u16 ┆ u16 ┆ f64 ┆ f64 ┆ f64 │
374╞════════╪══════╪═══════════════════╪═══════════════════╪═════════════════╪══════════╪═══════════╪═══════╡
375│ 101 ┆ 1 ┆ 0 ┆ 96 ┆ 192 ┆ 1.188 ┆ 0.0 ┆ 5.114 │
376│ 201 ┆ 2 ┆ 1 ┆ 97 ┆ 193 ┆ 2.296 ┆ 0.0 ┆ 1.457 │
377│ 301 ┆ 3 ┆ 2 ┆ 98 ┆ 194 ┆ 2.296 ┆ 0.0 ┆ 0.728 │
378│ 401 ┆ 4 ┆ 3 ┆ 99 ┆ 195 ┆ 2.296 ┆ 0.0 ┆ 0.0 │
379│ 102 ┆ 1 ┆ 4 ┆ 100 ┆ 196 ┆ 1.076821 ┆ 0.261538 ┆ 4.476 │
380│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
381│ 423 ┆ 4 ┆ 91 ┆ 187 ┆ 283 ┆ 2.295825 ┆ -0.523638 ┆ 0.0 │
382│ 124 ┆ 1 ┆ 92 ┆ 188 ┆ 284 ┆ 1.07708 ┆ -0.26224 ┆ 4.476 │
383│ 224 ┆ 2 ┆ 93 ┆ 189 ┆ 285 ┆ 2.295502 ┆ -0.261893 ┆ 1.457 │
384│ 324 ┆ 3 ┆ 94 ┆ 190 ┆ 286 ┆ 2.295502 ┆ -0.261893 ┆ 0.728 │
385│ 424 ┆ 4 ┆ 95 ┆ 191 ┆ 287 ┆ 2.295502 ┆ -0.261893 ┆ 0.0 │
386└────────┴──────┴───────────────────┴───────────────────┴─────────────────┴──────────┴───────────┴───────┘
388 * If we still pass these filtered antennas to the #objective_function,
389 SciPy's minimizer would act as if these antennas are relevant and try to still modify their
390 phase centers during the minization.
394 .select(pl.col(
r"^A1_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$"))
395 .rename(
lambda name: re.sub(
r"^A1_(.*)$",
r"\1", name))
399 .select(pl.col(
r"^A2_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$"))
400 .rename(
lambda name: re.sub(
r"^A2_(.*)$",
r"\1", name))
403 relevant_antennas = (
404 a1.vstack(a2).unique().sort(
"AntNum")
406 return relevant_antennas
409def l2_loss(residuals):
410 return np.sum(residuals**2)
412def l1_loss(residuals):
413 return np.sum(np.abs(residuals))
415def huber_loss(residuals, delta=0.3):
416 abs_r = np.abs(residuals)
417 quadratic = 0.5 * residuals**2
418 linear = delta * (abs_r - 0.5 * delta)
419 return np.sum(np.where(abs_r <= delta, quadratic, linear))
421def objective_function(phase_center_guess: pl.Series,
422 phase_center_placeholders: pl.Series,
423 pulser_directions_and_measured_time_delays: pl.DataFrame,
425 loss_type: str =
"l2",
428 r"""!Computes the expected time delays and compares them with the measured time delays,
429 designed to be run by SciPy's minimizer.
431 @param[in] phase_center_guess The output of #retrieve_relevant_antennas_from_all_pairs
432 @param[in] phase_center_placeholders The output of #retrieve_relevant_antennas_from_all_pairs
433 @param[in] pulser_directions_and_measured_time_delays The output of #load_dataset_and_measure_time_delays
434 @param[in] axis Axis to fit: \f$\rho\f$, \f$\phi\f$, or \f$z\f$
435 @param[in] loss_type The loss function type to minimize, "l2" is sum(residual square), "l1" is sum(abs(residual)), "huber_loss" is a combined of l1 and l2. l2 is default.
436 @param[in] delta paramter if you use "huber_loss"
437 @retval total_difference_bw_expected_time_delay_and_measured_time_delay A scalar
439 * In #load_dataset_and_measure_time_delays(), the `measured_time_delay` has already been computed.
440 * The job of this function is to
441 -# Given `phase_center_guess`, replace the placeholder antenna phase centers in
442 `pulser_directions_and_pairwise_time_delays`
443 -# Compute the pairwise antenna displacement vectors (call these \f$\bf{d}\f$),
444 using the updated antenna positions.
445 -# Compute the `expected time delay` based on the dot product between \f$\bf{d}\f$ and
446 `incoming pulse direction (Phi Theta)`,
447 see [this figure](@ref timedelaybydotproduct) for an illustration.
449 ┌───────────────────────────┬───────────────────────────┐
450 │ measured time delays (ns) ┆ expected time delays (ns) │
453 ╞═══════════════════════════╪═══════════════════════════╡
454 │ -0.866667 ┆ -0.853384 │
455 │ -0.866667 ┆ -0.853384 │
456 │ -7.466667 ┆ -7.487111 │
458 │ -9.466667 ┆ -9.48138 │
460 │ 8.333333 ┆ 8.33698 │
462 │ -0.866667 ┆ -0.855732 │
464 │ 1.466667 ┆ 1.463433 │
465 └───────────────────────────┴───────────────────────────┘
467 -# Quantify the difference between the expectation and the measurement; this is done by
468 summing the difference over all rows above.
469 * SciPy's minimizer tries to minimize the total difference by updating `phase_center_guess`.
471 from scipy.constants
import c
as speed_of_light
473 phi_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(0)
474 theta_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(1)
475 x_p = phi_p.cos() * theta_p.sin()
476 y_p = phi_p.sin() * theta_p.sin()
478 r_1 = pl.col(
"A1_Rho")
479 r_2 = pl.col(
"A2_Rho")
482 phi_1 = pl.col(
"A1_Phi")
483 phi_2 = pl.col(
"A2_Phi")
484 cabledelay_1 = pl.col(
"A1_CableDelay")
485 cabledelay_2 = pl.col(
"A2_CableDelay")
486 A1minusA2_dotProduct_signalDir = (
487 x_p * (r_1 * phi_1.cos() - r_2 * phi_2.cos()) +
488 y_p * (r_1 * phi_1.sin() - r_2 * phi_2.sin()) + z_p * (z_1 - z_2)
492 total_difference_bw_expected_time_delay_and_measured_time_delay = (
493 pulser_directions_and_measured_time_delays
495 pl.col(f
"A1_{axis} (placeholder)").replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A1_{axis}")).alias(f
"A1_{axis}"),
496 pl.col(f
"A2_{axis} (placeholder)").replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A2_{axis}")).alias(f
"A2_{axis}")
499 (A1minusA2_dotProduct_signalDir / speed_of_light * 1e9 + cabledelay_1-cabledelay_2).alias(
"expected time delays (ns)")
502 (pl.col(
"measured time delays (ns)")
503 - pl.col(
"expected time delays (ns)")
508 if loss_type ==
"l2":
509 loss = total_difference_bw_expected_time_delay_and_measured_time_delay.select(
510 (pl.col(
"residual")**2).sum()
512 elif loss_type ==
"l1":
513 loss = total_difference_bw_expected_time_delay_and_measured_time_delay.select(
514 pl.col(
"residual").abs().sum()
516 elif loss_type ==
"huber":
517 loss = total_difference_bw_expected_time_delay_and_measured_time_delay.select(
518 pl.when(pl.col(
"residual").abs() <= delta)
519 .then(0.5 * pl.col(
"residual")**2)
520 .otherwise(delta * (pl.col(
"residual").abs() - 0.5 * delta))
526def compute_time_delay_residuals(
527 phase_center_guess: np.ndarray,
528 phase_center_placeholders: pl.Series,
529 pulser_directions_and_measured_time_delays: pl.DataFrame,
532 r"""!Computes the time delays and residuals, given a solution. This can be used to track residuals evolution during minimization
533 @param[in] phase_center_guess The assumed solution to fill in placeholder
534 @param[in] phase_center_placeholders The output of #retrieve_relevant_antennas_from_all_pairs
535 @param[in] pulser_directions_and_measured_time_delays The output of #load_dataset_and_measure_time_delays
536 @param[in] axis Axis to fit: \f$\rho\f$, \f$\phi\f$, or \f$z\f$
537 @param[in] loss_type The loss function type to minimize, "l2" is sum(residual square), "l1" is sum(abs(residual)), "huber_loss" is a combined of l1 and l2. l2 is default.
538 @param[in] delta paramter if you use "huber_loss"
539 @retval pulser_directions_and_measured_time_delays with an additional column of pairwise delta t residual
541 from scipy.constants
import c
as speed_of_light
542 phi_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(0)
543 theta_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(1)
545 x_p = phi_p.cos() * theta_p.sin()
546 y_p = phi_p.sin() * theta_p.sin()
549 r_1 = pl.col(
"A1_Rho")
550 r_2 = pl.col(
"A2_Rho")
553 phi_1 = pl.col(
"A1_Phi")
554 phi_2 = pl.col(
"A2_Phi")
555 cabledelay_1 = pl.col(
"A1_CableDelay")
556 cabledelay_2 = pl.col(
"A2_CableDelay")
558 A1minusA2_dotProduct_signalDir = (
559 x_p * (r_1 * phi_1.cos() - r_2 * phi_2.cos()) +
560 y_p * (r_1 * phi_1.sin() - r_2 * phi_2.sin()) +
565 pulser_directions_and_measured_time_delays
567 pl.col(f
"A1_{axis} (placeholder)")
568 .replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A1_{axis}"))
569 .alias(f
"A1_{axis}"),
571 pl.col(f
"A2_{axis} (placeholder)")
572 .replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A2_{axis}"))
576 (A1minusA2_dotProduct_signalDir / speed_of_light * 1e9 + cabledelay_1 - cabledelay_2)
577 .alias(
"expected time delays (ns)")
580 (pl.col(
"measured time delays (ns)") - pl.col(
"expected time delays (ns)"))
581 .alias(
"residual (ns)")
587def _plot_calibration_result(axis: str, before: pl.DataFrame, after: np.ndarray, anttype=
'MI'):
589 @param[in] axis Axis to fit: \f$\rho\f$, \f$\phi\f$, or \f$z\f$
590 @param[in] before The output of of #retrieve_relevant_antennas_from_all_pairs
591 @param[in] after The `restul.x` of running `scipy.optimize.minimize` on #objective_function
595 
597 import matplotlib.pyplot
as plt
599 matplotlib.use(
"Agg")
601 plt.rcParams.update({
'font.size': 13})
602 fig1, ax1 = plt.subplots(nrows=4, figsize=(10, 15), tight_layout=
True)
606 pl.Series(after).alias(f
"{axis} minimization result")
610 for i
in range(before[
'Ring'].unique().len()):
612 ring = before_and_after.filter(pl.col(
"Ring") == (i + 1))
614 ring = before_and_after.filter(pl.col(
"Ring") == (i + 5))
619 if axis==
'Rho' or axis==
'Z':
620 ax1[i].scatter(ring[
'AntNum'], ring[f
"{axis} minimization result"] * 1e2 - ring[f
"{axis}"] * 1e2,
621 label=f
"Minimized - Simulated geometry", s=60)
623 diff = (ring[f
"{axis} minimization result"] - ring[f
"{axis}"]) * 180/np.pi
624 diff = (diff + 180) % 360 - 180
625 ax1[i].scatter(ring[
'AntNum'], diff,
626 label=f
"Minimized - Simulated geometry", s=60)
627 elif axis==
'CableDelay':
628 ax1[i].scatter(ring[
'AntNum'], ring[f
"{axis} minimization result"] - ring[f
"{axis}"],
629 label=f
"Minimized - Simulated Cable Delay", s=60)
631 x_ticks_to_show = [val
for val
in sorted(set(ring[
'AntNum']))]
632 ax1[i].set_xticks(x_ticks_to_show)
633 ax1[i].set_xticklabels([str(int(val))
for val
in x_ticks_to_show])
636 ax1[i].set_title(f
"Ring {i + 5}", fontsize=20)
639 ax1[i].set_title(f
"Ring {i + 1}", fontsize=20)
640 ax1[i].tick_params(axis=
'x', labelrotation=45, labelright=
True, labelsize=20)
641 ax1[i].tick_params(axis=
'y', labelright=
True, labelsize=16)
642 ax1[i].grid(alpha=.3)
645 ax1[0].set_xlim(500,525)
646 ax1[1].set_xlim(600,625)
647 ax1[2].set_xlim(700, 725)
648 ax1[3].set_xlim(800,825)
650 ax1[0].legend(fontsize=20)
652 if axis==
'Rho' or axis==
'Z':
653 fig1.supylabel(f
"$\Delta$ {axis} [cm]", fontsize=20)
655 fig1.supylabel(f
"$\Delta$ {axis} [deg]", fontsize=20)
656 elif axis==
'CableDelay':
657 fig1.supylabel(f
"$\Delta$ {axis} [ns]", fontsize=20)
660 fig1.supxlabel(
'Antenna Label', fontsize=20)
661 fig1.suptitle(
'Antenna Phase Centers (Payload Coordinates)\n', fontsize=24)
662 fig1.savefig(f
"plot/{anttype}_phase_center_fit_result_{axis}_tmp.png")
665if __name__ ==
"__main__":
667 from scipy.optimize
import minimize
668 import matplotlib.pyplot
as plt
670 matplotlib.use(
"Agg")
678 j25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/qrh.dat")
679 a25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/aug25/qrh.dat")
680 j26: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jan26/qrh.dat")
681 offset_inSIM: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/phase_center_offset_MI.dat")
683 j25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/lf_cal.dat")
684 a25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/aug25/lf.dat")
685 j26: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jan26/lf.dat")
686 offset_inSIM: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jan26/phase_center_offset_LF.dat")
688 print(
"Ant type doesn't exist!")
690 nominal_pairs = generate_antenna_phase_center_pairs_with_placeholders(qrh_dot_dat=j26, offset_inSIM=offset_inSIM, ant_type=ant_type)
693 ds: ROOT.pueo.Dataset = load_pueoEvent_Dataset(run_number = run, MC=MC)
696 Icefinalpath=f
'your_path_to_ice_final'
700 pulser_directions_and_pairwise_time_delays = (
701 load_dataset_and_measure_time_delays(dataset=ds, phase_center_pairs=nominal_pairs, MC=MC, AntType=ant_type, singlerun=run,IcefinalFileDir=Icefinalpath, event_list=event_list)
704 pulser_directions_and_pairwise_time_delays = pulser_directions_and_pairwise_time_delays.filter(pl.col(
"max correlation")> 0.80)
706 relevant_antennas = (
707 retrieve_relevant_antennas_from_all_pairs(
708 phase_center_pairs=pulser_directions_and_pairwise_time_delays
713 result = minimize(fun=objective_function,
714 x0=relevant_antennas[AXIS],
715 args=(relevant_antennas[f
"{AXIS} (placeholder)"],
716 pulser_directions_and_pairwise_time_delays, AXIS),
717 method=
'Nelder-Mead',
718 options={
'maxiter': 500_000})
721 _plot_calibration_result(axis=AXIS, before=relevant_antennas, after=result.x)