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
20dirname =
"simplesim_half_copy"
22def generate_antenna_phase_center_pairs_with_placeholders(qrh_dot_dat: Path, offset_inSIM:Path |
None =
None, ant_type=
'MI') -> pl.DataFrame:
23 r"""!Creates [antenna pairs](#antenna_pairs.generate_MI_antenna_pairs) with placeholder
24 integers as antenna phase centers.
26@param[in] qrh_dot_dat Path to `qrh.dat`
27@retval nominal_phase_center_pairs
33$ A1_Rho (placeholder) <u16>
34$ A1_Phi (placeholder) <u16>
35$ A1_Z (placeholder) <u16>
41$ A2_Rho (placeholder) <u16>
42$ A2_Phi (placeholder) <u16>
43$ A2_Z (placeholder) <u16>
49 -# Calls #antenna_attributes.read_MI_antenna_geometry() to read `qrh.dat` to obtain
50 antenna face center positions
51 -# Calls #antenna_attributes.get_MI_nominal_phase_center() to convert face centers to
53 -# Adds three columns to store placeholder values of the phase centers; these integer placeholders
54 serve as dictionary keys.
57┌────────┬────────┬──────┬────────────────┬────────────────┬───────────────┬──────────┬───────────┬───────┐
58│ AntNum ┆ AntIdx ┆ Ring ┆ Rho ┆ Phi ┆ Z ┆ Rho ┆ Phi ┆ Z │
59│ --- ┆ --- ┆ --- ┆ (placeholder) ┆ (placeholder) ┆ (placeholder) ┆ --- ┆ --- ┆ --- │
60│ enum ┆ u32 ┆ u8 ┆ --- ┆ --- ┆ --- ┆ f64 ┆ f64 ┆ f64 │
61│ ┆ ┆ ┆ u16 ┆ u16 ┆ u16 ┆ ┆ ┆ │
62╞════════╪════════╪══════╪════════════════╪════════════════╪═══════════════╪══════════╪═══════════╪═══════╡
63│ 101 ┆ 0 ┆ 1 ┆ 0 ┆ 96 ┆ 192 ┆ 1.188 ┆ 0.0 ┆ 5.114 │
64│ 201 ┆ 1 ┆ 2 ┆ 1 ┆ 97 ┆ 193 ┆ 2.296 ┆ 0.0 ┆ 1.457 │
65│ 301 ┆ 2 ┆ 3 ┆ 2 ┆ 98 ┆ 194 ┆ 2.296 ┆ 0.0 ┆ 0.728 │
66│ 401 ┆ 3 ┆ 4 ┆ 3 ┆ 99 ┆ 195 ┆ 2.296 ┆ 0.0 ┆ 0.0 │
67│ 102 ┆ 4 ┆ 1 ┆ 4 ┆ 100 ┆ 196 ┆ 1.076821 ┆ 0.261538 ┆ 4.476 │
68│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
69│ 423 ┆ 91 ┆ 4 ┆ 91 ┆ 187 ┆ 283 ┆ 2.295825 ┆ -0.523638 ┆ 0.0 │
70│ 124 ┆ 92 ┆ 1 ┆ 92 ┆ 188 ┆ 284 ┆ 1.07708 ┆ -0.26224 ┆ 4.476 │
71│ 224 ┆ 93 ┆ 2 ┆ 93 ┆ 189 ┆ 285 ┆ 2.295502 ┆ -0.261893 ┆ 1.457 │
72│ 324 ┆ 94 ┆ 3 ┆ 94 ┆ 190 ┆ 286 ┆ 2.295502 ┆ -0.261893 ┆ 0.728 │
73│ 424 ┆ 95 ┆ 4 ┆ 95 ┆ 191 ┆ 287 ┆ 2.295502 ┆ -0.261893 ┆ 0.0 │
74└────────┴────────┴──────┴────────────────┴────────────────┴───────────────┴──────────┴───────────┴───────┘
76 -# Calls #antenna_pairs.generate_MI_antenna_pairs with the above dataframe as the input.
78 from antenna_pairs
import generate_MI_antenna_pairs, generate_LF_antenna_pairs
79 from antenna_attributes
import read_antenna_geometry, get_nominal_phase_center
81 fc: pl.DataFrame = read_antenna_geometry(qrh_dot_dat=qrh_dot_dat, offset_inSIM=offset_inSIM, ant_type=ant_type)
83 nominal_and_placeholders: pl.DataFrame = (
84 get_nominal_phase_center(face_centers=fc, coordinates=
"cylindrical", ant_type=ant_type)
86 pl.arange(pl.len()).alias(
"Rho (placeholder)").cast(pl.UInt16),
87 pl.arange(pl.len(), pl.len() * 2).alias(
"Phi (placeholder)").cast(pl.UInt16),
88 pl.arange(pl.len() * 2, pl.len() * 3).alias(
"Z (placeholder)").cast(pl.UInt16),
89 pl.arange(pl.len() * 3, pl.len() * 4).alias(
"CableDelay (placeholder)").cast(pl.UInt16),
90 pl.lit(0.0).alias(
"CableDelay"),
93 "AntNum",
"AntIdx",
"Ring",
"Rho (placeholder)",
"Phi (placeholder)",
"Z (placeholder)",
"CableDelay (placeholder)",
94 pl.col(
"Rho[m]").alias(
"Rho"), pl.col(
"Phi[rad]").alias(
"Phi"), pl.col(
"Z[m]").alias(
"Z"),
"CableDelay"
99 nominal_phase_center_pairs: pl.DataFrame = (
100 generate_MI_antenna_pairs(antennas=nominal_and_placeholders)
101 .select(pl.all().exclude(
r"^A[12]_AntIdx$"))
106 nominal_phase_center_pairs: pl.DataFrame = (
107 generate_LF_antenna_pairs(antennas=nominal_and_placeholders)
108 .select(pl.all().exclude(
r"^A[12]_AntIdx$"))
111 print(
"Ant type is not supported!")
113 return nominal_phase_center_pairs
116def __get_all_run_numbers(pueo_mc_data: Path) -> [int]:
117 """! A temporary helper function of #load_dataset_and_measure_time_delays()
119 @param[in] pueo_mc_data See the parameter of #initialise.load_pueoEvent_Dataset
121 * This function retrieves the run numbers (negative sign allowed) of the `run/` subdirectories
124 * The runs are returned as a list of integers.
126 * The function is here only for testing purposes,
127 as it makes little sense to use every single run for antenna calibration.
131 run_number_pattern_ = re.compile(
r'^run([+\-]?\d+)$')
132 return [int(run_number_pattern_.match(run_directories_.name).group(1))
133 for run_directories_
in pueo_mc_data.rglob(
"run*")]
136def load_dataset_and_measure_time_delays(dataset: ROOT.pueo.Dataset,
137 phase_center_pairs: pl.DataFrame, pulser_station: str=
'taylor_dome', MC=
True, AntType=
'MI', IcefinalFileDir:str=
'', corr_fcn:str=
'') -> pl.DataFrame:
138 r"""! Uses [SciPy's correlate()](https://docs.scipy.org/doc//scipy-1.16.2/reference/generated/scipy.signal.correlate.html)
139 to "measure" the delays of [waveforms](#waveform_plots.load_waveforms) between antenna pairs
141 @param[in] dataset The output of #initialise.load_pueoEvent_Dataset
142 @param[in] phase_center_pairs The output of #generate_antenna_phase_center_pairs_with_placeholders
143 @retval pulser_directions_and_measured_time_delays
145 * The input `dataset` should have been initialized with a (valid) run number,
146 but that is not the only run that gets loaded.
147 For now, [all runs](#__get_all_run_numbers) are used.
150$ measured time delays (ns) <f64>
151$ max correlation <f64>
154$ A1_Rho (placeholder) <u16>
155$ A1_Phi (placeholder) <u16>
156$ A1_Z (placeholder) <u16>
162$ A2_Rho (placeholder) <u16>
163$ A2_Phi (placeholder) <u16>
164$ A2_Z (placeholder) <u16>
168$ incoming pulse direction (Phi Theta) <array[f64, 2]>
170 * The `dataset` is also used to load the directions of the incoming pulses
171 (\f$^\circ\phi\f$ and \f$^\circ\theta\f$ in payload coordinates).
172 * `measured time delays (ns)` is computed by the helper function
173 #_scipy_cross_correlate_all_pairs_for_one_event().
177 from locate_signal
import _get_true_direction, _get_true_pulser_direction
178 from waveform_plots
import load_waveforms, upsample_waveforms, load_waveforms_from_root
181 pulser_directions_and_measured_time_delays = pl.DataFrame()
184 all_runs: [int] = __get_all_run_numbers(
186 dataset.getDataDir(ROOT.pueo.Dataset.PUEO_MC_DATA)
192 dataset.loadRun(run, ROOT.pueo.Dataset.PUEO_MC_DATA)
193 for i
in range(dataset.N()):
197 load_waveforms(dataset=dataset)
198 .filter(pl.col(
"Pol") ==
"V")
199 .select(
"AntNum",
"Pol",
"waveforms (volts)",
"step size (nanoseconds)")
202 IcefinalFile=IcefinalFileDir+f
'run{run}/IceFinal_{run}_allTree.root'
204 load_waveforms_from_root(IcefinalFile, ev=i)
205 .filter(pl.col(
"Pol") ==
"V")
206 .select(
"AntNum",
"Pol",
"waveforms (volts)",
"step size (nanoseconds)")
208 up: pl.DataFrame = upsample_waveforms(waveforms=wf, upsample_factor=UPSAMPLE_FACTOR)
212 pulser_directions_and_measured_time_delays.vstack(
213 _scipy_cross_correlate_all_pairs_for_one_event(
214 phase_center_pairs=phase_center_pairs,
218 (pl.lit(_get_true_direction(dataset=dataset)) * np.pi / 180)
219 .cast(pl.Array(pl.Float64, 2))
220 .alias(
"incoming pulse direction (Phi Theta)")
229 pulser_directions_and_measured_time_delays.vstack(
230 _scipy_cross_correlate_all_pairs_for_one_event(
231 phase_center_pairs=phase_center_pairs,
235 (pl.lit(_get_true_pulser_direction(dataset=dataset, pulser_station=pulser_station)) * np.pi / 180)
236 .cast(pl.Array(pl.Float64, 2))
237 .alias(
"incoming pulse direction (Phi Theta)")
243 return pulser_directions_and_measured_time_delays
246def _scipy_cross_correlate_all_pairs_for_one_event(phase_center_pairs: pl.DataFrame,
247 waveforms: pl.DataFrame,
248 fcn: str=
'') -> pl.DataFrame:
249 r"""! A helper function of #load_dataset_and_measure_time_delays()
251 @param[in] phase_center_pairs The output of #generate_antenna_phase_center_pairs_with_placeholders
252 @param[in] waveforms The output of #waveform_plots.load_waveforms
253 @retval measured_time_delays
257$ measured time delays (ns) <f64>
258$ max correlation <f64>
261$ A1_Rho (placeholder) <u16>
262$ A1_Phi (placeholder) <u16>
263$ A1_Z (placeholder) <u16>
269$ A2_Rho (placeholder) <u16>
270$ A2_Phi (placeholder) <u16>
271$ A2_Z (placeholder) <u16>
277 -# [Load the waveforms](#waveform_plots.load_waveforms) of both antennas in the antenna pair
278 -# Cross correlate the waveform pair
279 -# Figure out by how much one needs to shift the second waveform against the first
280 in order to achieve the maximum correlation.
281 -# The above is called `measured time delays (ns)` in the output dataframe.
282 -# For more details, see [Explanation](#scipy_corr_expl).
284 from scipy.signal
import correlate, correlation_lags
288 signal_length = len(waveforms[
"waveforms (volts)"][0])
289 CORRLAGS: np.ndarray = correlation_lags(signal_length, signal_length)
291 STEP_SIZE = waveforms[
"step size (nanoseconds)"][0]
293 assert np.mean(waveforms[
"step size (nanoseconds)"].to_numpy()) - STEP_SIZE < 1e-10
296 corr_func = zncc_pairwise_correlation
298 corr_func = pairwise_correlation
300 measured_time_delays = (
302 .join(waveforms, left_on=
"A1_AntNum", right_on=
"AntNum")
303 .join(waveforms, left_on=[
"A2_AntNum",
"Pol"], right_on=[
"AntNum",
"Pol"])
305 pl.struct(
"waveforms (volts)",
"waveforms (volts)_right")
306 .map_batches(corr_func).alias(
"correlation"),
309 pl.col(
"correlation").arr.max().alias(
"max correlation"),
310 pl.col(
"correlation").arr.arg_max().alias(
"max correlation idx"),
313 pl.col(
"max correlation idx").map_batches(
lambda idx: CORRLAGS[idx] * STEP_SIZE)
314 .alias(
"measured time delays (ns)")
317 "measured time delays (ns)",
"max correlation",
318 r"^A[12]_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$",
322 return measured_time_delays
324def zncc_pairwise_correlation(s):
325 from scipy.signal
import correlate
328 (wf1 - np.mean(wf1)) / np.std(wf1),
329 (wf2 - np.mean(wf2)) / np.std(wf2),
332 for wf2, wf1
in s.to_numpy()
335def pairwise_correlation(s):
336 from scipy.signal
import correlate
343 for wf2, wf1
in s.to_numpy()
345def retrieve_relevant_antennas_from_all_pairs(phase_center_pairs: pl.DataFrame):
346 r"""! Retrieves the (unique) antennas, given a bunch of antenna pairs.
348 @param[in] phase_center_pairs The output of #load_dataset_and_measure_time_delays
349 @retval relevant_antennas
351 * This step is not trivial, as explained below.
352 * It is conceivable that not all antennas receive signals clear enough for calibration purposes
353 (resulting in weak correlations).
354 * As such, the weakly correlated antenna pairs should be filtered out.
357 pulser_directions_and_pairwise_time_delays = (
358 load_dataset_and_measure_time_delays(dataset=ds, phase_center_pairs=nominal_pairs)
359 .filter(pl.col("max correlation") > 0.8)
362 * It is possible that some antennas end up getting filtered out.
363 * Sample output (note that only 76 out of 96 MI antennas survived the filter in this case):
366┌────────┬──────┬───────────────────┬───────────────────┬─────────────────┬──────────┬───────────┬───────┐
367│ AntNum ┆ Ring ┆ Rho (placeholder) ┆ Phi (placeholder) ┆ Z (placeholder) ┆ Rho ┆ Phi ┆ Z │
368│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
369│ enum ┆ u8 ┆ u16 ┆ u16 ┆ u16 ┆ f64 ┆ f64 ┆ f64 │
370╞════════╪══════╪═══════════════════╪═══════════════════╪═════════════════╪══════════╪═══════════╪═══════╡
371│ 101 ┆ 1 ┆ 0 ┆ 96 ┆ 192 ┆ 1.188 ┆ 0.0 ┆ 5.114 │
372│ 201 ┆ 2 ┆ 1 ┆ 97 ┆ 193 ┆ 2.296 ┆ 0.0 ┆ 1.457 │
373│ 301 ┆ 3 ┆ 2 ┆ 98 ┆ 194 ┆ 2.296 ┆ 0.0 ┆ 0.728 │
374│ 401 ┆ 4 ┆ 3 ┆ 99 ┆ 195 ┆ 2.296 ┆ 0.0 ┆ 0.0 │
375│ 102 ┆ 1 ┆ 4 ┆ 100 ┆ 196 ┆ 1.076821 ┆ 0.261538 ┆ 4.476 │
376│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
377│ 423 ┆ 4 ┆ 91 ┆ 187 ┆ 283 ┆ 2.295825 ┆ -0.523638 ┆ 0.0 │
378│ 124 ┆ 1 ┆ 92 ┆ 188 ┆ 284 ┆ 1.07708 ┆ -0.26224 ┆ 4.476 │
379│ 224 ┆ 2 ┆ 93 ┆ 189 ┆ 285 ┆ 2.295502 ┆ -0.261893 ┆ 1.457 │
380│ 324 ┆ 3 ┆ 94 ┆ 190 ┆ 286 ┆ 2.295502 ┆ -0.261893 ┆ 0.728 │
381│ 424 ┆ 4 ┆ 95 ┆ 191 ┆ 287 ┆ 2.295502 ┆ -0.261893 ┆ 0.0 │
382└────────┴──────┴───────────────────┴───────────────────┴─────────────────┴──────────┴───────────┴───────┘
384 * If we still pass these filtered antennas to the #objective_function,
385 SciPy's minimizer would act as if these antennas are relevant and try to still modify their
386 phase centers during the minization.
390 .select(pl.col(
r"^A1_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$"))
391 .rename(
lambda name: re.sub(
r"^A1_(.*)$",
r"\1", name))
395 .select(pl.col(
r"^A2_(?:AntNum|Ring|Rho|Phi|Z|CableDelay)(?: \(placeholder\))?$"))
396 .rename(
lambda name: re.sub(
r"^A2_(.*)$",
r"\1", name))
399 relevant_antennas = (
400 a1.vstack(a2).unique().sort(
"AntNum")
402 return relevant_antennas
405def objective_function(phase_center_guess: pl.Series,
406 phase_center_placeholders: pl.Series,
407 pulser_directions_and_measured_time_delays: pl.DataFrame,
409 r"""!Computes the expected time delays and compares them with the measured time delays,
410 designed to be run by SciPy's minimizer.
412 @param[in] phase_center_guess The output of #retrieve_relevant_antennas_from_all_pairs
413 @param[in] phase_center_placeholders The output of #retrieve_relevant_antennas_from_all_pairs
414 @param[in] pulser_directions_and_measured_time_delays The output of #load_dataset_and_measure_time_delays
415 @param[in] axis Axis to fit: \f$\rho\f$, \f$\phi\f$, or \f$z\f$
416 @retval total_difference_bw_expected_time_delay_and_measured_time_delay A scalar
418 * In #load_dataset_and_measure_time_delays(), the `measured_time_delay` has already been computed.
419 * The job of this function is to
420 -# Given `phase_center_guess`, replace the placeholder antenna phase centers in
421 `pulser_directions_and_pairwise_time_delays`
422 -# Compute the pairwise antenna displacement vectors (call these \f$\bf{d}\f$),
423 using the updated antenna positions.
424 -# Compute the `expected time delay` based on the dot product between \f$\bf{d}\f$ and
425 `incoming pulse direction (Phi Theta)`,
426 see [this figure](@ref timedelaybydotproduct) for an illustration.
428 ┌───────────────────────────┬───────────────────────────┐
429 │ measured time delays (ns) ┆ expected time delays (ns) │
432 ╞═══════════════════════════╪═══════════════════════════╡
433 │ -0.866667 ┆ -0.853384 │
434 │ -0.866667 ┆ -0.853384 │
435 │ -7.466667 ┆ -7.487111 │
437 │ -9.466667 ┆ -9.48138 │
439 │ 8.333333 ┆ 8.33698 │
441 │ -0.866667 ┆ -0.855732 │
443 │ 1.466667 ┆ 1.463433 │
444 └───────────────────────────┴───────────────────────────┘
446 -# Quantify the difference between the expectation and the measurement; this is done by
447 summing the difference over all rows above.
448 * SciPy's minimizer tries to minimize the total difference by updating `phase_center_guess`.
450 from scipy.constants
import c
as speed_of_light
452 phi_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(0)
453 theta_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(1)
454 x_p = phi_p.cos() * theta_p.sin()
455 y_p = phi_p.sin() * theta_p.sin()
458 r_1 = pl.col(
"A1_Rho")
459 r_2 = pl.col(
"A2_Rho")
462 phi_1 = pl.col(
"A1_Phi")
463 phi_2 = pl.col(
"A2_Phi")
464 cabledelay_1 = pl.col(
"A1_CableDelay")
465 cabledelay_2 = pl.col(
"A2_CableDelay")
467 A1minusA2_dotProduct_signalDir = (
468 x_p * (r_1 * phi_1.cos() - r_2 * phi_2.cos()) +
469 y_p * (r_1 * phi_1.sin() - r_2 * phi_2.sin()) + z_p * (z_1 - z_2)
473 total_difference_bw_expected_time_delay_and_measured_time_delay = (
474 pulser_directions_and_measured_time_delays
476 pl.col(f
"A1_{axis} (placeholder)").replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A1_{axis}")).alias(f
"A1_{axis}"),
477 pl.col(f
"A2_{axis} (placeholder)").replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A2_{axis}")).alias(f
"A2_{axis}")
480 (A1minusA2_dotProduct_signalDir / speed_of_light * 1e9 + cabledelay_1-cabledelay_2).alias(
"expected time delays (ns)")
483 ((pl.col(
"measured time delays (ns)") - pl.col(
"expected time delays (ns)"))**2)
484 .sum().alias(
"time delay difference")
487 return total_difference_bw_expected_time_delay_and_measured_time_delay.item()
489def objective_function_MultipleAxes(phase_center_guess: np.ndarray,
490 phase_center_placeholders_list: list[pl.Series],
491 pulser_directions_and_measured_time_delays: pl.DataFrame,
492 axis_list:list[str]) -> int:
494 from scipy.constants
import c
as speed_of_light
495 N_ant_involved = len(phase_center_placeholders_list[0])
497 phi_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(0)
498 theta_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(1)
499 x_p = phi_p.cos() * theta_p.sin()
500 y_p = phi_p.sin() * theta_p.sin()
503 r_1 = pl.col(
"A1_Rho")
504 r_2 = pl.col(
"A2_Rho")
507 phi_1 = pl.col(
"A1_Phi")
508 phi_2 = pl.col(
"A2_Phi")
510 A1minusA2_dotProduct_signalDir = (
511 x_p * (r_1 * phi_1.cos() - r_2 * phi_2.cos()) +
512 y_p * (r_1 * phi_1.sin() - r_2 * phi_2.sin()) + z_p * (z_1 - z_2)
518 df = pulser_directions_and_measured_time_delays
520 for i, AXIS
in enumerate(axis_list):
521 placeholders = phase_center_placeholders_list[i]
522 end_idx = start_idx + len(placeholders)
525 df = df.with_columns(
526 pl.col(f
"A1_{AXIS} (placeholder)")
527 .replace_strict(placeholders, phase_center_guess[start_idx:end_idx])
528 .alias(f
"A1_{AXIS}"),
530 pl.col(f
"A2_{AXIS} (placeholder)")
531 .replace_strict(placeholders, phase_center_guess[start_idx:end_idx])
538 df = df.with_columns(
539 (A1minusA2_dotProduct_signalDir / speed_of_light * 1e9).alias(
"expected time delays (ns)")
543 total_difference_bw_expected_time_delay_and_measured_time_delay = df.select(
544 ((pl.col(
"measured time delays (ns)") - pl.col(
"expected time delays (ns)"))**2)
546 .alias(
"time delay difference")
549 return total_difference_bw_expected_time_delay_and_measured_time_delay.item()
551def compute_time_delay_residuals(
552 phase_center_guess: np.ndarray,
553 phase_center_placeholders: pl.Series,
554 pulser_directions_and_measured_time_delays: pl.DataFrame,
557 from scipy.constants
import c
as speed_of_light
558 phi_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(0)
559 theta_p = pl.col(
"incoming pulse direction (Phi Theta)").arr.get(1)
561 x_p = phi_p.cos() * theta_p.sin()
562 y_p = phi_p.sin() * theta_p.sin()
565 r_1 = pl.col(
"A1_Rho")
566 r_2 = pl.col(
"A2_Rho")
569 phi_1 = pl.col(
"A1_Phi")
570 phi_2 = pl.col(
"A2_Phi")
571 cabledelay_1 = pl.col(
"A1_CableDelay")
572 cabledelay_2 = pl.col(
"A2_CableDelay")
574 A1minusA2_dotProduct_signalDir = (
575 x_p * (r_1 * phi_1.cos() - r_2 * phi_2.cos()) +
576 y_p * (r_1 * phi_1.sin() - r_2 * phi_2.sin()) +
581 pulser_directions_and_measured_time_delays
583 pl.col(f
"A1_{axis} (placeholder)")
584 .replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A1_{axis}"))
585 .alias(f
"A1_{axis}"),
587 pl.col(f
"A2_{axis} (placeholder)")
588 .replace_strict(phase_center_placeholders, phase_center_guess, default=pl.col(f
"A2_{axis}"))
592 (A1minusA2_dotProduct_signalDir / speed_of_light * 1e9 + cabledelay_1 - cabledelay_2)
593 .alias(
"expected time delays (ns)")
596 (pl.col(
"measured time delays (ns)") - pl.col(
"expected time delays (ns)"))
597 .alias(
"residual (ns)")
603def _plot_calibration_result(axis: str, before: pl.DataFrame, after: np.ndarray, anttype=
'MI'):
605 @param[in] axis Axis to fit: \f$\rho\f$, \f$\phi\f$, or \f$z\f$
606 @param[in] before The output of of #retrieve_relevant_antennas_from_all_pairs
607 @param[in] after The `restul.x` of running `scipy.optimize.minimize` on #objective_function
611 
613 import matplotlib.pyplot
as plt
615 matplotlib.use(
"Agg")
617 plt.rcParams.update({
'font.size': 13})
618 fig1, ax1 = plt.subplots(nrows=4, figsize=(10, 15), tight_layout=
True)
622 pl.Series(after).alias(f
"{axis} minimization result")
626 before_and_after = before_and_after.with_columns(
627 pl.col(
"AntNum").cast(pl.Int32)
629 for i
in range(before[
'Ring'].unique().len()):
631 ring = before_and_after.filter(pl.col(
"Ring") == (i + 1))
633 ring = before_and_after.filter(pl.col(
"Ring") == (i + 5))
639 if axis==
'Rho' or axis==
'Z':
640 ax1[i].scatter(ring[
'AntNum'], ring[f
"{axis} minimization result"] * 1e2 - ring[f
"{axis}"] * 1e2,
641 label=f
"Minimized - Simulated geometry", s=60)
643 diff = (ring[f
"{axis} minimization result"] - ring[f
"{axis}"]) * 180/np.pi
644 diff = (diff + 180) % 360 - 180
645 ax1[i].scatter(ring[
'AntNum'], diff,
646 label=f
"Minimized - Simulated geometry", s=60)
647 elif axis==
'CableDelay':
648 ax1[i].scatter(ring[
'AntNum'], ring[f
"{axis} minimization result"] - ring[f
"{axis}"],
649 label=f
"Minimized - Simulated Cable Delay", s=60)
651 x_ticks_to_show = [val
for val
in sorted(set(ring[
'AntNum']))]
652 ax1[i].set_xticks(x_ticks_to_show)
653 ax1[i].set_xticklabels([str(int(val))
for val
in x_ticks_to_show])
656 ax1[i].set_title(f
"Ring {i + 5}", fontsize=20)
659 ax1[i].set_title(f
"Ring {i + 1}", fontsize=20)
660 ax1[i].tick_params(axis=
'x', labelrotation=45, labelright=
True, labelsize=20)
661 ax1[i].tick_params(axis=
'y', labelright=
True, labelsize=16)
662 ax1[i].grid(alpha=.3)
665 ax1[0].set_xlim(500,525)
666 ax1[1].set_xlim(600,625)
667 ax1[2].set_xlim(700, 725)
668 ax1[3].set_xlim(800,825)
670 ax1[0].legend(fontsize=20)
672 if axis==
'Rho' or axis==
'Z':
673 fig1.supylabel(f
"$\Delta$ {axis} [cm]", fontsize=20)
675 fig1.supylabel(f
"$\Delta$ {axis} [deg]", fontsize=20)
676 elif axis==
'CableDelay':
677 fig1.supylabel(f
"$\Delta$ {axis} [ns]", fontsize=20)
680 fig1.supxlabel(
'Antenna Label', fontsize=20)
681 fig1.suptitle(
'Antenna Phase Centers (Payload Coordinates)\n', fontsize=24)
682 fig1.savefig(f
"plot/{dirname}/{anttype}_phase_center_fit_result_{axis}_tmp.png")
685if __name__ ==
"__main__":
687 from scipy.optimize
import minimize
688 import matplotlib.pyplot
as plt
690 matplotlib.use(
"Agg")
696 j25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/qrh.dat")
697 a25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/aug25/qrh.dat")
698 offset_inSIM: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/phase_center_offset_MI.dat")
700 j25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/lf_cal.dat")
701 a25: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/aug25/lf.dat")
702 offset_inSIM: Path = os.environ.get(
"PUEO_UTIL_INSTALL_DIR") / Path(
"share/pueo/geometry/jun25/phase_center_offset_LF.dat")
704 print(
"Ant type doesn't exist!")
706 nominal_pairs = generate_antenna_phase_center_pairs_with_placeholders(qrh_dot_dat=j25, offset_inSIM=offset_inSIM, ant_type=ant_type)
709 ds: ROOT.pueo.Dataset = load_pueoEvent_Dataset(pueo_mc_data=Path(f
"/users/PAS0654/yuchieh/pueo_2025/components/pueoAnalysisTools/SharedUtils/{dirname}/"), run_number=20)
711 Icefinalpath=f
'./{dirname}/'
715 pulser_directions_and_pairwise_time_delays = (
716 load_dataset_and_measure_time_delays(dataset=ds, phase_center_pairs=nominal_pairs, MC=
True, AntType=ant_type, IcefinalFileDir=Icefinalpath, corr_fcn=
'ZNCC')
719 relevant_antennas = (
720 retrieve_relevant_antennas_from_all_pairs(
721 phase_center_pairs=pulser_directions_and_pairwise_time_delays
726 result = minimize(fun=objective_function,
727 x0=relevant_antennas[AXIS],
728 args=(relevant_antennas[f
"{AXIS} (placeholder)"],
729 pulser_directions_and_pairwise_time_delays, AXIS),
730 method=
'Nelder-Mead',
731 options={
'maxiter': 500_000})
734 _plot_calibration_result(axis=AXIS, before=relevant_antennas, after=result.x)