trxrdpy 3.2.1__tar.gz → 3.2.2__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 (96) hide show
  1. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/PKG-INFO +1 -1
  2. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/pyproject.toml +1 -1
  3. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/Spring8_SACLA/datared.py +79 -23
  4. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/_shared_2d/azimint.py +24 -6
  5. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/differential_analysis_utils.py +184 -29
  6. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/plot_utils.py +60 -8
  7. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/differential_analysis.py +49 -11
  8. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/fitting.py +47 -9
  9. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/main_window.py +271 -1
  10. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/state.py +20 -0
  11. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/differential_tab.py +67 -10
  12. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/fitting_tab.py +75 -15
  13. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/viewer_tab.py +32 -2
  14. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/multi_experiment_widgets.py +33 -2
  15. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/cif.py +2 -1
  16. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/PKG-INFO +1 -1
  17. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/README.md +0 -0
  18. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/setup.cfg +0 -0
  19. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/__init__.py +0 -0
  20. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/ESRF_ID09/__init__.py +0 -0
  21. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/ESRF_ID09/azimint.py +0 -0
  22. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/ESRF_ID09/datared.py +0 -0
  23. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/MaxIV_FemtoMAX/__init__.py +0 -0
  24. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/MaxIV_FemtoMAX/azimint.py +0 -0
  25. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/MaxIV_FemtoMAX/datared.py +0 -0
  26. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/MaxIV_FemtoMAX/datared_utils.py +0 -0
  27. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/MaxIV_FemtoMAX/ping_references_default.csv +0 -0
  28. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/Spring8_SACLA/__init__.py +0 -0
  29. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/Spring8_SACLA/azimint.py +0 -0
  30. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/Spring8_SACLA/pbs/parallel_job_sender.sh +0 -0
  31. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/__init__.py +0 -0
  32. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/_shared_2d/__init__.py +0 -0
  33. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/calibration.py +0 -0
  34. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/__init__.py +0 -0
  35. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/azimint_utils.py +0 -0
  36. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/calibration_utils.py +0 -0
  37. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/fitting_utils.py +0 -0
  38. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/general_utils.py +0 -0
  39. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/common/paths.py +0 -0
  40. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/__init__.py +0 -0
  41. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/defaults.py +0 -0
  42. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/runtime_guard.py +0 -0
  43. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/__init__.py +0 -0
  44. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/calibration_service.py +0 -0
  45. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/differential_service.py +0 -0
  46. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/facility_service.py +0 -0
  47. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/fitting_service.py +0 -0
  48. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/integration_service.py +0 -0
  49. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/path_service.py +0 -0
  50. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/services/preparation_service.py +0 -0
  51. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/style.py +0 -0
  52. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/__init__.py +0 -0
  53. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/calibration_tab.py +0 -0
  54. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/pattern_creation_tab.py +0 -0
  55. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/preparation_tab.py +0 -0
  56. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/tabs/session_tab.py +0 -0
  57. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/utils.py +0 -0
  58. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/__init__.py +0 -0
  59. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/experiment_widgets.py +0 -0
  60. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/facility_widgets.py +0 -0
  61. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/log_widget.py +0 -0
  62. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/parameter_widgets.py +0 -0
  63. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/path_widgets.py +0 -0
  64. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/polarization_widget.py +0 -0
  65. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/analysis/gui/widgets/task_output_dialog.py +0 -0
  66. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/__init__.py +0 -0
  67. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/detector.py +0 -0
  68. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/diffractometers.py +0 -0
  69. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/experiment.py +0 -0
  70. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/geometry.py +0 -0
  71. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/__init__.py +0 -0
  72. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/main_window.py +0 -0
  73. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/services/__init__.py +0 -0
  74. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/services/path_service.py +0 -0
  75. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/services/simulation_service.py +0 -0
  76. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/state.py +0 -0
  77. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/style.py +0 -0
  78. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/tabs/__init__.py +0 -0
  79. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/tabs/polycrystalline_tab.py +0 -0
  80. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/tabs/single_crystal_tab.py +0 -0
  81. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/widgets/__init__.py +0 -0
  82. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/widgets/geometry_panel.py +0 -0
  83. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/gui/widgets/matrix_rotation_window.py +0 -0
  84. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/plot.py +0 -0
  85. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/polycrystalline.py +0 -0
  86. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/poni.py +0 -0
  87. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/sample.py +0 -0
  88. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/single_crystal.py +0 -0
  89. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy/simulation/utils.py +0 -0
  90. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/SOURCES.txt +0 -0
  91. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/dependency_links.txt +0 -0
  92. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/entry_points.txt +0 -0
  93. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/requires.txt +0 -0
  94. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/src/trxrdpy.egg-info/top_level.txt +0 -0
  95. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/tests/test_calibration_2d_cake.py +0 -0
  96. {trxrdpy-3.2.1 → trxrdpy-3.2.2}/tests/test_id09_datared_io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trxrdpy
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Python toolkit for time-resolved X-ray diffraction simulation and beamline-dependent analysis workflows.
5
5
  Author: Julio Guzman-Brambila
6
6
  License-Expression: CC-BY-4.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trxrdpy"
7
- version = "3.2.1"
7
+ version = "3.2.2"
8
8
  description = "Python toolkit for time-resolved X-ray diffraction simulation and beamline-dependent analysis workflows."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.10"
@@ -1123,14 +1123,16 @@ def create_dark_from_laser_off(
1123
1123
  sample_name,
1124
1124
  temperature_K,
1125
1125
  excitation_wl_nm,
1126
- fluence_mJ_cm2,
1127
- time_window_fs,
1126
+ fluence_mJ_cm2=None,
1127
+ time_window_fs=None,
1128
1128
  *,
1129
+ scan_type="delay",
1130
+ delay_for_fluence=None,
1129
1131
  analysis_root=PATH_ANALYSIS_FOLDER,
1130
1132
  overwrite=True,
1131
1133
  min_files=1):
1132
1134
  """Build a representative DARK image by averaging all available *_laser_off.npy files
1133
- for a given delay-scan experiment, and save it.
1135
+ for a delay- or fluence-scan experiment, and save it.
1134
1136
 
1135
1137
  Inputs:
1136
1138
  runs : int, list/tuple/np.ndarray, or "1466556,1466557" / "1466556 1466557"
@@ -1140,11 +1142,19 @@ def create_dark_from_laser_off(
1140
1142
  fluence_mJ_cm2 : float/int, e.g. 25 (will be tagged like "25p0mJ")
1141
1143
  time_window_fs : int, e.g. 250
1142
1144
 
1143
- Behavior:
1145
+ Behavior for delay-family scans:
1144
1146
  - Searches in:
1145
1147
  <analysis_root>/<sample>/temperature_<T>K/excitation_wl_<wl>nm/delay/fluence_<flu>mJ/time_window_<tw>fs/2D_images
1146
1148
  for files matching:
1147
1149
  <sample>_<T>K_<wl>nm_<flu>mJ_<tw>fs_*_laser_off.npy
1150
+
1151
+ Behavior for fluence-family scans:
1152
+ - Searches in:
1153
+ <analysis_root>/<sample>/temperature_<T>K/excitation_wl_<wl>nm/fluence/delay_<delay>fs/time_window_<tw>fs/2D_images
1154
+ for files matching:
1155
+ <sample>_<T>K_<wl>nm_*mJ_<tw>fs_<delay>fs_laser_off.npy
1156
+
1157
+ In both cases:
1148
1158
  - Averages all matching files (skips unreadable / NaN / shape-mismatch files).
1149
1159
  - Saves to FemtoMAX-compatible dark path:
1150
1160
  <analysis_root>/<sample>/temperature_<T>K/dark/<scan_tag>/2D_images/<sample>_<T>K_dark_<scan_tag_file>.npy
@@ -1156,9 +1166,13 @@ def create_dark_from_laser_off(
1156
1166
  ----------
1157
1167
  runs
1158
1168
  Run number or run collection defining the dark provenance tag.
1159
- sample_name, temperature_K, excitation_wl_nm, fluence_mJ_cm2,
1160
- time_window_fs
1161
- Delay-scan identity used to locate laser-off images.
1169
+ sample_name, temperature_K, excitation_wl_nm, fluence_mJ_cm2, time_window_fs
1170
+ Experiment identity used to locate laser-off images. ``fluence_mJ_cm2``
1171
+ is required for delay-family inputs.
1172
+ scan_type : {"delay", "delay_off", "differentials", "fluence", "fluence_off"}
1173
+ Source image family to search.
1174
+ delay_for_fluence : int, optional
1175
+ Fixed delay folder used for fluence-family inputs.
1162
1176
  analysis_root : path-like
1163
1177
  Root of the standardized analysis tree.
1164
1178
  overwrite : bool
@@ -1256,29 +1270,59 @@ def create_dark_from_laser_off(
1256
1270
  sn = str(sample_name)
1257
1271
  tK = int(temperature_K)
1258
1272
  wl_tag = _wl_tag_nm_local(excitation_wl_nm)
1259
- flu_tag = _fluence_tag_file_local(fluence_mJ_cm2)
1260
1273
  tw = int(time_window_fs)
1261
1274
 
1262
1275
  runs_list = _parse_runs(runs)
1263
1276
  dark_folder_tag, dark_file_tag = _dark_tags_from_runs(runs_list)
1264
1277
 
1265
- delay_root = os.path.join(
1266
- str(analysis_root),
1267
- sn,
1268
- "temperature_{}K".format(int(tK)),
1269
- "excitation_wl_{}nm".format(str(wl_tag)),
1270
- "delay",
1271
- "fluence_{}mJ".format(str(flu_tag)),
1272
- "time_window_{}fs".format(int(tw)),
1273
- )
1274
- img_dir = os.path.join(delay_root, "2D_images")
1278
+ st = str(scan_type).strip().lower()
1279
+ if st in ("delay", "delay_off", "differentials"):
1280
+ if fluence_mJ_cm2 is None:
1281
+ raise ValueError("fluence_mJ_cm2 must be provided for delay laser-off dark creation.")
1282
+
1283
+ flu_tag = _fluence_tag_file_local(fluence_mJ_cm2)
1284
+ source_root = os.path.join(
1285
+ str(analysis_root),
1286
+ sn,
1287
+ "temperature_{}K".format(int(tK)),
1288
+ "excitation_wl_{}nm".format(str(wl_tag)),
1289
+ "delay",
1290
+ "fluence_{}mJ".format(str(flu_tag)),
1291
+ "time_window_{}fs".format(int(tw)),
1292
+ )
1293
+ prefix = "{}_{}K_{}nm_{}mJ_{}fs_".format(
1294
+ sn, int(tK), str(wl_tag), str(flu_tag), int(tw)
1295
+ )
1296
+ suffix = "_laser_off.npy"
1297
+
1298
+ elif st in ("fluence", "fluence_off"):
1299
+ if delay_for_fluence is None:
1300
+ raise ValueError("delay_for_fluence must be provided for fluence laser-off dark creation.")
1301
+
1302
+ dly = int(delay_for_fluence)
1303
+ source_root = os.path.join(
1304
+ str(analysis_root),
1305
+ sn,
1306
+ "temperature_{}K".format(int(tK)),
1307
+ "excitation_wl_{}nm".format(str(wl_tag)),
1308
+ "fluence",
1309
+ "delay_{}fs".format(int(dly)),
1310
+ "time_window_{}fs".format(int(tw)),
1311
+ )
1312
+ prefix = "{}_{}K_{}nm_".format(sn, int(tK), str(wl_tag))
1313
+ suffix = "_{}fs_{}fs_laser_off.npy".format(int(tw), int(dly))
1314
+
1315
+ else:
1316
+ raise ValueError(
1317
+ "dark_from_laser_off only supports scan_type delay/delay_off/differentials "
1318
+ "or fluence/fluence_off."
1319
+ )
1320
+
1321
+ img_dir = os.path.join(source_root, "2D_images")
1275
1322
 
1276
1323
  if not os.path.isdir(img_dir):
1277
1324
  raise FileNotFoundError("2D_images folder not found: {}".format(img_dir))
1278
1325
 
1279
- prefix = "{}_{}K_{}nm_{}mJ_{}fs_".format(sn, int(tK), str(wl_tag), str(flu_tag), int(tw))
1280
- suffix = "_laser_off.npy"
1281
-
1282
1326
  candidates = []
1283
1327
  for fn in os.listdir(img_dir):
1284
1328
  if not fn.endswith(suffix):
@@ -2735,8 +2779,18 @@ def main():
2735
2779
  print("Error: --excitation_wl_nm is required for dark_from_laser_off mode.")
2736
2780
  sys.exit(1)
2737
2781
 
2738
- if args.fluence_mJ_cm2 is None:
2739
- print("Error: --fluence_mJ_cm2 is required for dark_from_laser_off mode.")
2782
+ if st_images in ("delay", "delay_off", "differentials"):
2783
+ if args.fluence_mJ_cm2 is None:
2784
+ print("Error: --fluence_mJ_cm2 is required for dark_from_laser_off mode with delay scans.")
2785
+ sys.exit(1)
2786
+
2787
+ elif st_images in ("fluence", "fluence_off"):
2788
+ if args.delay_for_fluence is None:
2789
+ print("Error: --delay_for_fluence is required for dark_from_laser_off mode with fluence scans.")
2790
+ sys.exit(1)
2791
+
2792
+ else:
2793
+ print("Error: --mode dark_from_laser_off only supports delay-family or fluence-family scan types.")
2740
2794
  sys.exit(1)
2741
2795
 
2742
2796
  if args.time_window_fs is None:
@@ -2751,6 +2805,8 @@ def main():
2751
2805
  excitation_wl_nm=args.excitation_wl_nm,
2752
2806
  fluence_mJ_cm2=args.fluence_mJ_cm2,
2753
2807
  time_window_fs=args.time_window_fs,
2808
+ scan_type=st_images,
2809
+ delay_for_fluence=args.delay_for_fluence,
2754
2810
  analysis_root=PATH_ANALYSIS_FOLDER,
2755
2811
  overwrite=bool(args.overwrite),
2756
2812
  min_files=int(args.min_files),
@@ -614,6 +614,12 @@ def plot_1D_abs_and_diffs_fluence(
614
614
  vlines_peak: Optional[Tuple[float, float]] = None,
615
615
  vlines_bckg: Optional[Tuple[float, float]] = None,
616
616
  title: Optional[str] = None,
617
+ fluence_scale: float = 1.0,
618
+ fluence_offset: float = 0.0,
619
+ delay_offset_fs: float = 0.0,
620
+ fs_or_ps: str = "fs",
621
+ digits: int = 2,
622
+ fluence_unit: str = "mJ/cm$^2$",
617
623
  azim_offset_deg: float = -90.0,
618
624
  polarization_factor: Optional[float] = None,
619
625
  save_plots: bool = False,
@@ -648,8 +654,11 @@ def plot_1D_abs_and_diffs_fluence(
648
654
  Intensity normalization and XY cache-generation controls.
649
655
  xlim, ylim_top, ylim_diff, vlines_peak, vlines_bckg
650
656
  Axis limits and optional marked q intervals.
651
- title, azim_offset_deg, polarization_factor
652
- Figure title, azimuth-coordinate offset, and polarization correction.
657
+ title, fluence_scale, fluence_offset, delay_offset_fs, fs_or_ps, digits, fluence_unit
658
+ Figure title and display-only fluence/delay corrections. File lookup
659
+ still uses the stored ``delay_fs`` and ``fluences_mJ_cm2`` values.
660
+ azim_offset_deg, polarization_factor
661
+ Azimuth-coordinate offset and polarization correction.
653
662
  save_plots, out_name, save_format, save_dpi, save_overwrite, save_base_dir
654
663
  Figure-output controls.
655
664
  paths, path_root, analysis_subdir
@@ -704,6 +713,7 @@ def plot_1D_abs_and_diffs_fluence(
704
713
  if ref_value is None:
705
714
  raise ValueError("ref_type='fluence' requires ref_value=<fluence_mJ_cm2>.")
706
715
  ref_f = float(ref_value)
716
+ ref_f_display = ref_f * float(fluence_scale) + float(fluence_offset)
707
717
  ds_ref = azimint_utils.FluenceDataset(
708
718
  sample_name,
709
719
  temperature_K,
@@ -713,7 +723,7 @@ def plot_1D_abs_and_diffs_fluence(
713
723
  int(delay_fs),
714
724
  **dataset_kwargs,
715
725
  )
716
- ref_label = f"ref: {ref_f:g} mJ/cm$^2$"
726
+ ref_label = f"ref: {ref_f_display:g} {fluence_unit}"
717
727
  ref_tag_for_file = f"fluence_{general_utils.fluence_tag_file(ref_f)}mJ"
718
728
 
719
729
  else:
@@ -757,12 +767,20 @@ def plot_1D_abs_and_diffs_fluence(
757
767
  compute_if_missing=bool(compute_if_missing),
758
768
  overwrite_xy=bool(overwrite_xy),
759
769
  )
760
- patterns.append((f"{float(f):g}", q, I))
770
+ f_display = float(f) * float(fluence_scale) + float(fluence_offset)
771
+ patterns.append((f"{f_display:g}", q, I))
761
772
 
762
773
  if title is None:
774
+ display_delay_fs = int(delay_fs) + float(delay_offset_fs)
775
+ display_delay = azimint_utils.delay_label_value(
776
+ display_delay_fs,
777
+ fs_or_ps=fs_or_ps,
778
+ digits=digits,
779
+ )
780
+ display_unit = general_utils.time_unit_label(fs_or_ps)
763
781
  title = (
764
782
  f"{sample_name}. {temperature_K}K.\n"
765
- f"ex. wl={excitation_wl_nm}nm. delay={int(delay_fs)}fs. tw={time_window_fs}fs.\n"
783
+ f"ex. wl={excitation_wl_nm}nm. delay={display_delay:g} {display_unit}. tw={time_window_fs}fs.\n"
766
784
  f"azim=({azim_window[0]},{azim_window[1]})"
767
785
  )
768
786
 
@@ -808,7 +826,7 @@ def plot_1D_abs_and_diffs_fluence(
808
826
  ylim_diff=ylim_diff,
809
827
  vlines_peak=vlines_peak,
810
828
  vlines_bckg=vlines_bckg,
811
- legend_title="Fluence [mJ/cm$^2$]",
829
+ legend_title=f"Fluence [{fluence_unit}]",
812
830
  legend_loc="upper left",
813
831
  legend_outside=True,
814
832
  **save_kwargs,
@@ -28,7 +28,7 @@ from .paths import AnalysisPaths
28
28
 
29
29
  @dataclass(frozen=True)
30
30
  class PeakSpec:
31
- """Define a q-space peak window and its adjacent-background convention.
31
+ """Define a q-space peak window and its background convention.
32
32
 
33
33
  Attributes
34
34
  ----------
@@ -36,14 +36,33 @@ class PeakSpec:
36
36
  Stable peak identifier written to result tables and figure labels.
37
37
  q_range : tuple of float
38
38
  Lower and upper q limits of the peak integration window in Å⁻¹.
39
- bg_mode : {"before", "after", "avg"}
39
+ bg_mode : {"before", "after", "avg", "custom"}
40
40
  Location of the equal-width background window. ``"avg"`` averages
41
- windows immediately below and above the peak interval.
41
+ windows immediately below and above the peak interval. ``"custom"``
42
+ uses the explicit ``bg_range`` interval.
43
+ bg_range : tuple of float, optional
44
+ Explicit q limits for a custom background integration window.
42
45
  """
43
46
 
44
47
  name: str
45
48
  q_range: Tuple[float, float]
46
49
  bg_mode: str = "after"
50
+ bg_range: Optional[Tuple[float, float]] = None
51
+
52
+
53
+ def _normalize_q_range(value: Sequence[Any], *, name: str) -> Tuple[float, float]:
54
+ """Return sorted q limits and validate positive width."""
55
+ try:
56
+ q0, q1 = value
57
+ except Exception:
58
+ raise ValueError(f"{name} must be a two-value q range.")
59
+
60
+ lo, hi = float(q0), float(q1)
61
+ if hi < lo:
62
+ lo, hi = hi, lo
63
+ if hi <= lo:
64
+ raise ValueError(f"{name} must have positive width.")
65
+ return lo, hi
47
66
 
48
67
 
49
68
  def resolve_bg_mode(spec: Dict[str, Any], bg_mode: Optional[str]) -> str:
@@ -52,25 +71,31 @@ def resolve_bg_mode(spec: Dict[str, Any], bg_mode: Optional[str]) -> str:
52
71
  An explicit mode takes precedence over the legacy ``bg_side`` entry.
53
72
  ``left`` and ``right`` are accepted as aliases for ``before`` and ``after``.
54
73
  """
55
- if bg_mode is not None:
56
- m = str(bg_mode).strip().lower()
57
- if m in ("before", "after", "avg"):
74
+ source = bg_mode
75
+ if source is None and spec.get("bg_mode", None) is not None:
76
+ source = spec.get("bg_mode")
77
+ if source is None and spec.get("bg_side", None) is not None:
78
+ source = spec.get("bg_side")
79
+
80
+ if source is not None:
81
+ m = str(source).strip().lower()
82
+ if m in ("before", "after", "avg", "custom"):
58
83
  return m
59
- if m == "left":
84
+ if m in ("left", "before_peak"):
60
85
  return "before"
61
- if m == "right":
86
+ if m in ("right", "rigth", "after_peak"):
62
87
  return "after"
63
- raise ValueError("bg_mode must be 'before'/'after'/'avg' (or 'left'/'right').")
64
-
65
- if "bg_side" in spec and spec["bg_side"] is not None:
66
- s = str(spec["bg_side"]).strip().lower()
67
- if s == "left":
68
- return "before"
69
- if s == "right":
70
- return "after"
71
- if s == "avg":
88
+ if m in ("both", "average"):
72
89
  return "avg"
73
- raise ValueError("PEAK_SPECS[*]['bg_side'] must be 'left','right','avg'.")
90
+ if m in ("manual", "range", "explicit"):
91
+ return "custom"
92
+ raise ValueError(
93
+ "bg_mode must be 'before'/'after'/'avg'/'custom' "
94
+ "(or 'left'/'right')."
95
+ )
96
+
97
+ if spec.get("bg_range", None) is not None:
98
+ return "custom"
74
99
 
75
100
  return "after"
76
101
 
@@ -92,7 +117,8 @@ def get_peak_spec(
92
117
  peak_specs : Optional[Dict[str, Dict[str, Any]]]
93
118
  Mapping from peak labels to q ranges and optional background or fit settings.
94
119
  bg_mode : Optional[str]
95
- Adjacent-background choice: ``"before"``, ``"after"``, or ``"avg"``.
120
+ Background choice: ``"before"``, ``"after"``, ``"avg"``, or
121
+ ``"custom"``.
96
122
 
97
123
  Returns
98
124
  -------
@@ -115,9 +141,35 @@ def get_peak_spec(
115
141
  if "q_range" not in s:
116
142
  raise ValueError(f"Peak '{peak}' spec must include q_range=(q0,q1).")
117
143
 
118
- q0, q1 = s["q_range"]
144
+ q0, q1 = _normalize_q_range(s["q_range"], name=f"Peak '{peak}' q_range")
119
145
  bm = resolve_bg_mode(s, bg_mode)
120
- return PeakSpec(name=str(peak), q_range=(float(q0), float(q1)), bg_mode=str(bm))
146
+ bg_range = None
147
+ if s.get("bg_range", None) is not None:
148
+ bg_range = _normalize_q_range(
149
+ s["bg_range"],
150
+ name=f"Peak '{peak}' bg_range",
151
+ )
152
+ if bm == "custom" and bg_range is None:
153
+ raise ValueError(
154
+ f"Peak '{peak}' uses bg_mode='custom' and must include bg_range=(q0,q1)."
155
+ )
156
+ return PeakSpec(
157
+ name=str(peak),
158
+ q_range=(float(q0), float(q1)),
159
+ bg_mode=str(bm),
160
+ bg_range=bg_range,
161
+ )
162
+
163
+
164
+ def background_description(peak_spec: PeakSpec) -> str:
165
+ """Return a compact background description for figure titles and logs."""
166
+ mode = str(peak_spec.bg_mode).strip().lower()
167
+ if mode == "custom" and peak_spec.bg_range is not None:
168
+ q0, q1 = peak_spec.q_range
169
+ b0, b1 = peak_spec.bg_range
170
+ scale = abs(float(q1) - float(q0)) / abs(float(b1) - float(b0))
171
+ return f"custom bg=({b0:.3g}, {b1:.3g}), scale={scale:g}"
172
+ return mode
121
173
 
122
174
 
123
175
  def select_series_for_fft(
@@ -410,7 +462,7 @@ class DelayDifferentialAnalyzer:
410
462
 
411
463
  Output columns (long-form):
412
464
  delay_fs, delay_ps, region, int_delta, int_abs_delta,
413
- q0, q1, bg0, bg1, bg_mode, peak_name,
465
+ q0, q1, bg0, bg1, bg_mode, bg_scale, peak_width, bg_width, peak_name,
414
466
  ref_type, ref_value, azim0, azim1
415
467
 
416
468
  ``region`` is either ``"peak"`` or ``"background"``.
@@ -533,8 +585,9 @@ class DelayDifferentialAnalyzer:
533
585
  def _bg_ranges(
534
586
  q_range: Tuple[float, float],
535
587
  bg_mode: str,
588
+ bg_range: Optional[Tuple[float, float]] = None,
536
589
  ) -> Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]]:
537
- """Construct equal-width adjacent background interval(s) for a peak range."""
590
+ """Construct background interval(s) for a peak range."""
538
591
  q0, q1 = float(q_range[0]), float(q_range[1])
539
592
  if q1 < q0:
540
593
  q0, q1 = q1, q0
@@ -549,7 +602,37 @@ class DelayDifferentialAnalyzer:
549
602
  return (q1, q1 + w)
550
603
  if m == "avg":
551
604
  return ((q0 - w, q0), (q1, q1 + w))
552
- raise ValueError("bg_mode must be 'before', 'after', or 'avg'.")
605
+ if m == "custom":
606
+ if bg_range is None:
607
+ raise ValueError("bg_mode='custom' requires bg_range=(q0,q1).")
608
+ return _normalize_q_range(bg_range, name="bg_range")
609
+ raise ValueError("bg_mode must be 'before', 'after', 'avg', or 'custom'.")
610
+
611
+ @staticmethod
612
+ def _background_width(
613
+ bg_ranges: Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]],
614
+ bg_mode: str,
615
+ ) -> float:
616
+ """Return the effective background q width."""
617
+ if str(bg_mode).strip().lower() == "avg":
618
+ return float(abs(float(bg_ranges[0][1]) - float(bg_ranges[0][0]))) # type: ignore[index]
619
+ return float(abs(float(bg_ranges[1]) - float(bg_ranges[0]))) # type: ignore[arg-type]
620
+
621
+ @classmethod
622
+ def _background_scale(
623
+ cls,
624
+ q_range: Tuple[float, float],
625
+ bg_ranges: Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]],
626
+ bg_mode: str,
627
+ ) -> float:
628
+ """Scale custom-background integrals to the peak q width."""
629
+ if str(bg_mode).strip().lower() != "custom":
630
+ return 1.0
631
+ peak_width = abs(float(q_range[1]) - float(q_range[0]))
632
+ bg_width = cls._background_width(bg_ranges, bg_mode)
633
+ if bg_width <= 0:
634
+ raise ValueError("bg_range must have positive width.")
635
+ return float(peak_width / bg_width)
553
636
 
554
637
  @staticmethod
555
638
  def _interp_to_ref_grid(q_ref: np.ndarray, q: np.ndarray, I: np.ndarray) -> np.ndarray:
@@ -699,7 +782,10 @@ class DelayDifferentialAnalyzer:
699
782
 
700
783
  q_range = (float(pk.q_range[0]), float(pk.q_range[1]))
701
784
  bg_mode = str(pk.bg_mode).strip().lower()
702
- bg_ranges = self._bg_ranges(q_range, bg_mode)
785
+ bg_ranges = self._bg_ranges(q_range, bg_mode, pk.bg_range)
786
+ peak_width = abs(float(q_range[1]) - float(q_range[0]))
787
+ bg_width = self._background_width(bg_ranges, bg_mode)
788
+ bg_scale = self._background_scale(q_range, bg_ranges, bg_mode)
703
789
 
704
790
  rows: List[Dict[str, Any]] = []
705
791
 
@@ -716,6 +802,9 @@ class DelayDifferentialAnalyzer:
716
802
  bg0=np.nan,
717
803
  bg1=np.nan,
718
804
  bg_mode=bg_mode,
805
+ bg_scale=np.nan,
806
+ peak_width=peak_width,
807
+ bg_width=np.nan,
719
808
  peak_name=pk.name,
720
809
  ref_type=str(ref_type),
721
810
  ref_value=str(ref_value),
@@ -762,6 +851,9 @@ class DelayDifferentialAnalyzer:
762
851
  bg0=np.nan,
763
852
  bg1=np.nan,
764
853
  bg_mode=bg_mode,
854
+ bg_scale=np.nan,
855
+ peak_width=peak_width,
856
+ bg_width=np.nan,
765
857
  peak_name=pk.name,
766
858
  ref_type=str(ref_type),
767
859
  ref_value=str(ref_value),
@@ -783,6 +875,9 @@ class DelayDifferentialAnalyzer:
783
875
  b_int, b_abs = self._integrals_from_diff(q_ref, diff, (b0, b1))
784
876
  bg0, bg1 = float(b0), float(b1)
785
877
 
878
+ b_int = float(b_int) * float(bg_scale)
879
+ b_abs = float(b_abs) * float(bg_scale)
880
+
786
881
  rows.append(
787
882
  dict(
788
883
  delay_fs=int(d),
@@ -795,6 +890,9 @@ class DelayDifferentialAnalyzer:
795
890
  bg0=bg0,
796
891
  bg1=bg1,
797
892
  bg_mode=bg_mode,
893
+ bg_scale=float(bg_scale),
894
+ peak_width=float(peak_width),
895
+ bg_width=float(bg_width),
798
896
  peak_name=pk.name,
799
897
  ref_type=str(ref_type),
800
898
  ref_value=str(ref_value),
@@ -985,7 +1083,7 @@ class FluenceDifferentialAnalyzer:
985
1083
 
986
1084
  Output columns (long-form):
987
1085
  fluence_mJ_cm2, region, int_delta, int_abs_delta,
988
- q0, q1, bg0, bg1, bg_mode, peak_name,
1086
+ q0, q1, bg0, bg1, bg_mode, bg_scale, peak_width, bg_width, peak_name,
989
1087
  ref_type, ref_value, azim0, azim1, delay_fs
990
1088
 
991
1089
  ``region`` is either ``"peak"`` or ``"background"``.
@@ -1108,8 +1206,9 @@ class FluenceDifferentialAnalyzer:
1108
1206
  def _bg_ranges(
1109
1207
  q_range: Tuple[float, float],
1110
1208
  bg_mode: str,
1209
+ bg_range: Optional[Tuple[float, float]] = None,
1111
1210
  ) -> Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]]:
1112
- """Construct equal-width adjacent background interval(s) for a peak range."""
1211
+ """Construct background interval(s) for a peak range."""
1113
1212
  q0, q1 = float(q_range[0]), float(q_range[1])
1114
1213
  if q1 < q0:
1115
1214
  q0, q1 = q1, q0
@@ -1124,7 +1223,37 @@ class FluenceDifferentialAnalyzer:
1124
1223
  return (q1, q1 + w)
1125
1224
  if m == "avg":
1126
1225
  return ((q0 - w, q0), (q1, q1 + w))
1127
- raise ValueError("bg_mode must be 'before', 'after', or 'avg'.")
1226
+ if m == "custom":
1227
+ if bg_range is None:
1228
+ raise ValueError("bg_mode='custom' requires bg_range=(q0,q1).")
1229
+ return _normalize_q_range(bg_range, name="bg_range")
1230
+ raise ValueError("bg_mode must be 'before', 'after', 'avg', or 'custom'.")
1231
+
1232
+ @staticmethod
1233
+ def _background_width(
1234
+ bg_ranges: Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]],
1235
+ bg_mode: str,
1236
+ ) -> float:
1237
+ """Return the effective background q width."""
1238
+ if str(bg_mode).strip().lower() == "avg":
1239
+ return float(abs(float(bg_ranges[0][1]) - float(bg_ranges[0][0]))) # type: ignore[index]
1240
+ return float(abs(float(bg_ranges[1]) - float(bg_ranges[0]))) # type: ignore[arg-type]
1241
+
1242
+ @classmethod
1243
+ def _background_scale(
1244
+ cls,
1245
+ q_range: Tuple[float, float],
1246
+ bg_ranges: Union[Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]]],
1247
+ bg_mode: str,
1248
+ ) -> float:
1249
+ """Scale custom-background integrals to the peak q width."""
1250
+ if str(bg_mode).strip().lower() != "custom":
1251
+ return 1.0
1252
+ peak_width = abs(float(q_range[1]) - float(q_range[0]))
1253
+ bg_width = cls._background_width(bg_ranges, bg_mode)
1254
+ if bg_width <= 0:
1255
+ raise ValueError("bg_range must have positive width.")
1256
+ return float(peak_width / bg_width)
1128
1257
 
1129
1258
  @staticmethod
1130
1259
  def _interp_to_ref_grid(q_ref: np.ndarray, q: np.ndarray, I: np.ndarray) -> np.ndarray:
@@ -1271,7 +1400,10 @@ class FluenceDifferentialAnalyzer:
1271
1400
 
1272
1401
  q_range = (float(pk.q_range[0]), float(pk.q_range[1]))
1273
1402
  bg_mode = str(pk.bg_mode).strip().lower()
1274
- bg_ranges = self._bg_ranges(q_range, bg_mode)
1403
+ bg_ranges = self._bg_ranges(q_range, bg_mode, pk.bg_range)
1404
+ peak_width = abs(float(q_range[1]) - float(q_range[0]))
1405
+ bg_width = self._background_width(bg_ranges, bg_mode)
1406
+ bg_scale = self._background_scale(q_range, bg_ranges, bg_mode)
1275
1407
 
1276
1408
  rows: List[Dict[str, Any]] = []
1277
1409
 
@@ -1287,6 +1419,9 @@ class FluenceDifferentialAnalyzer:
1287
1419
  bg0=np.nan,
1288
1420
  bg1=np.nan,
1289
1421
  bg_mode=bg_mode,
1422
+ bg_scale=np.nan,
1423
+ peak_width=peak_width,
1424
+ bg_width=np.nan,
1290
1425
  peak_name=pk.name,
1291
1426
  ref_type=str(ref_type),
1292
1427
  ref_value=str(ref_value),
@@ -1332,6 +1467,9 @@ class FluenceDifferentialAnalyzer:
1332
1467
  bg0=np.nan,
1333
1468
  bg1=np.nan,
1334
1469
  bg_mode=bg_mode,
1470
+ bg_scale=np.nan,
1471
+ peak_width=peak_width,
1472
+ bg_width=np.nan,
1335
1473
  peak_name=pk.name,
1336
1474
  ref_type=str(ref_type),
1337
1475
  ref_value=str(ref_value),
@@ -1353,6 +1491,9 @@ class FluenceDifferentialAnalyzer:
1353
1491
  b_int, b_abs = self._integrals_from_diff(q_ref, diff, (b0, b1))
1354
1492
  bg0, bg1 = float(b0), float(b1)
1355
1493
 
1494
+ b_int = float(b_int) * float(bg_scale)
1495
+ b_abs = float(b_abs) * float(bg_scale)
1496
+
1356
1497
  rows.append(
1357
1498
  dict(
1358
1499
  fluence_mJ_cm2=float(f),
@@ -1364,6 +1505,9 @@ class FluenceDifferentialAnalyzer:
1364
1505
  bg0=bg0,
1365
1506
  bg1=bg1,
1366
1507
  bg_mode=bg_mode,
1508
+ bg_scale=float(bg_scale),
1509
+ peak_width=float(peak_width),
1510
+ bg_width=float(bg_width),
1367
1511
  peak_name=pk.name,
1368
1512
  ref_type=str(ref_type),
1369
1513
  ref_value=str(ref_value),
@@ -1824,7 +1968,9 @@ def build_multi_fluence_integral_series(
1824
1968
 
1825
1969
  Each returned dict has keys:
1826
1970
  - "experiment": original experiment dict
1971
+ - "fluence_scale": float (optional x-scale for plotting)
1827
1972
  - "fluence_offset": float (optional x-shift for plotting)
1973
+ - "delay_offset_fs": float (optional display offset for fixed-delay labels)
1828
1974
  - "fluence_mJ_cm2": 1D np.ndarray (x axis, no offset)
1829
1975
  - "int_delta": 1D np.ndarray (peak ΔI)
1830
1976
  - "int_abs_delta": 1D np.ndarray (peak |ΔI|)
@@ -1865,7 +2011,14 @@ def build_multi_fluence_integral_series(
1865
2011
  if ref_value is None:
1866
2012
  raise ValueError("Each experiment must provide ref_value (e.g. dark scans or fluence).")
1867
2013
 
2014
+ fluence_scale = float(exp.get("fluence_scale", 1.0))
1868
2015
  fluence_offset = float(exp.get("fluence_offset", 0.0))
2016
+ delay_offset_fs = float(
2017
+ exp.get(
2018
+ "delay_offset_fs",
2019
+ float(exp.get("delay_offset_ps", 0.0)) * 1000.0,
2020
+ )
2021
+ )
1869
2022
 
1870
2023
  analyzer = FluenceDifferentialAnalyzer(
1871
2024
  sample_name=sample_name,
@@ -1929,7 +2082,9 @@ def build_multi_fluence_integral_series(
1929
2082
  series_list.append(
1930
2083
  dict(
1931
2084
  experiment=exp,
2085
+ fluence_scale=float(fluence_scale),
1932
2086
  fluence_offset=float(fluence_offset),
2087
+ delay_offset_fs=float(delay_offset_fs),
1933
2088
  fluence_mJ_cm2=np.asarray(x_flu, float),
1934
2089
  int_delta=np.asarray(y_delta, float),
1935
2090
  int_abs_delta=np.asarray(y_abs, float),