pygama 2.3.8a2__tar.gz → 2.3.8a3__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.
Files changed (141) hide show
  1. {pygama-2.3.8a2/src/pygama.egg-info → pygama-2.3.8a3}/PKG-INFO +1 -1
  2. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/_version.py +3 -3
  3. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/build_evt.py +6 -0
  4. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/hit/build_hit.py +3 -0
  5. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/AoE_cal.py +330 -111
  6. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/dplms_ge_dict.py +11 -42
  7. {pygama-2.3.8a2 → pygama-2.3.8a3/src/pygama.egg-info}/PKG-INFO +1 -1
  8. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/test_build_evt.py +27 -0
  9. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/hit/test_build_hit.py +27 -0
  10. {pygama-2.3.8a2 → pygama-2.3.8a3}/LICENSE +0 -0
  11. {pygama-2.3.8a2 → pygama-2.3.8a3}/README.md +0 -0
  12. {pygama-2.3.8a2 → pygama-2.3.8a3}/pyproject.toml +0 -0
  13. {pygama-2.3.8a2 → pygama-2.3.8a3}/setup.cfg +0 -0
  14. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/__init__.py +0 -0
  15. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/cli.py +0 -0
  16. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/__init__.py +0 -0
  17. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/aggregators.py +0 -0
  18. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/build_tcm.py +0 -0
  19. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/__init__.py +0 -0
  20. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/geds.py +0 -0
  21. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/larveto.py +0 -0
  22. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/legend.py +0 -0
  23. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/spms.py +0 -0
  24. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/modules/xtalk.py +0 -0
  25. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/tcm.py +0 -0
  26. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/evt/utils.py +0 -0
  27. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/flow/__init__.py +0 -0
  28. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/flow/data_loader.py +0 -0
  29. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/flow/file_db.py +0 -0
  30. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/flow/utils.py +0 -0
  31. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/hit/__init__.py +0 -0
  32. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/logging.py +0 -0
  33. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/__init__.py +0 -0
  34. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/binned_fitting.py +0 -0
  35. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/distributions.py +0 -0
  36. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/__init__.py +0 -0
  37. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/crystal_ball.py +0 -0
  38. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/error_function.py +0 -0
  39. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/exgauss.py +0 -0
  40. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/exponential.py +0 -0
  41. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss.py +0 -0
  42. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss_on_exgauss.py +0 -0
  43. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss_on_exponential.py +0 -0
  44. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss_on_linear.py +0 -0
  45. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss_on_step.py +0 -0
  46. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/gauss_on_uniform.py +0 -0
  47. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/hpge_peak.py +0 -0
  48. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/linear.py +0 -0
  49. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/moyal.py +0 -0
  50. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/poisson.py +0 -0
  51. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/polynomial.py +0 -0
  52. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/pygama_continuous.py +0 -0
  53. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/step.py +0 -0
  54. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/sum_dists.py +0 -0
  55. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/triple_gauss_on_double_step.py +0 -0
  56. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/functions/uniform.py +0 -0
  57. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/histogram.py +0 -0
  58. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/hpge_peak_fitting.py +0 -0
  59. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/least_squares.py +0 -0
  60. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/unbinned_fitting.py +0 -0
  61. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/units.py +0 -0
  62. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/math/utils.py +0 -0
  63. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/__init__.py +0 -0
  64. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/data_cleaning.py +0 -0
  65. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/dsp_optimize.py +0 -0
  66. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/energy_cal.py +0 -0
  67. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/energy_optimisation.py +0 -0
  68. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/lq_cal.py +0 -0
  69. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/noise_optimization.py +0 -0
  70. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/pz_correct.py +0 -0
  71. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/survival_fractions.py +0 -0
  72. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/pargen/utils.py +0 -0
  73. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama/utils.py +0 -0
  74. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/SOURCES.txt +0 -0
  75. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/dependency_links.txt +0 -0
  76. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/entry_points.txt +0 -0
  77. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/not-zip-safe +0 -0
  78. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/requires.txt +0 -0
  79. {pygama-2.3.8a2 → pygama-2.3.8a3}/src/pygama.egg-info/top_level.txt +0 -0
  80. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/configs/icpc-dsp-config.json +0 -0
  81. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/configs/sipm-dplms-config.json +0 -0
  82. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/configs/sipm-dsp-config.json +0 -0
  83. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/conftest.py +0 -0
  84. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/configs/basic-evt-config.yaml +0 -0
  85. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/configs/query-test-evt-config.json +0 -0
  86. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/configs/spms-module-config.yaml +0 -0
  87. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/configs/spms-p13-config.yaml +0 -0
  88. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/configs/vov-test-evt-config.json +0 -0
  89. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/modules/larveto.py +0 -0
  90. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/test_build_tcm.py +0 -0
  91. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/test_evt_utils.py +0 -0
  92. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/test_geds.py +0 -0
  93. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/evt/test_xtalk.py +0 -0
  94. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/configs/config.json +0 -0
  95. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/configs/data-loader-config.json +0 -0
  96. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/configs/filedb-config.json +0 -0
  97. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/configs/nested/config.json +0 -0
  98. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/configs/nested/data-loader-config-nested.json +0 -0
  99. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/conftest.py +0 -0
  100. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/test_data_loader.py +0 -0
  101. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/test_filedb.py +0 -0
  102. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/flow/test_flow_utils.py +0 -0
  103. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/hit/configs/aggregations-hit-config.json +0 -0
  104. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/hit/configs/basic-hit-config.json +0 -0
  105. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/hit/configs/hit-multi-config.json +0 -0
  106. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_crystal_ball.py +0 -0
  107. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_error_function.py +0 -0
  108. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_exgauss.py +0 -0
  109. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_exponential.py +0 -0
  110. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss.py +0 -0
  111. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss_on_exgauss.py +0 -0
  112. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss_on_exponential.py +0 -0
  113. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss_on_linear.py +0 -0
  114. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss_on_step.py +0 -0
  115. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_gauss_on_uniform.py +0 -0
  116. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_hpge_peak.py +0 -0
  117. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_linear.py +0 -0
  118. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_moyal.py +0 -0
  119. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_numba_frozen.py +0 -0
  120. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_poisson.py +0 -0
  121. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_polynomial.py +0 -0
  122. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_step.py +0 -0
  123. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_sum_dists.py +0 -0
  124. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_triple_gauss_on_double_step.py +0 -0
  125. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/functions/test_uniform.py +0 -0
  126. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_binned_fitting.py +0 -0
  127. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_distribution_selector_in_sum_dists.py +0 -0
  128. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_hpge_peak_fitting.py +0 -0
  129. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_iminuit_integration.py +0 -0
  130. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_least_squares.py +0 -0
  131. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_math_histogram.py +0 -0
  132. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_math_utils.py +0 -0
  133. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/math/test_unbinned_fitting.py +0 -0
  134. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_aoecal.py +0 -0
  135. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_datacleaning.py +0 -0
  136. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_dsp_optimize.py +0 -0
  137. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_ecal.py +0 -0
  138. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_energy_optimization.py +0 -0
  139. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_lqcal.py +0 -0
  140. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/pargen/test_pz_correct.py +0 -0
  141. {pygama-2.3.8a2 → pygama-2.3.8a3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygama
3
- Version: 2.3.8a2
3
+ Version: 2.3.8a3
4
4
  Summary: Python package for data processing and analysis
5
5
  Author: The LEGEND collaboration
6
6
  Maintainer: The LEGEND collaboration
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.3.8a2'
22
- __version_tuple__ = version_tuple = (2, 3, 8, 'a2')
21
+ __version__ = version = '2.3.8a3'
22
+ __version_tuple__ = version_tuple = (2, 3, 8, 'a3')
23
23
 
24
- __commit_id__ = commit_id = 'g13d41716a'
24
+ __commit_id__ = commit_id = 'g5a070cc53'
@@ -308,6 +308,9 @@ def build_evt_cols(
308
308
  if "lgdo_attrs" in v:
309
309
  obj.attrs |= v["lgdo_attrs"]
310
310
 
311
+ if "description" in v:
312
+ obj.attrs["description"] = v["description"]
313
+
311
314
  # else we build the event entry
312
315
  else:
313
316
  if "channels" not in v:
@@ -357,6 +360,9 @@ def build_evt_cols(
357
360
  if "lgdo_attrs" in v:
358
361
  obj.attrs |= v["lgdo_attrs"]
359
362
 
363
+ if "description" in v:
364
+ obj.attrs["description"] = v["description"]
365
+
360
366
  # cast to type, if required
361
367
  # hijack the poor LGDO
362
368
  if "dtype" in v:
@@ -156,6 +156,9 @@ def build_hit(
156
156
  if "lgdo_attrs" in info:
157
157
  outcol.attrs |= info["lgdo_attrs"]
158
158
 
159
+ if "description" in info:
160
+ outcol.attrs["description"] = info["description"]
161
+
159
162
  log.debug("made new column %r=%r", outname, outcol)
160
163
  outtbl_obj.add_column(outname, outcol)
161
164
 
@@ -18,6 +18,7 @@ import pandas as pd
18
18
  from iminuit import Minuit, cost
19
19
  from matplotlib.colors import LogNorm
20
20
  from scipy.stats import chi2
21
+ from sklearn.covariance import MinCovDet
21
22
 
22
23
  import pygama.math.binned_fitting as pgf
23
24
  import pygama.math.histogram as pgh
@@ -743,6 +744,290 @@ def interpolate_consecutive(tstamps, means, times, aoe_param, output_name):
743
744
  return out_dict
744
745
 
745
746
 
747
+ def bimodal_dt_fit(
748
+ data: pd.DataFrame,
749
+ aoe_param: str,
750
+ dt_param: str,
751
+ fit_selection: str,
752
+ cal_energy_param: str,
753
+ pdf,
754
+ debug_mode: bool = False,
755
+ display: int = 0,
756
+ **_kwargs,
757
+ ) -> tuple[float, dict]:
758
+ """
759
+ Calculates the correction needed to align the two drift time regions for ICPC detectors.
760
+ This is done by fitting the two drift time peaks in the drift time spectrum and then
761
+ fitting the A/E peaks in each of these regions. A simple linear correction is then applied
762
+ to align these regions.
763
+
764
+ Parameters
765
+ ----------
766
+ data
767
+ DataFrame containing the data.
768
+ aoe_param
769
+ Name of the A/E parameter to use as starting point.
770
+ dt_param
771
+ Name of the drift time parameter.
772
+ fit_selection
773
+ Selection string for the fit.
774
+ cal_energy_param
775
+ Name of the calibrated energy parameter.
776
+ pdf
777
+ PDF to use for the A/E fit.
778
+ debug_mode
779
+ If True, re-raises exceptions instead of logging them.
780
+ display
781
+ Plot verbosity level.
782
+
783
+ Returns
784
+ -------
785
+ alpha
786
+ Linear correction coefficient.
787
+ dt_res_dict
788
+ Dictionary containing intermediate fit results.
789
+ """
790
+ log.info("Starting A/E drift time correction (bimodal_dt_fit mode)")
791
+ dt_res_dict = {}
792
+ alpha = 0
793
+ try:
794
+ dep_events = data.query(
795
+ f"{fit_selection}&{cal_energy_param}>1582&{cal_energy_param}<1602&{cal_energy_param}=={cal_energy_param}&{aoe_param}=={aoe_param}"
796
+ )
797
+
798
+ hist, bins, var = pgh.get_hist(
799
+ dep_events[aoe_param],
800
+ bins=500,
801
+ )
802
+ bin_cs = (bins[1:] + bins[:-1]) / 2
803
+ mu = bin_cs[np.argmax(hist)]
804
+ aoe_range = [mu * 0.9, mu * 1.1]
805
+
806
+ dt_range = [
807
+ np.nanpercentile(dep_events[dt_param], 1),
808
+ np.nanpercentile(dep_events[dt_param], 99),
809
+ ]
810
+
811
+ dt_res_dict["final_selection"] = (
812
+ f"{aoe_param}>{aoe_range[0]}&{aoe_param}<{aoe_range[1]}&{dt_param}>{dt_range[0]}&{dt_param}<{dt_range[1]}&{dt_param}=={dt_param}"
813
+ )
814
+
815
+ final_df = dep_events.query(dt_res_dict["final_selection"])
816
+
817
+ hist, bins, var = pgh.get_hist(
818
+ final_df[dt_param],
819
+ dx=32,
820
+ range=(
821
+ np.nanmin(final_df[dt_param]),
822
+ np.nanmax(final_df[dt_param]),
823
+ ),
824
+ )
825
+
826
+ bcs = pgh.get_bin_centers(bins)
827
+ mus = bcs[pgc.get_i_local_maxima(hist / (np.sqrt(var) + 10**-99), 2)]
828
+ pk_pars, _pk_covs = pgc.hpge_fit_energy_peak_tops(
829
+ hist,
830
+ bins,
831
+ var=var,
832
+ peak_locs=mus,
833
+ n_to_fit=5,
834
+ )
835
+
836
+ mus = pk_pars[:, 0]
837
+ sigmas = pk_pars[:, 1]
838
+ amps = pk_pars[:, 2]
839
+
840
+ if len(mus) > 2:
841
+ ids = np.array(
842
+ sorted([np.argmax(amps), np.argmax(amps[amps != np.argmax(amps)])])
843
+ )
844
+ else:
845
+ ids = np.full(len(mus), True, dtype=bool)
846
+ mus = mus[ids]
847
+ sigmas = sigmas[ids]
848
+ amps = amps[ids]
849
+
850
+ dt_res_dict["dt_fit"] = {"mus": mus, "sigmas": sigmas, "amps": amps}
851
+
852
+ if len(mus) < 2:
853
+ log.info("Only 1 drift time peak found, no correction needed")
854
+
855
+ else:
856
+ aoe_grp1 = dt_res_dict["aoe_grp1"] = (
857
+ f"{dt_param}>{mus[0] - 2 * sigmas[0]} & {dt_param}<{mus[0] + 2 * sigmas[0]}"
858
+ )
859
+ aoe_grp2 = dt_res_dict["aoe_grp2"] = (
860
+ f"{dt_param}>{mus[1] - 2 * sigmas[1]} & {dt_param}<{mus[1] + 2 * sigmas[1]}"
861
+ )
862
+
863
+ aoe_pars, aoe_errs, _, _ = unbinned_aoe_fit(
864
+ final_df.query(aoe_grp1)[aoe_param], pdf=pdf, display=display
865
+ )
866
+ dt_res_dict["aoe_fit1"] = {
867
+ "pars": aoe_pars.to_dict(),
868
+ "errs": aoe_errs.to_dict(),
869
+ }
870
+
871
+ aoe_pars2, aoe_errs2, _, _ = unbinned_aoe_fit(
872
+ final_df.query(aoe_grp2)[aoe_param], pdf=pdf, display=display
873
+ )
874
+ dt_res_dict["aoe_fit2"] = {
875
+ "pars": aoe_pars2.to_dict(),
876
+ "errs": aoe_errs2.to_dict(),
877
+ }
878
+
879
+ try:
880
+ alpha = (aoe_pars2["mu"] - aoe_pars["mu"]) / (
881
+ (mus[0] * aoe_pars["mu"]) - (mus[1] * aoe_pars2["mu"])
882
+ )
883
+ except ZeroDivisionError:
884
+ alpha = 0
885
+
886
+ dt_res_dict["alpha"] = alpha
887
+ log.info("dtcorr successful alpha: %s", alpha)
888
+
889
+ except BaseException as e:
890
+ if isinstance(e, KeyboardInterrupt) or debug_mode:
891
+ raise e
892
+ log.error("Drift time correction (bimodal_dt_fit) failed")
893
+
894
+ return alpha, dt_res_dict
895
+
896
+
897
+ def mcdrift(
898
+ data: pd.DataFrame,
899
+ aoe_param: str,
900
+ dt_param: str,
901
+ fit_selection: str,
902
+ cal_energy_param: str,
903
+ debug_mode: bool = False,
904
+ **_kwargs,
905
+ ) -> tuple[float, dict]:
906
+ """
907
+ Calculates the drift time correction for the A/E parameter using the
908
+ Minimum Covariance Determinant (MCD) method and PCA on the (AoE, dt_eff) distribution.
909
+
910
+ Parameters
911
+ ----------
912
+ data
913
+ DataFrame containing the data.
914
+ aoe_param
915
+ Name of the A/E parameter to use as starting point.
916
+ dt_param
917
+ Name of the drift time parameter.
918
+ fit_selection
919
+ Selection string for the fit.
920
+ cal_energy_param
921
+ Name of the calibrated energy parameter.
922
+ debug_mode
923
+ If True, re-raises exceptions instead of logging them.
924
+
925
+ Returns
926
+ -------
927
+ alpha
928
+ Linear correction coefficient.
929
+ dt_res_dict
930
+ Dictionary containing intermediate fit results.
931
+ """
932
+ log.info("Starting A/E drift time correction (MCD mode)")
933
+
934
+ try:
935
+ CC_events = data.query(
936
+ f"{fit_selection}&{cal_energy_param}=={cal_energy_param}&{aoe_param}=={aoe_param}&{dt_param}=={dt_param}"
937
+ )
938
+ except Exception as e:
939
+ if debug_mode:
940
+ raise e
941
+ log.error("MCD drift: event selection query failed: %s", e)
942
+ return 0, {}
943
+
944
+ peaks = np.array([1080, 1094, 1459, 1512, 1552, 1620, 1650, 1670, 1830, 2105])
945
+ CC_selection = (CC_events[cal_energy_param] >= 1000) & (
946
+ CC_events[cal_energy_param] <= 2400
947
+ )
948
+ for peak in peaks:
949
+ CC_selection &= ~(
950
+ (CC_events[cal_energy_param] >= peak - 5)
951
+ & (CC_events[cal_energy_param] <= peak + 5)
952
+ )
953
+ CC_events = CC_events[CC_selection]
954
+
955
+ if len(CC_events) == 0:
956
+ log.error("MCD drift: no CC events found after selection")
957
+ return 0, {}
958
+
959
+ aoe_vals = CC_events[aoe_param].to_numpy()
960
+ dt_vals = CC_events[dt_param].to_numpy() / 1000.0 # convert to us for stability
961
+ mcd_data = np.column_stack([aoe_vals, dt_vals])
962
+
963
+ # --- find AoE range ---
964
+ classifier = mcd_data[:, 0]
965
+ xrange = (0.9, 1.1)
966
+ bin_width_x = (
967
+ 0.5
968
+ * (np.nanpercentile(classifier, 75) - np.nanpercentile(classifier, 25))
969
+ * len(classifier) ** (-1 / 3)
970
+ )
971
+ if bin_width_x <= 0:
972
+ log.error("MCD drift: degenerate AoE distribution, bin width <= 0")
973
+ return 0, {}
974
+
975
+ xbins = int((xrange[1] - xrange[0]) / bin_width_x)
976
+ counts, bin_edges = np.histogram(classifier, bins=xbins, range=xrange, density=True)
977
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
978
+ peak_position = bin_centers[np.argmax(counts)]
979
+ half_max = np.max(counts) / 2.0
980
+ indices_above_half = np.where(counts >= half_max)[0]
981
+
982
+ if len(indices_above_half) < 2:
983
+ log.error("MCD drift: could not determine FWHM of AoE peak")
984
+ return 0, {}
985
+
986
+ bw = bin_centers[1] - bin_centers[0]
987
+ fwhm = bin_centers[indices_above_half[-1]] - bin_centers[indices_above_half[0]] + bw
988
+ x_low = peak_position - 2 * fwhm
989
+ x_high = peak_position + 3 * fwhm
990
+
991
+ # --- MCD fit ---
992
+ in_x = (mcd_data[:, 0] >= x_low) & (mcd_data[:, 0] <= x_high)
993
+ in_y = (mcd_data[:, 1] >= 0) & (
994
+ mcd_data[:, 1] <= np.quantile(mcd_data[:, 1], 0.999)
995
+ )
996
+ subset = mcd_data[in_x & in_y]
997
+
998
+ if len(subset) < 2:
999
+ log.error("MCD drift: insufficient events in AoE range for MCD fit")
1000
+ return 0, {}
1001
+
1002
+ try:
1003
+ mcd = MinCovDet().fit(subset)
1004
+ except Exception as e:
1005
+ if debug_mode:
1006
+ raise e
1007
+ log.error("MCD drift: MCD fit failed: %s", e)
1008
+ return 0, {}
1009
+
1010
+ # --- PCA slope ---
1011
+ _, eigvecs = np.linalg.eigh(mcd.covariance_)
1012
+ principal = eigvecs[:, -1]
1013
+ if abs(principal[0]) > abs(principal[1]):
1014
+ log.warning(
1015
+ "Principal eigenvector not aligned with drift time axis (|v_aoe|=%.4f > |v_dt|=%.4f), "
1016
+ "switching to minor eigenvector",
1017
+ abs(principal[0]),
1018
+ abs(principal[1]),
1019
+ )
1020
+ principal = eigvecs[:, -2]
1021
+
1022
+ if abs(principal[1]) < 1e-10:
1023
+ log.error("MCD drift: eigenvector has near-zero dt component")
1024
+ return 0, {}
1025
+
1026
+ alpha = -principal[0] / principal[1] / 1000.0
1027
+ log.info("dtcorr (MCD) successful alpha: %s", alpha)
1028
+ return alpha, {"alpha": alpha}
1029
+
1030
+
746
1031
  class CalAoE:
747
1032
  """
748
1033
  Class for calibrating the A/E,
@@ -1149,15 +1434,17 @@ class CalAoE:
1149
1434
  def drift_time_correction(
1150
1435
  self,
1151
1436
  data: pd.DataFrame,
1152
- aoe_param,
1153
- out_param="AoE_DTcorr",
1437
+ aoe_param: str = "AoE_Timecorr",
1438
+ out_param: str = "AoE_DTcorr",
1439
+ mode: str = "mcdrift",
1154
1440
  display: int = 0,
1155
1441
  ):
1156
1442
  """
1157
- Calculates the correction needed to align the two drift time regions for ICPC detectors.
1158
- This is done by fitting the two drift time peaks in the drift time spectrum and then
1159
- fitting the A/E peaks in each of these regions. A simple linear correction is then applied
1160
- to align these regions.
1443
+ Apply a linear drift-time correction to the A/E distribution.
1444
+
1445
+ Estimates a linear correction coefficient to align the A/E values
1446
+ across drift time and writes the corrected parameter to the DataFrame
1447
+ and calibration dictionary.
1161
1448
 
1162
1449
  Parameters
1163
1450
  ----------
@@ -1165,119 +1452,46 @@ class CalAoE:
1165
1452
  data
1166
1453
  DataFrame containing the data.
1167
1454
  aoe_param
1168
- Name of the A/E parameter to use as starting point.
1455
+ Name of the A/E parameter to use as the starting point for correction.
1169
1456
  out_param
1170
- Name of the output parameter for the drift-time-corrected A/E in the dataframe and to
1171
- be added to the calibration dictionary.
1457
+ Name of the output parameter for the drift-time-corrected A/E,
1458
+ stored in the DataFrame and added to the calibration dictionary.
1459
+ mode
1460
+ Mode used to estimate the correction coefficient. Options:
1461
+ - ``"mcdrift"`` : Minimum Covariance Determinant (MCD) based estimation
1462
+ (improved stability for low statistic and no evident structure)
1463
+ - ``"bimodal_dt_fit"`` : Fit the position of two drift time peaks
1464
+ (heavily relying on DEP events and clear structure).
1172
1465
  display
1173
1466
  Plot verbosity level.
1174
1467
 
1175
1468
  """
1176
1469
  log.info("Starting A/E drift time correction")
1177
- self.dt_res_dict = {}
1178
- try:
1179
- dep_events = data.query(
1180
- f"{self.fit_selection}&{self.cal_energy_param}>1582&{self.cal_energy_param}<1602&{self.cal_energy_param}=={self.cal_energy_param}&{aoe_param}=={aoe_param}"
1181
- )
1182
1470
 
1183
- hist, bins, var = pgh.get_hist(
1184
- dep_events[aoe_param],
1185
- bins=500,
1186
- )
1187
- bin_cs = (bins[1:] + bins[:-1]) / 2
1188
- mu = bin_cs[np.argmax(hist)]
1189
- aoe_range = [mu * 0.9, mu * 1.1]
1190
-
1191
- dt_range = [
1192
- np.nanpercentile(dep_events[self.dt_param], 1),
1193
- np.nanpercentile(dep_events[self.dt_param], 99),
1194
- ]
1195
-
1196
- self.dt_res_dict["final_selection"] = (
1197
- f"{aoe_param}>{aoe_range[0]}&{aoe_param}<{aoe_range[1]}&{self.dt_param}>{dt_range[0]}&{self.dt_param}<{dt_range[1]}&{self.dt_param}=={self.dt_param}"
1198
- )
1199
-
1200
- final_df = dep_events.query(self.dt_res_dict["final_selection"])
1201
-
1202
- hist, bins, var = pgh.get_hist(
1203
- final_df[self.dt_param],
1204
- dx=32,
1205
- range=(
1206
- np.nanmin(final_df[self.dt_param]),
1207
- np.nanmax(final_df[self.dt_param]),
1208
- ),
1209
- )
1210
-
1211
- bcs = pgh.get_bin_centers(bins)
1212
- mus = bcs[pgc.get_i_local_maxima(hist / (np.sqrt(var) + 10**-99), 2)]
1213
- pk_pars, _pk_covs = pgc.hpge_fit_energy_peak_tops(
1214
- hist,
1215
- bins,
1216
- var=var,
1217
- peak_locs=mus,
1218
- n_to_fit=5,
1219
- )
1220
-
1221
- mus = pk_pars[:, 0]
1222
- sigmas = pk_pars[:, 1]
1223
- amps = pk_pars[:, 2]
1224
-
1225
- if len(mus) > 2:
1226
- ids = np.array(
1227
- sorted([np.argmax(amps), np.argmax(amps[amps != np.argmax(amps)])])
1228
- )
1229
- else:
1230
- ids = np.full(len(mus), True, dtype=bool)
1231
- mus = mus[ids]
1232
- sigmas = sigmas[ids]
1233
- amps = amps[ids]
1234
-
1235
- self.dt_res_dict["dt_fit"] = {"mus": mus, "sigmas": sigmas, "amps": amps}
1236
-
1237
- if len(mus) < 2:
1238
- log.info("Only 1 drift time peak found, no correction needed")
1239
- self.alpha = 0
1240
-
1241
- else:
1242
- aoe_grp1 = self.dt_res_dict["aoe_grp1"] = (
1243
- f"{self.dt_param}>{mus[0] - 2 * sigmas[0]} & {self.dt_param}<{mus[0] + 2 * sigmas[0]}"
1244
- )
1245
- aoe_grp2 = self.dt_res_dict["aoe_grp2"] = (
1246
- f"{self.dt_param}>{mus[1] - 2 * sigmas[1]} & {self.dt_param}<{mus[1] + 2 * sigmas[1]}"
1247
- )
1248
-
1249
- aoe_pars, aoe_errs, _, _ = unbinned_aoe_fit(
1250
- final_df.query(aoe_grp1)[aoe_param], pdf=self.pdf, display=display
1251
- )
1252
-
1253
- self.dt_res_dict["aoe_fit1"] = {
1254
- "pars": aoe_pars.to_dict(),
1255
- "errs": aoe_errs.to_dict(),
1256
- }
1257
-
1258
- aoe_pars2, aoe_errs2, _, _ = unbinned_aoe_fit(
1259
- final_df.query(aoe_grp2)[aoe_param], pdf=self.pdf, display=display
1260
- )
1261
-
1262
- self.dt_res_dict["aoe_fit2"] = {
1263
- "pars": aoe_pars2.to_dict(),
1264
- "errs": aoe_errs2.to_dict(),
1265
- }
1266
-
1267
- try:
1268
- self.alpha = (aoe_pars2["mu"] - aoe_pars["mu"]) / (
1269
- (mus[0] * aoe_pars["mu"]) - (mus[1] * aoe_pars2["mu"])
1270
- )
1271
- except ZeroDivisionError:
1272
- self.alpha = 0
1273
- self.dt_res_dict["alpha"] = self.alpha
1274
- log.info("dtcorr successful alpha:%s", self.alpha)
1471
+ _modes = {
1472
+ "mcdrift": mcdrift,
1473
+ "bimodal_dt_fit": bimodal_dt_fit,
1474
+ }
1275
1475
 
1276
- except BaseException as e:
1277
- if isinstance(e, KeyboardInterrupt) or self.debug_mode:
1278
- raise (e)
1279
- log.error("Drift time correction failed")
1476
+ if mode not in _modes:
1280
1477
  self.alpha = 0
1478
+ log.error(
1479
+ "Unknown mode '%s' for drift time correction. Valid options are: %s",
1480
+ mode,
1481
+ list(_modes),
1482
+ )
1483
+ return
1484
+
1485
+ self.alpha, self.dt_res_dict = _modes[mode](
1486
+ data,
1487
+ aoe_param,
1488
+ dt_param=self.dt_param,
1489
+ fit_selection=self.fit_selection,
1490
+ cal_energy_param=self.cal_energy_param,
1491
+ pdf=self.pdf,
1492
+ debug_mode=self.debug_mode,
1493
+ display=display,
1494
+ )
1281
1495
 
1282
1496
  data[out_param] = data[aoe_param] * (1 + self.alpha * data[self.dt_param])
1283
1497
  self.update_cal_dicts(
@@ -1923,6 +2137,7 @@ class CalAoE:
1923
2137
  sf_nsamples: int = 11,
1924
2138
  sf_cut_range: tuple = (-5, 5),
1925
2139
  timecorr_mode: str = "full",
2140
+ dtcorr_mode: str = "mcdrift",
1926
2141
  override_dict: dict | None = None,
1927
2142
  ):
1928
2143
  """
@@ -1950,6 +2165,8 @@ class CalAoE:
1950
2165
  Range of A/E cut values to use for the survival fraction sweep.
1951
2166
  timecorr_mode
1952
2167
  Mode to use for the time correction; see :meth:`time_correction` for details.
2168
+ dtcorr_mode
2169
+ Mode to use for the drift time correction; see :meth:`drift_time_correction` for details.
1953
2170
  override_dict
1954
2171
  Optional dictionary of pre-computed calibration entries that bypass one or more
1955
2172
  fit steps. Each key should map to a sub-dict with ``"expression"`` (a string
@@ -1993,7 +2210,9 @@ class CalAoE:
1993
2210
  if self.dt_corr is True:
1994
2211
  aoe_param = "AoE_DTcorr"
1995
2212
  if override_dict is None or "AoE_DTcorr" not in override_dict:
1996
- self.drift_time_correction(df, "AoE_Timecorr", out_param=aoe_param)
2213
+ self.drift_time_correction(
2214
+ df, "AoE_Timecorr", out_param=aoe_param, mode=dtcorr_mode
2215
+ )
1997
2216
  else:
1998
2217
  self.update_cal_dicts({"AoE_DTcorr": override_dict["AoE_DTcorr"]})
1999
2218
  df["AoE_DTcorr"] = ne.evaluate(
@@ -14,7 +14,6 @@ from lgdo import Table
14
14
  from scipy.signal import convolve, convolve2d
15
15
  from scipy.stats import chi2
16
16
 
17
- from pygama.pargen.data_cleaning import generate_cuts
18
17
  from pygama.pargen.dsp_optimize import run_one_dsp
19
18
  from pygama.pargen.energy_optimisation import fom_fwhm_with_alpha_fit
20
19
 
@@ -39,7 +38,8 @@ def dplms_ge_dict(
39
38
  Parameters
40
39
  ----------
41
40
  raw_fft
42
- table with fft raw data
41
+ Table with FFT raw data already baseline-selected upstream. All events in
42
+ this table are used to estimate the noise matrix.
43
43
  raw_cal
44
44
  table with cal raw data
45
45
  dsp_config
@@ -57,33 +57,18 @@ def dplms_ge_dict(
57
57
  """
58
58
 
59
59
  t0 = time.time()
60
- log.info("Selecting baselines")
60
+ log.info("Using baseline-selected FFT events")
61
61
 
62
62
  dsp_fft = run_one_dsp(raw_fft, dsp_config, db_dict=par_dsp)
63
63
 
64
- cut_dict = generate_cuts(dsp_fft, cut_dict=dplms_dict["bls_cut_pars"])
65
- log.debug("Cuts are %s", cut_dict)
66
- idxs = np.full(len(dsp_fft), True, dtype=bool)
67
- for outname, info in cut_dict.items():
68
- outcol = dsp_fft.eval(info["expression"], info.get("parameters", None))
69
- dsp_fft.add_column(outname, outcol)
70
- for cut in cut_dict:
71
- idxs = dsp_fft[cut].nda & idxs
72
- log.debug("Applied Cuts")
73
-
74
64
  bl_field = dplms_dict["bl_field"]
75
- log.info("... %s baselines after cuts", len(dsp_fft[bl_field].values.nda[idxs, :]))
65
+ log.info("... %s baselines", len(dsp_fft[bl_field].values.nda))
76
66
 
77
- bls = dsp_fft[bl_field].values.nda[idxs, : dplms_dict["bsize"]]
78
- bls_par = {}
79
- bls_cut_pars = list(dplms_dict["bls_cut_pars"])
80
- for par in bls_cut_pars:
81
- bls_par[par] = dsp_fft[dplms_dict["bls_cut_pars"][par]["cut_parameter"]].nda
67
+ bls = dsp_fft[bl_field].values.nda[:, : dplms_dict["bsize"]]
82
68
  t1 = time.time()
83
69
  log.info(
84
- "total events %s, %s baseline selected in %.2f s",
70
+ "using %s baseline-selected events for noise estimation in %.2f s",
85
71
  len(raw_fft),
86
- len(bls),
87
72
  t1 - t0,
88
73
  )
89
74
  log.info(
@@ -304,27 +289,7 @@ def dplms_ge_dict(
304
289
  ax.plot(wf)
305
290
  ax.legend(loc="upper right")
306
291
  plot_dict["bls"] = fig
307
- fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(16, 9), facecolor="white")
308
- for ii, par in enumerate(bls_cut_pars):
309
- if "parameters" in cut_dict[par]:
310
- if "a" in cut_dict[par]["parameters"]:
311
- llo = cut_dict[par]["parameters"]["a"]
312
- else:
313
- llo = np.nan
314
- if "b" in cut_dict[par]["parameters"]:
315
- lup = cut_dict[par]["parameters"]["b"]
316
- else:
317
- lup = np.nan
318
- mean = (lup + llo) / 2
319
- plo, pup = mean - 2 * (mean - llo), mean + 2 * (lup - mean)
320
- hh, bb = np.histogram(bls_par[par], bins=np.linspace(plo, pup, 200))
321
- ax.flat[ii].plot(bb[1:], hh, ds="steps", label=f"cut on {par}")
322
- ax.flat[ii].axvline(lup, color="k", linestyle=":", label="selection")
323
- ax.flat[ii].axvline(llo, color="k", linestyle=":")
324
- ax.flat[ii].set_xlabel(par)
325
- ax.flat[ii].set_yscale("log")
326
- ax.flat[ii].legend(loc="upper right")
327
- plot_dict["bl_sel"] = fig
292
+ plt.close()
328
293
 
329
294
  wf_idxs = np.random.choice(len(wfs), dplms_dict["n_plot"]) # noqa: NPY002
330
295
  wfs = wfs[wf_idxs]
@@ -343,6 +308,7 @@ def dplms_ge_dict(
343
308
  axin.set_xlim(wsize / 2 - dplms_dict["zoom"], wsize / 2 + dplms_dict["zoom"])
344
309
  axin.set_yticklabels("")
345
310
  plot_dict["wfs"] = fig
311
+ plt.close()
346
312
 
347
313
  peak_pos = dsp_cal["peak_pos"].nda
348
314
  risetime = dsp_cal["tp_90"].nda - dsp_cal["tp_10"].nda
@@ -392,6 +358,7 @@ def dplms_ge_dict(
392
358
  ax.flat[0].set_yscale("log")
393
359
  ax.flat[0].legend(loc="upper right")
394
360
  plot_dict["wf_sel"] = fig
361
+ plt.close()
395
362
 
396
363
  fig, ax = plt.subplots(figsize=(12, 6.75), facecolor="white")
397
364
  ax.plot(x, "r-", label="filter")
@@ -405,6 +372,8 @@ def dplms_ge_dict(
405
372
  )
406
373
  axin.set_yticklabels("")
407
374
  ax.indicate_inset_zoom(axin)
375
+ plot_dict["filter"] = fig
376
+ plt.close()
408
377
 
409
378
  return out_dict, plot_dict
410
379
  return out_dict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygama
3
- Version: 2.3.8a2
3
+ Version: 2.3.8a3
4
4
  Summary: Python package for data processing and analysis
5
5
  Author: The LEGEND collaboration
6
6
  Maintainer: The LEGEND collaboration