xrayscatteringtools 1.2.0__tar.gz → 1.3.0__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 (67) hide show
  1. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/.github/workflows/tests.yml +5 -3
  2. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/PKG-INFO +1 -1
  3. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/pyproject.toml +1 -1
  4. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/calib/masking.py +100 -0
  5. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/io.py +4 -1
  6. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/.gitattributes +0 -0
  7. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/.gitignore +0 -0
  8. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/LICENSE +0 -0
  9. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/README.md +0 -0
  10. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/__init__.py +0 -0
  11. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/azimuthal.py +0 -0
  12. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/calib/__init__.py +0 -0
  13. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/calib/geometry_calibration.py +0 -0
  14. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/calib/scattering_corrections.py +0 -0
  15. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/calib/timetool_calibration.py +0 -0
  16. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/data/Jungfrau4M.h5 +0 -0
  17. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/epicsArch.py +0 -0
  18. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/plotting.py +0 -0
  19. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/__init__.py +0 -0
  20. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/IAM/Compton_Factors.npy +0 -0
  21. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/IAM/Scattering_Factors.npy +0 -0
  22. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/geometries/SF6__CCSD_T_DHK__aug_cc_pV5Z_DK.h5 +0 -0
  23. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/geometries/make_geometry_h5.py +0 -0
  24. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/NBD__CCSD_FC__aug_cc_pVDZ.h5 +0 -0
  25. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/NBD__HF__cc_pVTZ.h5 +0 -0
  26. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/Ne__CCSD_X2C__aug_cc_pVDZ_DK.h5 +0 -0
  27. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/Ne__CCSD_X2C__aug_cc_pVQZ_DK.h5 +0 -0
  28. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/Ne__CCSD_X2C__aug_cc_pVTZ_DK.h5 +0 -0
  29. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/QC__CCSD_FC__aug_cc_pVDZ.h5 +0 -0
  30. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/QC__HF__cc_pVTZ.h5 +0 -0
  31. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__CCSD_FC__aug_cc_pVDZ.h5 +0 -0
  32. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__CCSD_FC__aug_cc_pVTZ.h5 +0 -0
  33. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__CCSD__aug_cc_pVDZ.h5 +0 -0
  34. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__HF__aug_cc_pVDZ.h5 +0 -0
  35. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__HF__cc_pVTZ.h5 +0 -0
  36. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/SF6__MP2__aug_cc_pVDZ.h5 +0 -0
  37. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/convert_ab_initio_to_h5.py +0 -0
  38. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/data/patterns/convert_thistle_to_h5.py +0 -0
  39. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/geometries.py +0 -0
  40. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/geometries.pyi +0 -0
  41. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/iam.py +0 -0
  42. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/patterns.py +0 -0
  43. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/theory/patterns.pyi +0 -0
  44. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/src/xrayscatteringtools/utils.py +0 -0
  45. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/__init__.py +0 -0
  46. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/DMP_D.xyz +0 -0
  47. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/DMP_L1.xyz +0 -0
  48. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/DMP_L2.xyz +0 -0
  49. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/DMP_ground.xyz +0 -0
  50. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/azav_test.ipynb +0 -0
  51. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/calib_test.ipynb +0 -0
  52. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/geometries_test.ipynb +0 -0
  53. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/iam_oriented_test.ipynb +0 -0
  54. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/iam_pattern_test.ipynb +0 -0
  55. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/old/patterns_test.ipynb +0 -0
  56. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/sf6_test.xyz +0 -0
  57. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_azimuthal.py +0 -0
  58. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_geometries.py +0 -0
  59. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_geometry_calibration.py +0 -0
  60. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_iam.py +0 -0
  61. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_io.py +0 -0
  62. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_masking.py +0 -0
  63. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_patterns.py +0 -0
  64. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_plotting.py +0 -0
  65. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_scattering_corrections.py +0 -0
  66. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_timetool_calibration.py +0 -0
  67. {xrayscatteringtools-1.2.0 → xrayscatteringtools-1.3.0}/tests/test_utils.py +0 -0
@@ -8,16 +8,18 @@ on:
8
8
 
9
9
  jobs:
10
10
  test:
11
- runs-on: ubuntu-latest
11
+ # Pinned: ubuntu-latest is now 26.04, which has no Python 3.9 builds.
12
+ runs-on: ubuntu-24.04
12
13
  strategy:
14
+ fail-fast: false
13
15
  matrix:
14
16
  python-version: ["3.9", "3.10", "3.11", "3.12"]
15
17
 
16
18
  steps:
17
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v5
18
20
 
19
21
  - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v5
22
+ uses: actions/setup-python@v6
21
23
  with:
22
24
  python-version: ${{ matrix.python-version }}
23
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xrayscatteringtools
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A library for gas phase analysis at the CXI endstation
5
5
  Project-URL: Homepage, https://github.com/Weber-Group/xrayscatteringtools
6
6
  Project-URL: Issues, https://github.com/Weber-Group/xrayscatteringtools/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xrayscatteringtools"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  description = "A library for gas phase analysis at the CXI endstation"
9
9
  authors = [
10
10
  { name = "David Romano", email = "david_j_romano@brown.edu" }
@@ -511,6 +511,9 @@ class MaskMaker:
511
511
  q_map = compute_q_map(J4M.x, J4M.y, x0, y0, z0, tx, ty, keV, z_off)
512
512
  q_bins = np.linspace(np.nanmin(q_map[~np.isnan(sample_avg_masked)]), np.nanmax(q_map[~np.isnan(sample_avg_masked)]), n_q_bins)
513
513
 
514
+ # Per-ring histograms and chosen bounds, for the waterfall summary plot
515
+ ring_records = []
516
+
514
517
  for q_idx, q_lo in tqdm(enumerate(q_bins[:-1]), total=len(q_bins) - 1):
515
518
  q_hi = q_bins[q_idx + 1]
516
519
  ring_sel = (q_map >= q_lo) & (q_map < q_hi)
@@ -534,6 +537,7 @@ class MaskMaker:
534
537
  100 * (1 - kept_frac),
535
538
  )
536
539
  self.sample_mask &= ring_mask
540
+ chosen_lb, chosen_ub, is_manual = lower_bound, upper_bound, False
537
541
  else:
538
542
  # Manual review required — show histogram and detector ring
539
543
  ring_data = q_ring[ring_sel].flatten()
@@ -555,6 +559,7 @@ class MaskMaker:
555
559
  ax_hist.set_xlabel('Intensity')
556
560
  ax_hist.set_ylabel('Counts')
557
561
  ax_hist.grid(True)
562
+ ax_hist.minorticks_on()
558
563
 
559
564
  # Right: pcolormesh of the ring pixels on the detector
560
565
  ring_display = np.where(ring_sel, sample_avg_corrected, np.nan)
@@ -587,10 +592,38 @@ class MaskMaker:
587
592
  manual_mask = np.ones_like(q_ring, dtype=bool)
588
593
  manual_mask[valid] = (q_ring[valid] >= user_lb) & (q_ring[valid] <= user_ub)
589
594
  self.sample_mask &= manual_mask
595
+ chosen_lb, chosen_ub, is_manual = user_lb, user_ub, True
596
+
597
+ if plotting:
598
+ ring_vals = q_ring[ring_sel]
599
+ ring_vals = ring_vals[np.isfinite(ring_vals)]
600
+ if ring_vals.size > 0:
601
+ # Normalise by the ring median so weak high-q rings are as
602
+ # readable as the bright low-q ones on a shared axis
603
+ median = np.median(ring_vals)
604
+ scale = median if median > 0 else 1.0
605
+ ring_vals = ring_vals / scale
606
+ lb_n, ub_n = chosen_lb / scale, chosen_ub / scale
607
+ p01, p05, p995, p999 = np.percentile(ring_vals, [0.1, 0.5, 99.5, 99.9])
608
+ # Widen the range out to the bounds so the cut region is
609
+ # visible, but stop at the 0.1/99.9 percentiles so a few
610
+ # hot pixels (or a very loose manual bound) don't stretch the axis
611
+ counts, edges = np.histogram(
612
+ ring_vals,
613
+ bins=200,
614
+ range=(min(p05, max(lb_n, p01)), max(p995, min(ub_n, p999))),
615
+ )
616
+ cut_frac = np.mean((ring_vals < lb_n) | (ring_vals > ub_n))
617
+ ring_records.append(
618
+ dict(q_lo=q_lo, q_hi=q_hi, counts=counts, edges=edges,
619
+ lb=lb_n, ub=ub_n, manual=is_manual, cut_frac=cut_frac)
620
+ )
590
621
 
591
622
  self.sample_mask = self.sample_mask.astype(bool)
592
623
 
593
624
  if plotting:
625
+ self._plot_q_ring_waterfall(ring_records, n_std)
626
+
594
627
  sample_avg_all_masked = np.where(self.sample_mask, sample_avg_masked, np.nan)
595
628
 
596
629
  fig, axes = plt.subplots(1, 3, figsize=(18, 8))
@@ -602,6 +635,73 @@ class MaskMaker:
602
635
 
603
636
  print(f'Masked percentage from sample: {100 * self._masked_fraction(self.sample_mask):.2f}%')
604
637
 
638
+ def _plot_q_ring_waterfall(self, ring_records: list[dict], n_std: float) -> None:
639
+ """Waterfall of per-ring intensity histograms with the mask bounds shaded.
640
+
641
+ Intensities are divided by each ring's median, and each histogram is
642
+ normalised to unit peak height and offset vertically by its ring index.
643
+ The intensity range that survives the mask is shaded in steel-blue for
644
+ auto-accepted rings and red for rings whose bounds were entered
645
+ manually; the range that is cut is shaded grey. The percentage of
646
+ ring pixels cut is printed to the right of each row.
647
+ """
648
+ if not ring_records:
649
+ return
650
+
651
+ n_rings = len(ring_records)
652
+ spacing = 0.5
653
+ colors = {False: 'steelblue', True: 'tab:red'}
654
+ cut_color = 'tab:gray'
655
+
656
+ fig, ax = plt.subplots(figsize=(18, max(8, 0.12 * n_rings)))
657
+ for i, rec in enumerate(ring_records):
658
+ offset = i * spacing
659
+ centers = 0.5 * (rec['edges'][:-1] + rec['edges'][1:])
660
+ peak = rec['counts'].max()
661
+ height = rec['counts'] / peak if peak > 0 else rec['counts'].astype(float)
662
+ kept = (centers >= rec['lb']) & (centers <= rec['ub'])
663
+ # Lower rings are drawn in front so their peaks occlude the rows above
664
+ base_z = 4 * (n_rings - i)
665
+ ax.fill_between(centers, offset, offset + height, color='white', zorder=base_z)
666
+ ax.fill_between(
667
+ centers, offset, offset + height, where=kept,
668
+ color=colors[rec['manual']], alpha=0.6, linewidth=0, zorder=base_z + 1,
669
+ )
670
+ # Cut tails are usually near-zero counts, so shade at least the full
671
+ # row height there to keep them visible
672
+ ax.fill_between(
673
+ centers, offset, offset + np.maximum(height, spacing), where=~kept,
674
+ color=cut_color, alpha=0.5, linewidth=0, zorder=base_z + 2,
675
+ )
676
+ ax.plot(centers, offset + height, color='k', lw=0.6, zorder=base_z + 3)
677
+ ax.text(
678
+ 1.005, offset, f"{100 * rec['cut_frac']:.1f}%",
679
+ transform=ax.get_yaxis_transform(), fontsize=6, va='bottom',
680
+ color=colors[rec['manual']],
681
+ )
682
+ ax.text(1.005, 1.0, '% cut', transform=ax.transAxes, fontsize=8, va='bottom')
683
+
684
+ # Label a subset of rows with their q-centre
685
+ tick_step = max(1, n_rings // 15)
686
+ tick_idx = np.arange(0, n_rings, tick_step)
687
+ ax.set_yticks(tick_idx * spacing)
688
+ ax.set_yticklabels(
689
+ [f"{0.5 * (ring_records[i]['q_lo'] + ring_records[i]['q_hi']):.3f}" for i in tick_idx]
690
+ )
691
+ ax.set_ylim(-spacing, (n_rings - 1) * spacing + 1.5)
692
+ ax.set_xlabel('Intensity / ring median (Thompson-corrected)')
693
+ ax.set_ylabel(r'$q\;(\AA^{-1})$')
694
+ ax.set_title(f'Run {self.sample_run_number} Q-ring Histograms (n_std={n_std})')
695
+ legend_elements = [
696
+ Patch(facecolor='steelblue', alpha=0.6, edgecolor='k', label='Auto bounds (kept)'),
697
+ Patch(facecolor='tab:red', alpha=0.6, edgecolor='k', label='Manual bounds (kept)'),
698
+ Patch(facecolor=cut_color, alpha=0.5, edgecolor='k', label='Cut by mask'),
699
+ ]
700
+ ax.legend(handles=legend_elements, loc='lower left', bbox_to_anchor=(0, 1.0), ncol=3, frameon=False)
701
+ ax.grid(True, axis='x', alpha=0.3)
702
+ plt.tight_layout()
703
+ plt.show()
704
+
605
705
  def diagnose_q_bins(
606
706
  self,
607
707
  n_std: float = 2,
@@ -155,7 +155,10 @@ def combineRuns(runNumbers, folders, keys_to_combine, keys_to_sum, keys_to_check
155
155
  if isinstance(archPVs, str):
156
156
  archPVs = [archPVs]
157
157
  archive = EpicsArchive()
158
- unixTime = data_combined['unixTime']
158
+ for i, data in enumerate(data_array):
159
+ if 'unixTime' not in data:
160
+ raise KeyError(f"'unixTime' not found in run {runNumbers[i]}; it is required for archPVs.")
161
+ unixTime = np.concatenate([np.squeeze(d['unixTime']) for d in data_array])
159
162
  for pv in tqdm(archPVs, desc="Loading EPICS PVs"):
160
163
  pv_chunks = []
161
164
  for i, runNumber in enumerate(runNumbers):