sxs 2024.0.26__tar.gz → 2024.0.28__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 (130) hide show
  1. {sxs-2024.0.26 → sxs-2024.0.28}/.github/workflows/build.yml +2 -2
  2. {sxs-2024.0.26 → sxs-2024.0.28}/CITATION.cff +2 -2
  3. {sxs-2024.0.26 → sxs-2024.0.28}/PKG-INFO +5 -5
  4. {sxs-2024.0.26 → sxs-2024.0.28}/pyproject.toml +4 -4
  5. sxs-2024.0.28/sxs/__version__.py +1 -0
  6. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/alignment.py +263 -30
  7. sxs-2024.0.26/sxs/__version__.py +0 -1
  8. {sxs-2024.0.26 → sxs-2024.0.28}/.codecov.yml +0 -0
  9. {sxs-2024.0.26 → sxs-2024.0.28}/.github/dependabot.yml +0 -0
  10. {sxs-2024.0.26 → sxs-2024.0.28}/.github/scripts/parse_bump_rule.py +0 -0
  11. {sxs-2024.0.26 → sxs-2024.0.28}/.github/workflows/pr_rtd_link.yml +0 -0
  12. {sxs-2024.0.26 → sxs-2024.0.28}/.gitignore +0 -0
  13. {sxs-2024.0.26 → sxs-2024.0.28}/.readthedocs.yaml +0 -0
  14. {sxs-2024.0.26 → sxs-2024.0.28}/LICENSE +0 -0
  15. {sxs-2024.0.26 → sxs-2024.0.28}/README.md +0 -0
  16. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/catalog.md +0 -0
  17. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/horizons.md +0 -0
  18. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/load.md +0 -0
  19. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/metadata.md +0 -0
  20. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/simulation.md +0 -0
  21. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/simulations.md +0 -0
  22. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/time_series.md +0 -0
  23. {sxs-2024.0.26 → sxs-2024.0.28}/docs/api/waveforms.md +0 -0
  24. {sxs-2024.0.26 → sxs-2024.0.28}/docs/html/main.html +0 -0
  25. {sxs-2024.0.26 → sxs-2024.0.28}/docs/images/favicon.ico +0 -0
  26. {sxs-2024.0.26 → sxs-2024.0.28}/docs/index.md +0 -0
  27. {sxs-2024.0.26 → sxs-2024.0.28}/docs/javascript/mathjax.js +0 -0
  28. {sxs-2024.0.26 → sxs-2024.0.28}/docs/julia.md +0 -0
  29. {sxs-2024.0.26 → sxs-2024.0.28}/docs/mathematica.md +0 -0
  30. {sxs-2024.0.26 → sxs-2024.0.28}/docs/stylesheets/extra.css +0 -0
  31. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/00-Introduction.ipynb +0 -0
  32. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/01-Simulations_and_Metadata.ipynb +0 -0
  33. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/02-Simulation.ipynb +0 -0
  34. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/03-Horizons.ipynb +0 -0
  35. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/04-Waveforms.ipynb +0 -0
  36. {sxs-2024.0.26 → sxs-2024.0.28}/docs/tutorials/05-PreprocessingForFFTs.ipynb +0 -0
  37. {sxs-2024.0.26 → sxs-2024.0.28}/mkdocs.yml +0 -0
  38. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/__init__.py +0 -0
  39. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/caltechdata/__init__.py +0 -0
  40. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/caltechdata/catalog.py +0 -0
  41. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/caltechdata/login.py +0 -0
  42. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/catalog/__init__.py +0 -0
  43. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/catalog/catalog.py +0 -0
  44. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/catalog/create.py +0 -0
  45. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/catalog/description.py +0 -0
  46. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/handlers.py +0 -0
  47. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/horizons/__init__.py +0 -0
  48. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/horizons/spec_horizons_h5.py +0 -0
  49. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/horizons/xor_multishuffle_bzip2.py +0 -0
  50. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/julia/GWFrames.py +0 -0
  51. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/julia/__init__.py +0 -0
  52. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/juliapkg.json +0 -0
  53. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/metadata/__init__.py +0 -0
  54. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/metadata/metadata.py +0 -0
  55. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/simulations/__init__.py +0 -0
  56. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/simulations/local.py +0 -0
  57. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/simulations/simulation.py +0 -0
  58. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/simulations/simulations.py +0 -0
  59. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/time_series.py +0 -0
  60. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/__init__.py +0 -0
  61. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/bitwise.py +0 -0
  62. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/decimation/__init__.py +0 -0
  63. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/decimation/greedy_spline.py +0 -0
  64. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/decimation/linear_bisection.py +0 -0
  65. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/decimation/peak_greed.py +0 -0
  66. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/decimation/suppression.py +0 -0
  67. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/dicts.py +0 -0
  68. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/downloads.py +0 -0
  69. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/files.py +0 -0
  70. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/formats.py +0 -0
  71. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/inspire.py +0 -0
  72. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/__init__.py +0 -0
  73. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/comparisons.py +0 -0
  74. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/conversion.py +0 -0
  75. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/dataset.py +0 -0
  76. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/horizons.py +0 -0
  77. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/metadata.py +0 -0
  78. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/waveform_amp_phase.py +0 -0
  79. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/lvcnr/waveforms.py +0 -0
  80. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/monotonicity.py +0 -0
  81. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/pretty_print.py +0 -0
  82. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/__init__.py +0 -0
  83. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/ads.py +0 -0
  84. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/arxiv.py +0 -0
  85. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/fairchild_report.py +0 -0
  86. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/inspire.py +0 -0
  87. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/journal_abbreviations.py +0 -0
  88. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/references/references.py +0 -0
  89. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/select.py +0 -0
  90. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/smooth_functions.py +0 -0
  91. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/sxs_directories.py +0 -0
  92. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/sxs_identifiers.py +0 -0
  93. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/utilities/url.py +0 -0
  94. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/__init__.py +0 -0
  95. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/__init__.py +0 -0
  96. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/grathena.py +0 -0
  97. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/lvc.py +0 -0
  98. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/nrar.py +0 -0
  99. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/rotating_paired_diff_multishuffle_bzip2.py +0 -0
  100. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/rotating_paired_xor_multishuffle_bzip2.py +0 -0
  101. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/format_handlers/spectre_cce_v1.py +0 -0
  102. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/memory.py +0 -0
  103. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/mode_utilities.py +0 -0
  104. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/transformations.py +0 -0
  105. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/waveform_grid.py +0 -0
  106. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/waveform_mixin.py +0 -0
  107. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/waveform_modes.py +0 -0
  108. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/waveforms/waveform_signal.py +0 -0
  109. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/__init__.py +0 -0
  110. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/api/__init__.py +0 -0
  111. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/api/deposit.py +0 -0
  112. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/api/login.py +0 -0
  113. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/api/records.py +0 -0
  114. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/catalog.py +0 -0
  115. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/creators.py +0 -0
  116. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/simannex.py +0 -0
  117. {sxs-2024.0.26 → sxs-2024.0.28}/sxs/zenodo/surrogatemodeling.py +0 -0
  118. {sxs-2024.0.26 → sxs-2024.0.28}/tests/__init__.py +0 -0
  119. {sxs-2024.0.26 → sxs-2024.0.28}/tests/conftest.py +0 -0
  120. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_catalog.py +0 -0
  121. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_horizons.py +0 -0
  122. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_julia.py +0 -0
  123. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_loader.py +0 -0
  124. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_metadata.py +0 -0
  125. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_simulation.py +0 -0
  126. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_time_series.py +0 -0
  127. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_transformations.py +0 -0
  128. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_utilities.py +0 -0
  129. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_waveform_rotations.py +0 -0
  130. {sxs-2024.0.26 → sxs-2024.0.28}/tests/test_waveforms.py +0 -0
@@ -30,12 +30,12 @@ jobs:
30
30
  fail-fast: false
31
31
  matrix:
32
32
  os: [ubuntu-latest, macos-latest, windows-latest]
33
- python-version: ['3.8', '3.12']
33
+ python-version: ['3.10', '3.12']
34
34
 
35
35
  steps:
36
36
  - name: Skip replicates on main branch
37
37
  env:
38
- skip_replicates: ${{ github.ref == 'refs/heads/main' && (matrix.os != 'ubuntu-latest' || matrix.python-version != '3.12') }}
38
+ skip_replicates: ${{ github.ref == 'refs/heads/main' && matrix.os != 'ubuntu-latest' }}
39
39
  shell: bash
40
40
  run: |
41
41
  echo "skipping_build_and_test_replicate=${skip_replicates}" >> $GITHUB_ENV
@@ -10,5 +10,5 @@ authors:
10
10
  title: "The sxs package"
11
11
  license: MIT
12
12
  doi: 10.5281/zenodo.4034006
13
- version: 2024.0.26
14
- date-released: 2024-12-18
13
+ version: 2024.0.28
14
+ date-released: 2025-01-31
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sxs
3
- Version: 2024.0.26
3
+ Version: 2024.0.28
4
4
  Summary: Interface to data produced by the Simulating eXtreme Spacetimes collaboration
5
5
  Project-URL: Homepage, https://github.com/sxs-collaboration/sxs
6
6
  Project-URL: Documentation, https://sxs.readthedocs.io/
@@ -34,18 +34,18 @@ Classifier: Operating System :: OS Independent
34
34
  Classifier: Programming Language :: Python :: 3
35
35
  Classifier: Topic :: Scientific/Engineering :: Astronomy
36
36
  Classifier: Topic :: Scientific/Engineering :: Physics
37
- Requires-Python: >=3.8
37
+ Requires-Python: >=3.10
38
38
  Requires-Dist: h5py>=3
39
39
  Requires-Dist: inflection>=0.5.1
40
40
  Requires-Dist: juliacall>=0.9.20
41
41
  Requires-Dist: numba>=0.55; implementation_name == 'cpython'
42
- Requires-Dist: numpy>=1.20
42
+ Requires-Dist: numpy>=1.25
43
43
  Requires-Dist: pandas>=1.1.2
44
44
  Requires-Dist: pytz>=2020.1
45
45
  Requires-Dist: quaternionic>=1.0
46
46
  Requires-Dist: requests>=2.24.0
47
- Requires-Dist: scipy>=1.7
48
- Requires-Dist: spherical>=1.0
47
+ Requires-Dist: scipy>=1.13
48
+ Requires-Dist: spherical>=1.0.15
49
49
  Requires-Dist: tqdm>=4.63.1
50
50
  Requires-Dist: urllib3>=1.25.10
51
51
  Provides-Extra: docs
@@ -3,7 +3,7 @@ name = "sxs"
3
3
  dynamic = ["version"]
4
4
  description = "Interface to data produced by the Simulating eXtreme Spacetimes collaboration"
5
5
  readme = "README.md"
6
- requires-python = ">=3.8"
6
+ requires-python = ">=3.10"
7
7
  license = {file = "LICENSE"}
8
8
  authors = [
9
9
  { name = "Michael Boyle", email = "michael.oliver.boyle@gmail.com" }
@@ -18,11 +18,11 @@ classifiers = [
18
18
  "Topic :: Scientific/Engineering :: Astronomy"
19
19
  ]
20
20
  dependencies = [
21
- "numpy >=1.20",
22
- "scipy >=1.7",
21
+ "numpy >=1.25",
22
+ "scipy >=1.13",
23
23
  "numba >=0.55; implementation_name == 'cpython'",
24
24
  "quaternionic >=1.0",
25
- "spherical >=1.0",
25
+ "spherical >=1.0.15",
26
26
  "h5py >=3",
27
27
  "inflection >=0.5.1",
28
28
  "requests >=2.24.0",
@@ -0,0 +1 @@
1
+ __version__ = "2024.0.28"
@@ -1,12 +1,13 @@
1
1
  import numpy as np
2
2
 
3
+ import quaternionic
4
+
3
5
  from scipy.integrate import trapezoid
4
6
 
5
7
  import multiprocessing as mp
6
8
  from functools import partial
7
9
 
8
10
 
9
-
10
11
  def align1d(wa, wb, t1, t2, n_brute_force=None):
11
12
  """Align waveforms by shifting in time
12
13
 
@@ -77,7 +78,6 @@ def align1d(wa, wb, t1, t2, n_brute_force=None):
77
78
 
78
79
  """
79
80
  from scipy.interpolate import CubicSpline
80
- from scipy.integrate import trapezoid
81
81
  from scipy.optimize import least_squares
82
82
 
83
83
  # Check that (t1, t2) makes sense and is actually contained in both waveforms
@@ -89,8 +89,8 @@ def align1d(wa, wb, t1, t2, n_brute_force=None):
89
89
  raise ValueError(f"(t1,t2)=({t1}, {t2}) not contained in wb.t, which spans ({wb.t[0]}, {wb.t[-1]})")
90
90
 
91
91
  # Figure out time offsets to try
92
- δt_lower = max(t1 - t2, wb.t[0] - t1)
93
- δt_upper = min(t2 - t1, wb.t[-1] - t2)
92
+ δt_lower = max(t1 - t2, t2 - wb.t[-1])
93
+ δt_upper = min(t2 - t1, t1 - wb.t[0])
94
94
 
95
95
  # We'll start by brute forcing, sampling time offsets evenly at as many
96
96
  # points as there are time steps in (t1,t2) in the input waveforms
@@ -126,15 +126,13 @@ def _cost2d(δt_δϕ, args):
126
126
 
127
127
  # Take the sqrt because least_squares squares the inputs...
128
128
  diff = trapezoid(
129
- np.sum(
130
- abs(modes_A(t_reference + δt) * np.exp(1j * m * δϕ) * δΨ_factor - modes_B) ** 2, axis=1
131
- ),
129
+ np.sum(abs(modes_A(t_reference + δt) * np.exp(1j * m * δϕ) * δΨ_factor - modes_B) ** 2, axis=1),
132
130
  t_reference,
133
131
  )
134
132
  return np.sqrt(diff / normalization)
135
133
 
136
134
 
137
- def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, include_modes=None, nprocs=None):
135
+ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, max_δt=np.inf, use_δΨ=False, include_modes=None, nprocs=None):
138
136
  """Align waveforms by shifting in time and phase
139
137
 
140
138
  This function determines the optimal time and phase offset to apply to `wa` by
@@ -166,7 +164,7 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
166
164
  Waveforms to be aligned
167
165
  t1 : float
168
166
  t2 : float
169
- Beginning and end of integration interval
167
+ Beginning and end of integration interval.
170
168
  n_brute_force_δt : int, optional
171
169
  Number of evenly spaced δt values between (t1-t2) and (t2-t1) to sample
172
170
  for the initial guess. By default, this is just the maximum number of
@@ -179,6 +177,10 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
179
177
  is not the default because, even though it is formally the right
180
178
  thing to do, it takes much longer to produce the same result
181
179
  as just using 5, which is much faster.
180
+ max_δt : float, optional
181
+ Max δt to allow for when choosing the initial guess.
182
+ use_δΨ : float, optional
183
+ Whether or not to allow for a h -> -h transformation.
182
184
  include_modes: list, optional
183
185
  A list containing the (ell, m) modes to be included in the L² norm.
184
186
  nprocs: int, optional
@@ -218,23 +220,19 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
218
220
  if t2 <= t1:
219
221
  raise ValueError(f"(t1,t2)=({t1}, {t2}) is out of order")
220
222
  if wa.t[0] > t1 or wa.t[-1] < t2:
221
- raise ValueError(
222
- f"(t1,t2)=({t1}, {t2}) not contained in wa.t, which spans ({wa.t[0]}, {wa.t[-1]})"
223
- )
223
+ raise ValueError(f"(t1,t2)=({t1}, {t2}) not contained in wa.t, which spans ({wa.t[0]}, {wa.t[-1]})")
224
224
  if wb.t[0] > t1 or wb.t[-1] < t2:
225
- raise ValueError(
226
- f"(t1,t2)=({t1}, {t2}) not contained in wb.t, which spans ({wb.t[0]}, {wb.t[-1]})"
227
- )
225
+ raise ValueError(f"(t1,t2)=({t1}, {t2}) not contained in wb.t, which spans ({wb.t[0]}, {wb.t[-1]})")
228
226
 
229
227
  # Figure out time offsets to try
230
- δt_lower = max(t1 - t2, wa.t[0] - t1)
231
- δt_upper = min(t2 - t1, wa.t[-1] - t2)
232
-
228
+ δt_lower = max(-max_δt, max(t1 - t2, t2 - wa.t[-1]))
229
+ δt_upper = min(max_δt, min(t2 - t1, t1 - wa.t[0]))
230
+
233
231
  # We'll start by brute forcing, sampling time offsets evenly at as many
234
232
  # points as there are time steps in (t1,t2) in the input waveforms
235
233
  if n_brute_force_δt is None:
236
234
  n_brute_force_δt = max(sum((wa.t >= t1) & (wa.t <= t2)), sum((wb.t >= t1) & (wb.t <= t2)))
237
- δt_brute_force = np.linspace(δt_lower, δt_upper, num=n_brute_force_δt)
235
+ δt_brute_force = np.array(list(np.linspace(δt_lower, δt_upper, num=n_brute_force_δt)) + [0.0])
238
236
 
239
237
  if n_brute_force_δϕ is None:
240
238
  n_brute_force_δϕ = 2 * wa.ell_max + 1
@@ -246,7 +244,7 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
246
244
 
247
245
  # Remove certain modes, if requested
248
246
  ell_max = min(wa.ell_max, wb.ell_max)
249
- if include_modes != None:
247
+ if include_modes is not None:
250
248
  for L in range(2, ell_max + 1):
251
249
  for M in range(-L, L + 1):
252
250
  if not (L, M) in include_modes:
@@ -254,17 +252,11 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
254
252
  wb.data[:, wb.index(L, M)] *= 0
255
253
 
256
254
  # Define the cost function
257
- modes_A = CubicSpline(
258
- wa.t, wa[:, wa.index(2, -2) : wa.index(ell_max + 1, -(ell_max + 1))].data
259
- )
260
- modes_B = CubicSpline(
261
- wb.t, wb[:, wb.index(2, -2) : wb.index(ell_max + 1, -(ell_max + 1))].data
262
- )(t_reference)
255
+ modes_A = CubicSpline(wa.t, wa[:, wa.index(2, -2) : wa.index(ell_max + 1, -(ell_max + 1))].data)
256
+ modes_B = CubicSpline(wb.t, wb[:, wb.index(2, -2) : wb.index(ell_max + 1, -(ell_max + 1))].data)(t_reference)
263
257
 
264
258
  normalization = trapezoid(
265
- CubicSpline(
266
- wb.t, wb[:, wb.index(2, -2) : wb.index(ell_max + 1, -(ell_max + 1))].norm ** 2
267
- )(t_reference),
259
+ CubicSpline(wb.t, wb[:, wb.index(2, -2) : wb.index(ell_max + 1, -(ell_max + 1))].norm ** 2)(t_reference),
268
260
  t_reference,
269
261
  )
270
262
 
@@ -272,7 +264,10 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
272
264
 
273
265
  optimums = []
274
266
  wa_primes = []
275
- for δΨ_factor in [-1, +1]:
267
+ δΨ_factors = [1]
268
+ if use_δΨ:
269
+ δΨ_factors = [-1, +1]
270
+ for δΨ_factor in δΨ_factors:
276
271
  # Optimize by brute force with multiprocessing
277
272
  cost_wrapper = partial(_cost2d, args=[modes_A, modes_B, t_reference, m, δΨ_factor, normalization])
278
273
 
@@ -310,3 +305,241 @@ def align2d(wa, wb, t1, t2, n_brute_force_δt=None, n_brute_force_δϕ=5, includ
310
305
  idx = np.argmin(abs(np.array([optimum.cost for optimum in optimums])))
311
306
 
312
307
  return optimums[idx].cost, wa_primes[idx], optimums[idx]
308
+
309
+
310
+ def _cost4d(δt_δso3, args):
311
+ from .. import WaveformModes
312
+
313
+ modes_A, modes_B, t_reference, normalization = args
314
+ δt = δt_δso3[0]
315
+ δSO3 = np.exp(quaternionic.array.from_vector_part(δt_δso3[1:]))
316
+
317
+ modes_A_at_δt = modes_A(t_reference + δt)
318
+ ell_max = int(np.sqrt(modes_A_at_δt.shape[1] + 4)) - 1
319
+
320
+ wa_prime = WaveformModes(
321
+ input_array=(modes_A_at_δt),
322
+ time=t_reference,
323
+ time_axis=0,
324
+ modes_axis=1,
325
+ ell_min=2,
326
+ ell_max=ell_max,
327
+ spin_weight=-2,
328
+ )
329
+
330
+ wa_prime = wa_prime.rotate(δSO3)
331
+
332
+ # Take the sqrt because least_squares squares the inputs...
333
+ diff = trapezoid(
334
+ np.sum(abs(wa_prime.data - modes_B) ** 2, axis=1),
335
+ t_reference,
336
+ )
337
+ return np.sqrt(diff / normalization)
338
+
339
+
340
+ def align4d(
341
+ wa,
342
+ wb,
343
+ t1,
344
+ t2,
345
+ n_brute_force_δt=1_000,
346
+ n_brute_force_δϕ=None,
347
+ max_δt=np.inf,
348
+ include_modes=None,
349
+ align2d_first=False,
350
+ nprocs=None,
351
+ ):
352
+ """Align waveforms by optimizing over a time translation and an SO(3) rotation.
353
+
354
+ This function determines the optimal transformation to apply to `wa` by
355
+ minimizing the averaged (over time) L² norm (over the sphere) of the
356
+ difference of the waveforms.
357
+
358
+ The integral is taken from time `t1` to `t2`.
359
+
360
+ Note that the input waveforms are assumed to be initially aligned at least
361
+ well enough that:
362
+
363
+ 1) the time span from `t1` to `t2` in the two waveforms will overlap at
364
+ least slightly after the second waveform is shifted in time; and
365
+ 2) waveform `wb` contains all the times corresponding to `t1` to `t2` in
366
+ waveform `wa`.
367
+
368
+ The first of these can usually be assured by simply aligning the peaks prior
369
+ to calling this function:
370
+
371
+ wa.t -= wa.max_norm_time() - wb.max_norm_time()
372
+
373
+ The second assumption will be satisfied as long as `t1` is not too close to
374
+ the beginning of `wb` and `t2` is not too close to the end.
375
+
376
+ Parameters
377
+ ----------
378
+ wa : WaveformModes
379
+ wb : WaveformModes
380
+ Waveforms to be aligned
381
+ t1 : float
382
+ t2 : float
383
+ Beginning and end of integration interval
384
+ n_brute_force_δt : int, optional
385
+ Number of evenly spaced δt values between (t1-t2) and (t2-t1) to sample
386
+ for the initial guess. By default, this is 1,000. If this is too small,
387
+ an incorrect local minimum may be found.
388
+ n_brute_force_δϕ : int, optional
389
+ Number of evenly spaced angles about the angular-velocity axis to sample
390
+ for the initial guess. By default, this is `2 * (2 * ell_max + 1)`.
391
+ max_δt : float, optional
392
+ Max δt to allow for when choosing the initial guess.
393
+ include_modes: list, optional
394
+ A list containing the (ell, m) modes to be included in the L² norm.
395
+ align2d_first : bool, optional
396
+ Do a 2d align first for an initial guess, with no SO(3) initial guess
397
+ (besides the phase returned by the 2d solve)
398
+ nprocs: int, optional
399
+ Number of cpus to use. Default is maximum number. If -1 is provided,
400
+ then no multiprocessing is performed.
401
+
402
+ Returns
403
+ -------
404
+ error: float
405
+ Cost of scipy.optimize.least_squares. This is 0.5 ||wa - wb||² / ||wb||²
406
+ wa_prime: WaveformModes
407
+ Resulting waveform after transforming `wa` using `optimum`
408
+ optimum: OptimizeResult
409
+ Result of scipy.optimize.least_squares
410
+
411
+ Notes
412
+ -----
413
+ Choosing the time interval is usually the most difficult choice to make when
414
+ aligning waveforms. Assuming you want to align during inspiral, the times
415
+ must span sufficiently long that the waveforms' norm (equivalently, orbital
416
+ frequency changes) significantly from `t1` to `t2`. This means that you
417
+ cannot always rely on a specific number of orbits, for example. Also note
418
+ that neither number should be too close to the beginning or end of either
419
+ waveform, to provide some "wiggle room".
420
+
421
+ """
422
+ from scipy.interpolate import CubicSpline
423
+ from scipy.optimize import least_squares
424
+ from .. import WaveformModes
425
+
426
+ if wa.spin_weight != wb.spin_weight:
427
+ raise ValueError(f"Waveform spin weights do not match: {wa.spin_weight=} != {wb.spin_weight=}")
428
+ spin_weight = wa.spin_weight
429
+ ell_min = max(wa.ell_min, wb.ell_min)
430
+ ell_max = min(wa.ell_max, wb.ell_max)
431
+
432
+ wa_orig = wa
433
+ wa = wa.copy()
434
+ wb = wb.copy()
435
+
436
+ # Check that (t1, t2) makes sense and is actually contained in both waveforms
437
+ if t2 <= t1:
438
+ raise ValueError(f"(t1,t2)=({t1}, {t2}) is out of order")
439
+ if wa.t[0] > t1 or wa.t[-1] < t2:
440
+ raise ValueError(f"(t1,t2)=({t1}, {t2}) not contained in wa.t, which spans ({wa.t[0]}, {wa.t[-1]})")
441
+ if wb.t[0] > t1 or wb.t[-1] < t2:
442
+ raise ValueError(f"(t1,t2)=({t1}, {t2}) not contained in wb.t, which spans ({wb.t[0]}, {wb.t[-1]})")
443
+
444
+ # Figure out time offsets to try
445
+ δt_lower = max(-max_δt, max(t1 - t2, t2 - wa.t[-1]))
446
+ δt_upper = min(max_δt, min(t2 - t1, t1 - wa.t[0]))
447
+
448
+ t_reference = wb.t[np.argmin(abs(wb.t - t1)) : np.argmin(abs(wb.t - t2)) + 1]
449
+
450
+ if not align2d_first:
451
+ # Get time initial guess
452
+ # Negative sign because align1d aligns wb to wa
453
+ δt_IG = -align1d(wa, wb, t1, t2)
454
+
455
+ wa_interp = wa.interpolate(t_reference + δt_IG)
456
+ wb_interp = wb.interpolate(t_reference)
457
+
458
+ # Get rotor initial guess
459
+ omegaa = wa_interp.angular_velocity
460
+ omegab = wb_interp.angular_velocity
461
+ R_IG = quaternionic.align(omegaa, omegab)
462
+ else:
463
+ _, _, res = align2d(
464
+ wa, wb, t1, t2, max_δt=50,
465
+ n_brute_force_δt=n_brute_force_δt, nprocs=nprocs
466
+ )
467
+ δt_IG = res.x[0]
468
+
469
+ wa_interp = wa.interpolate(t_reference + δt_IG)
470
+ wb_interp = wb.interpolate(t_reference)
471
+
472
+ # Get rotor initial guess
473
+ omegaa = wa_interp.angular_velocity
474
+ omegab = wb_interp.angular_velocity
475
+ R_IG = quaternionic.align(omegaa, omegab)
476
+
477
+ R_IG = quaternionic.array([0, 0, 0, res.x[1] / 2]) * R_IG
478
+
479
+ # Brute force over R_IG * exp(theta * z / 2) with δt_IG
480
+ n_brute_force_δϕ = n_brute_force_δϕ or 2 * (2 * ell_max + 1)
481
+ δt_δso3_brute_force = [
482
+ [δt_IG, *np.log(R_IG * np.exp(quaternionic.array([0, 0, 0, angle / 2]))).vector]
483
+ for angle in np.linspace(0, 2 * np.pi, num=n_brute_force_δϕ, endpoint=False)
484
+ ]
485
+
486
+ # Remove certain modes, if requested
487
+ if include_modes is not None:
488
+ for L in range(ell_min, ell_max + 1):
489
+ for M in range(-L, L + 1):
490
+ if not (L, M) in include_modes:
491
+ wa.data[:, wa.index(L, M)] *= 0
492
+ wb.data[:, wb.index(L, M)] *= 0
493
+
494
+ # Define the cost function
495
+ modes_A = CubicSpline(wa.t, wa[:, wa.index(ell_min, -ell_min) : wa.index(ell_max, ell_max)+1].data)
496
+ modes_B = CubicSpline(wb.t, wb[:, wb.index(ell_min, -ell_min) : wb.index(ell_max, ell_max)+1].data)(t_reference)
497
+
498
+ normalization = trapezoid(
499
+ CubicSpline(
500
+ wb.t,
501
+ wb[:, wb.index(ell_min, -ell_min) : wb.index(ell_max, ell_max)+1].norm ** 2
502
+ )(t_reference),
503
+ t_reference,
504
+ )
505
+
506
+ # Optimize by brute force with multiprocessing
507
+ cost_wrapper = partial(_cost4d, args=[modes_A, modes_B, t_reference, normalization])
508
+
509
+ if nprocs != -1:
510
+ if nprocs is None:
511
+ nprocs = mp.cpu_count()
512
+ pool = mp.Pool(processes=nprocs)
513
+ cost_brute_force = pool.map(cost_wrapper, δt_δso3_brute_force)
514
+ pool.close()
515
+ pool.join()
516
+ else:
517
+ cost_brute_force = [
518
+ cost_wrapper(δt_δso3_brute_force_item)
519
+ for δt_δso3_brute_force_item in δt_δso3_brute_force
520
+ ]
521
+
522
+ δt_δso3 = δt_δso3_brute_force[np.argmin(cost_brute_force)]
523
+
524
+ # Optimize explicitly
525
+ optimum = least_squares(
526
+ cost_wrapper,
527
+ δt_δso3,
528
+ bounds=[(δt_lower, -np.pi/2, -np.pi/2, -np.pi/2), (δt_upper, np.pi/2, np.pi/2, np.pi/2)],
529
+ max_nfev=50000,
530
+ )
531
+ δt = optimum.x[0]
532
+ δSO3 = np.exp(quaternionic.array.from_vector_part(optimum.x[1:]))
533
+
534
+ wa_prime = WaveformModes(
535
+ input_array=(wa_orig[:, wa_orig.index(ell_min, -ell_min) : wa_orig.index(ell_max, ell_max)+1].data),
536
+ time=wa_orig.t - δt,
537
+ time_axis=0,
538
+ modes_axis=1,
539
+ ell_min=ell_min,
540
+ ell_max=ell_max,
541
+ spin_weight=spin_weight,
542
+ )
543
+ wa_prime = wa_prime.rotate(δSO3)
544
+
545
+ return optimum.cost, wa_prime, optimum
@@ -1 +0,0 @@
1
- __version__ = "2024.0.26"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes