sxs 2025.0.1__tar.gz → 2025.0.3__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 (133) hide show
  1. {sxs-2025.0.1 → sxs-2025.0.3}/.github/workflows/build.yml +2 -2
  2. {sxs-2025.0.1 → sxs-2025.0.3}/CITATION.cff +2 -2
  3. {sxs-2025.0.1 → sxs-2025.0.3}/PKG-INFO +3 -2
  4. {sxs-2025.0.1 → sxs-2025.0.3}/pyproject.toml +4 -2
  5. sxs-2025.0.3/sxs/__version__.py +1 -0
  6. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/handlers.py +24 -3
  7. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/simulations/__init__.py +1 -0
  8. sxs-2025.0.3/sxs/simulations/analyze.py +276 -0
  9. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/simulations/simulation.py +95 -21
  10. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/__init__.py +1 -1
  11. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/alignment.py +379 -55
  12. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/lvc.py +9 -5
  13. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/rotating_paired_diff_multishuffle_bzip2.py +6 -0
  14. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/memory.py +11 -41
  15. sxs-2025.0.3/sxs/waveforms/norms.py +270 -0
  16. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/waveform_modes.py +84 -0
  17. sxs-2025.0.3/sxs/waveforms/waveform_mts.py +30 -0
  18. {sxs-2025.0.1 → sxs-2025.0.3}/tests/conftest.py +31 -1
  19. sxs-2025.0.3/tests/test_alignment.py +93 -0
  20. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_horizons.py +5 -11
  21. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_simulation.py +12 -0
  22. sxs-2025.0.1/sxs/__version__.py +0 -1
  23. {sxs-2025.0.1 → sxs-2025.0.3}/.codecov.yml +0 -0
  24. {sxs-2025.0.1 → sxs-2025.0.3}/.github/dependabot.yml +0 -0
  25. {sxs-2025.0.1 → sxs-2025.0.3}/.github/scripts/parse_bump_rule.py +0 -0
  26. {sxs-2025.0.1 → sxs-2025.0.3}/.github/workflows/pr_rtd_link.yml +0 -0
  27. {sxs-2025.0.1 → sxs-2025.0.3}/.gitignore +0 -0
  28. {sxs-2025.0.1 → sxs-2025.0.3}/.readthedocs.yaml +0 -0
  29. {sxs-2025.0.1 → sxs-2025.0.3}/LICENSE +0 -0
  30. {sxs-2025.0.1 → sxs-2025.0.3}/README.md +0 -0
  31. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/catalog.md +0 -0
  32. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/horizons.md +0 -0
  33. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/load.md +0 -0
  34. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/metadata.md +0 -0
  35. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/simulation.md +0 -0
  36. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/simulations.md +0 -0
  37. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/time_series.md +0 -0
  38. {sxs-2025.0.1 → sxs-2025.0.3}/docs/api/waveforms.md +0 -0
  39. {sxs-2025.0.1 → sxs-2025.0.3}/docs/html/main.html +0 -0
  40. {sxs-2025.0.1 → sxs-2025.0.3}/docs/images/favicon.ico +0 -0
  41. {sxs-2025.0.1 → sxs-2025.0.3}/docs/index.md +0 -0
  42. {sxs-2025.0.1 → sxs-2025.0.3}/docs/javascript/mathjax.js +0 -0
  43. {sxs-2025.0.1 → sxs-2025.0.3}/docs/julia.md +0 -0
  44. {sxs-2025.0.1 → sxs-2025.0.3}/docs/mathematica.md +0 -0
  45. {sxs-2025.0.1 → sxs-2025.0.3}/docs/stylesheets/extra.css +0 -0
  46. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/00-Introduction.ipynb +0 -0
  47. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/01-Simulations_and_Metadata.ipynb +0 -0
  48. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/02-Simulation.ipynb +0 -0
  49. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/03-Horizons.ipynb +0 -0
  50. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/04-Waveforms.ipynb +0 -0
  51. {sxs-2025.0.1 → sxs-2025.0.3}/docs/tutorials/05-PreprocessingForFFTs.ipynb +0 -0
  52. {sxs-2025.0.1 → sxs-2025.0.3}/mkdocs.yml +0 -0
  53. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/__init__.py +0 -0
  54. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/catalog/__init__.py +0 -0
  55. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/catalog/catalog.py +0 -0
  56. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/catalog/create.py +0 -0
  57. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/catalog/description.py +0 -0
  58. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/horizons/__init__.py +0 -0
  59. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/horizons/spec_horizons_h5.py +0 -0
  60. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/horizons/xor_multishuffle_bzip2.py +0 -0
  61. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/julia/GWFrames.py +0 -0
  62. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/julia/__init__.py +0 -0
  63. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/juliapkg.json +0 -0
  64. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/metadata/__init__.py +0 -0
  65. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/metadata/metadata.py +0 -0
  66. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/metadata/metric.py +0 -0
  67. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/simulations/local.py +0 -0
  68. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/simulations/simulations.py +0 -0
  69. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/time_series.py +0 -0
  70. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/__init__.py +0 -0
  71. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/bitwise.py +0 -0
  72. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/decimation/__init__.py +0 -0
  73. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/decimation/greedy_spline.py +0 -0
  74. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/decimation/linear_bisection.py +0 -0
  75. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/decimation/peak_greed.py +0 -0
  76. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/decimation/suppression.py +0 -0
  77. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/dicts.py +0 -0
  78. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/downloads.py +0 -0
  79. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/files.py +0 -0
  80. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/formats.py +0 -0
  81. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/inspire.py +0 -0
  82. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/__init__.py +0 -0
  83. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/comparisons.py +0 -0
  84. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/conversion.py +0 -0
  85. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/dataset.py +0 -0
  86. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/horizons.py +0 -0
  87. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/metadata.py +0 -0
  88. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/waveform_amp_phase.py +0 -0
  89. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/lvcnr/waveforms.py +0 -0
  90. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/monotonicity.py +0 -0
  91. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/pretty_print.py +0 -0
  92. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/__init__.py +0 -0
  93. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/ads.py +0 -0
  94. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/arxiv.py +0 -0
  95. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/fairchild_report.py +0 -0
  96. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/inspire.py +0 -0
  97. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/journal_abbreviations.py +0 -0
  98. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/references/references.py +0 -0
  99. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/select.py +0 -0
  100. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/smooth_functions.py +0 -0
  101. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/string_converters.py +0 -0
  102. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/sxs_directories.py +0 -0
  103. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/sxs_identifiers.py +0 -0
  104. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/utilities/url.py +0 -0
  105. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/__init__.py +0 -0
  106. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/grathena.py +0 -0
  107. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/nrar.py +0 -0
  108. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/rotating_paired_xor_multishuffle_bzip2.py +0 -0
  109. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/format_handlers/spectre_cce_v1.py +0 -0
  110. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/mode_utilities.py +0 -0
  111. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/transformations.py +0 -0
  112. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/waveform_grid.py +0 -0
  113. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/waveform_mixin.py +0 -0
  114. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/waveforms/waveform_signal.py +0 -0
  115. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/__init__.py +0 -0
  116. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/api/__init__.py +0 -0
  117. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/api/deposit.py +0 -0
  118. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/api/login.py +0 -0
  119. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/api/records.py +0 -0
  120. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/catalog.py +0 -0
  121. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/creators.py +0 -0
  122. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/simannex.py +0 -0
  123. {sxs-2025.0.1 → sxs-2025.0.3}/sxs/zenodo/surrogatemodeling.py +0 -0
  124. {sxs-2025.0.1 → sxs-2025.0.3}/tests/__init__.py +0 -0
  125. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_catalog.py +0 -0
  126. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_julia.py +0 -0
  127. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_loader.py +0 -0
  128. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_metadata.py +0 -0
  129. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_time_series.py +0 -0
  130. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_transformations.py +0 -0
  131. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_utilities.py +0 -0
  132. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_waveform_rotations.py +0 -0
  133. {sxs-2025.0.1 → sxs-2025.0.3}/tests/test_waveforms.py +0 -0
@@ -47,7 +47,7 @@ jobs:
47
47
  if: ${{ env.skipping_build_and_test_replicate != 'true' }}
48
48
  uses: actions/cache@v4
49
49
  with:
50
- key: sxs-${{ runner.os }}-1 # Increment this number whenever the cached files should change
50
+ key: sxs-${{ runner.os }}-2 # Increment this number whenever the cached files should change
51
51
  path: |
52
52
  ${{ runner.os == 'Linux' && '/home/runner/.cache/sxs' || runner.os == 'Windows' && 'C:\Users\runneradmin\.sxs\cache' || '/Users/runner/.sxs/cache' }}
53
53
 
@@ -79,7 +79,7 @@ jobs:
79
79
  if: ${{ env.skipping_build_and_test_replicate != 'true' }}
80
80
  shell: bash
81
81
  run: |
82
- hatch run test
82
+ hatch run test --durations=0
83
83
 
84
84
  - name: Upload coverage
85
85
  if: "matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest'"
@@ -10,5 +10,5 @@ authors:
10
10
  title: "The sxs package"
11
11
  license: MIT
12
12
  doi: 10.5281/zenodo.4034006
13
- version: 2025.0.1
14
- date-released: 2025-03-19
13
+ version: 2025.0.3
14
+ date-released: 2025-04-10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sxs
3
- Version: 2025.0.1
3
+ Version: 2025.0.3
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/
@@ -40,13 +40,14 @@ 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
42
  Requires-Dist: numpy>=1.25
43
+ Requires-Dist: packaging
43
44
  Requires-Dist: pandas>=1.1.2
44
45
  Requires-Dist: pytz>=2020.1
45
46
  Requires-Dist: quaternionic>=1.0.15
46
47
  Requires-Dist: requests>=2.24.0
47
48
  Requires-Dist: scipy>=1.13
48
49
  Requires-Dist: spherical>=1.0.15
49
- Requires-Dist: sxscatalog>=3.0.0a1
50
+ Requires-Dist: sxscatalog>=3.0.0a26
50
51
  Requires-Dist: tqdm>=4.63.1
51
52
  Requires-Dist: urllib3>=1.25.10
52
53
  Provides-Extra: docs
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Topic :: Scientific/Engineering :: Astronomy"
19
19
  ]
20
20
  dependencies = [
21
- "sxscatalog >=3.0.0a1",
21
+ "sxscatalog >=3.0.0a26",
22
22
  "numpy >=1.25",
23
23
  "scipy >=1.13",
24
24
  "numba >=0.55; implementation_name == 'cpython'",
@@ -31,7 +31,8 @@ dependencies = [
31
31
  "pytz >=2020.1",
32
32
  "urllib3 >=1.25.10",
33
33
  "pandas >=1.1.2",
34
- "juliacall >=0.9.20"
34
+ "juliacall >=0.9.20",
35
+ "packaging",
35
36
  ]
36
37
 
37
38
  [project.optional-dependencies]
@@ -73,6 +74,7 @@ path = "sxs/__version__.py"
73
74
 
74
75
  [tool.hatch.envs.default]
75
76
  dependencies = [
77
+ "ipython",
76
78
  "pytest",
77
79
  "pytest-cov",
78
80
  "pytest-forked >=1.3.0",
@@ -0,0 +1 @@
1
+ __version__ = "2025.0.3"
@@ -237,7 +237,7 @@ def load(location, download=None, cache=None, progress=None, truepath=None, **kw
237
237
  import pathlib
238
238
  import urllib.request
239
239
  from . import Simulations, Simulation, read_config, sxs_directory, Catalog
240
- from .utilities import url, download_file, sxs_path_to_system_path, sxs_id_version_lev_exact_re, lev_path_re
240
+ from .utilities import url, download_file, sxs_path_to_system_path, sxs_id_version_lev_exact_re, lev_path_re, sxs_identifier_re
241
241
 
242
242
  # Note: `download` and/or `cache` may still be `None` after this
243
243
  if download is None:
@@ -294,11 +294,30 @@ def load(location, download=None, cache=None, progress=None, truepath=None, **kw
294
294
  local=kwargs.get("local", False),
295
295
  annex_dir=kwargs.get("annex_dir", None)
296
296
  )
297
+ # If we chop off any "/LevN", and it's in the simulations, load it as a simulation
297
298
  if lev_path_re.sub("", location) in simulations:
298
299
  return Simulation(
299
300
  location, download=download, cache=cache, progress=progress, **kwargs
300
301
  )
301
302
 
303
+ # Now we look for a file in `simulations`
304
+ if sxs_identifier_re.match(location):
305
+ split_location = sxs_identifier_re.split(location)
306
+ # Currently we can only handle unversioned SXS ID files
307
+ if split_location[5] is None:
308
+ sxs_id = split_location[1]
309
+ file = split_location[-1].lstrip("/").lstrip("\\")
310
+ if sxs_id in simulations:
311
+ simulation = simulations[sxs_id]
312
+ if "files" in simulation:
313
+ file_info = simulation["files"][file]
314
+ location = file_info["link"]
315
+ truepath = truepath or (pathlib.Path(sxs_id) / file)
316
+ return load(
317
+ location, truepath=truepath,
318
+ download=download, cache=cache, progress=progress, **kwargs
319
+ )
320
+
302
321
  # Try to find an appropriate SXS file in the catalog
303
322
  catalog = Catalog.load(download=download)
304
323
  selections = catalog.select_files(location)
@@ -331,7 +350,9 @@ def load(location, download=None, cache=None, progress=None, truepath=None, **kw
331
350
  return loaded
332
351
 
333
352
 
334
- def load_via_sxs_id(sxsid, location, *, download=None, cache=None, progress=None, truepath=None, **kwargs):
353
+ def load_via_sxs_id(
354
+ sxsid, location, *, download=None, cache=None, progress=None, truepath=None, timeout=30, **kwargs
355
+ ):
335
356
  """Load a path via a (possibly versioned) SXS ID
336
357
 
337
358
  Given some SXS ID like "SXS:BBH:1234" or a versioned ID like
@@ -354,7 +375,7 @@ def load_via_sxs_id(sxsid, location, *, download=None, cache=None, progress=None
354
375
  import requests
355
376
  from .utilities import sxs_path_to_system_path
356
377
  url = f"{doi_url}{sxsid}"
357
- response = requests.head(url, allow_redirects=True)
378
+ response = requests.head(url, allow_redirects=True, timeout=timeout)
358
379
  if response.status_code != 200:
359
380
  raise ValueError(f"Could not load via DOI {url=}")
360
381
  final_url = f"{response.url}/{location}"
@@ -1,3 +1,4 @@
1
1
  from .simulation import Simulation
2
2
  from .simulations import Simulations
3
3
  from .local import write_local_simulations, local_simulations
4
+ from . import analyze
@@ -0,0 +1,276 @@
1
+ import multiprocessing
2
+
3
+ from ..waveforms.norms import create_unified_waveforms, L2_difference, mismatch
4
+ from ..waveforms.alignment import align_waveforms, align_simulations
5
+ from ..handlers import load
6
+
7
+ import numpy as np
8
+
9
+
10
+ def compute_error_summary(wa, wb, t1, t2, modes=None, ASDs=None, total_masses=None):
11
+ """
12
+ Compute various errors between two waveforms.
13
+
14
+ This computes the time-domain mismatch over the two-sphere,
15
+ the normalized L² norm of the residual, the frequency-domain mismatch
16
+ against whatever detector ASDs are provided with some total mass,
17
+ and the individual modes' residual norms and norms.
18
+
19
+ Parameters
20
+ ----------
21
+ wa : WaveformModes
22
+ wb : WaveformModes
23
+ t1 : float
24
+ Beginning of integrals.
25
+ t2 : float
26
+ End of integrals.
27
+ modes : list, optional
28
+ Modes (ell, m) to include in error calculations.
29
+ Default is all modes.
30
+ ASDs : dict of funcs, optional
31
+ Dictionary of functions mapping frequencies to the ASD of a detector(s).
32
+ Default is no frequency-domain mismatch is calculated.
33
+ total_masses : list of floats, optional
34
+ Total masses in solar masses to use for frequency-domain mismatches.
35
+ Default is 1.
36
+
37
+ Returns
38
+ -------
39
+ errors : dict
40
+ Dictionary of the time-domain mismatch over the two-sphere,
41
+ the normalized L² norm of the residual, the frequency-domain mismatch
42
+ against whatever detector ASDs are provided with some total mass,
43
+ and the modes' absolute errors and norms.
44
+ """
45
+ from .. import m_sun_in_seconds
46
+
47
+ errors = {}
48
+
49
+ errors["t1"] = t1
50
+ errors["t2"] = t2
51
+
52
+ wa, wb = create_unified_waveforms(wa, wb, t1, t2, padding_time_factor=0)
53
+
54
+ errors["mismatch"] = mismatch(wa, wb, t1, t2, modes=modes)
55
+
56
+ errors["residual L2 norm"] = L2_difference(wa, wb, t1, t2, modes=modes)
57
+
58
+ i1 = wa.index_closest_to(t1)
59
+ i0, i2 = max(0, i1-5), min(i1+6, wa.n_times-1)
60
+ Ω1 = np.linalg.norm(wa[i0:i2].angular_velocity, axis=1)[5]
61
+ f1 = 2*Ω1 / (2*np.pi)
62
+
63
+ wa = wa.preprocess(t1, t1 + 0.01 * (t2 - t1), t2 - (t2 - t1) * 0.01, t2)
64
+ wb = wb.preprocess(t1, t1 + 0.01 * (t2 - t1), t2 - (t2 - t1) * 0.01, t2)
65
+
66
+ wa_tilde = wa.fourier_transform()
67
+ wb_tilde = wb.fourier_transform()
68
+ if ASDs is not None:
69
+ if total_masses is None:
70
+ raise ValueError("Need to specify total masses if ASDs are provided.")
71
+ for ASD in ASDs:
72
+ for total_mass in total_masses:
73
+ # Note that we only need to make the frequency unitful, since the
74
+ # magnitude of the strain scales out in the mismatch.
75
+ # We make things unitful here because the fourier transform
76
+ # earlier was called outside of the for loop without a total mass
77
+ # so that it doesn't need to be computed at each iteration.
78
+ frequency_factor = 1 / (total_mass * m_sun_in_seconds)
79
+
80
+ wa_tilde_total_mass = wa_tilde.copy()
81
+ wb_tilde_total_mass = wb_tilde.copy()
82
+ wa_tilde_total_mass.t = wa_tilde_total_mass.t * frequency_factor
83
+ wb_tilde_total_mass.t = wb_tilde_total_mass.t * frequency_factor
84
+
85
+ errors[f"mismatch {ASD} {total_mass}"] = mismatch(
86
+ wa_tilde_total_mass, wb_tilde_total_mass, f1 * frequency_factor, modes=modes, ASD=ASDs[ASD]
87
+ )
88
+
89
+ ell_min = max(wa.ell_min, wb.ell_min)
90
+ ell_max = min(wa.ell_max, wb.ell_max)
91
+ for L in range(ell_min, ell_max + 1):
92
+ for M in range(-L, L + 1):
93
+ absolute_error, norm = L2_difference(
94
+ wa, wb, t1, t2, modes=[(L, M)], modes_for_norm=[(L, M)], normalize=False
95
+ )
96
+ errors[f"(L, M) = {(L, M)} residual L2 norm"] = absolute_error
97
+ errors[f"(L, M) = {(L, M)} L2 norm"] = norm
98
+
99
+ return errors
100
+
101
+
102
+ def analyze_simulation(
103
+ sim_name,
104
+ analyze_levs=True,
105
+ analyze_extrapolation=True,
106
+ analyze_psi4=True,
107
+ ASDs=None,
108
+ total_masses=None,
109
+ nprocs=None,
110
+ ):
111
+ """
112
+ Analyze a simulation's waveform agreement across
113
+ Levs, extrapolation orders, and the psi4 constraint.
114
+
115
+ For each Lev in a simulation, align the strain waveforms
116
+ (if it's the highest Lev pair, then a 4d optimization is also performed
117
+ and the transformation from the low to high lev is included in the returned dictionary)
118
+ via the independent alignment method. Then, compute the L² norm of the residual between
119
+ the two aligned waveforms (if it's the highest Lev pair, then the
120
+ mismatch against the ASDs and total masses is also computed, as well as the
121
+ individual modes' residual norms and norms). For the
122
+ extrapolation order and psi4 analyses, only the L² norm of the residuals are computed.
123
+
124
+ Parameters
125
+ ----------
126
+ sim_name : str
127
+ Simulation name, e.g., "SXS:BBH:2092".
128
+ analyze_levs : bool, optional
129
+ Whether or not to analyze the various Levs.
130
+ Default is True.
131
+ analyze_extrapolation : bool, optional
132
+ Whether or not to analyze the various extrapolation orders.
133
+ Default is True.
134
+ analyze_psi4 : bool, optional
135
+ Whether or not to analyze the psi4 constraint -h.ddot = psi4..
136
+ Default is True.
137
+ ASDs : dict of funcs, optional
138
+ Dictionary of functions mapping frequencies to the ASD of a detector(s).
139
+ Default is no frequency-domain mismatch is calculated.
140
+ total_masses : list of floats, optional
141
+ Total masses in solar masses to use for frequency-domain mismatches.
142
+ Default is 1.
143
+ nprocs : int, optional
144
+ Number of cpus to use. Default is maximum number. If -1 is provided,
145
+ then no multiprocessing is performed.
146
+
147
+ Returns
148
+ -------
149
+ errors : dict
150
+ Dictionary of the errors described above.
151
+ """
152
+ errors = {}
153
+
154
+ sim = load(sim_name)
155
+
156
+ # Lev analysis
157
+ if analyze_levs:
158
+ for i, lev in enumerate(sim.lev_numbers[1:][::-1]):
159
+ sim_low_lev = load(f"{sim_name}/Lev{lev - 1}")
160
+ sim_high_lev = load(f"{sim_name}/Lev{lev}")
161
+
162
+ w_high_lev = sim_high_lev.h
163
+ if i == 0:
164
+ w_low_lev_prime, transformation, L2_norm, t1, t2 = align_simulations(
165
+ sim_low_lev, sim_high_lev, alignment_method="4d", nprocs=nprocs
166
+ )
167
+ errors[f"(Lev{lev - 1}, Lev{lev}) 4d"] = compute_error_summary(w_low_lev_prime, w_high_lev, t1, t2)
168
+ errors[f"(Lev{lev - 1}, Lev{lev}) 4d transformation"] = transformation
169
+
170
+ w_low_lev_prime, transformation, _, t1, t2 = align_simulations(
171
+ sim_low_lev, sim_high_lev, alignment_method="independent alignment", nprocs=nprocs
172
+ )
173
+ errors[f"(Lev{lev - 1}, Lev{lev})"] = L2_difference(w_high_lev, w_low_lev_prime, t1, t2)
174
+
175
+ # Extrapolation order analysis
176
+ if analyze_extrapolation:
177
+ for extrapolation in ["N3", "N4", "Outer"]:
178
+ other = load(sim_name, extrapolation=extrapolation)
179
+
180
+ w_n2 = sim.h
181
+ w_other = other.h
182
+
183
+ t1 = sim.metadata.relaxation_time
184
+
185
+ w_other_prime, transformation, _, t1, t2 = align_waveforms(
186
+ w_other, w_n2, t1, alignment_method="independent alignment", nprocs=nprocs
187
+ )
188
+
189
+ L2_norm = L2_difference(w_n2, w_other_prime, t1, t2)
190
+
191
+ errors[f"(N2, {extrapolation})"] = L2_norm
192
+
193
+ # Psi4 analysis
194
+ if analyze_psi4:
195
+ h_as_psi4 = -sim.h.ddot
196
+ psi4 = sim.psi4
197
+
198
+ t1 = sim.metadata.relaxation_time
199
+ t2 = h_as_psi4.t[-1]
200
+
201
+ L2_norm = L2_difference(h_as_psi4, psi4, t1, t2)
202
+
203
+ errors[f"(-h.ddot, psi4)"] = L2_norm
204
+
205
+ return errors
206
+
207
+
208
+ def analyze_simulations(
209
+ sim_names,
210
+ analyze_levs=True,
211
+ analyze_extrapolation=True,
212
+ analyze_psi4=True,
213
+ ASDs=None,
214
+ total_masses=None,
215
+ nprocs=None,
216
+ ):
217
+ """
218
+ Analyze simulations' waveform agreement across
219
+ Levs, extrapolation orders, and the psi4 constraint.
220
+
221
+ For each Lev in a simulation, align the strain waveforms
222
+ (if it's the highest Lev pair, then a 4d optimization is also performed
223
+ and the transformation from the low to high lev is included in the returned dictionary)
224
+ via the independent alignment method. Then, compute mismatches between
225
+ the two aligned waveforms (if it's the highest Lev pair, then the
226
+ relative L² norm error over the two-sphere is computed, as well as the
227
+ individual mode absolute errors and norms). For the
228
+ extrapolation order and psi4 analyses, only mismatches are computed.
229
+
230
+ Parameters
231
+ ----------
232
+ sim_names : list of strs
233
+ Simulation names, e.g., ["SXS:BBH:2092"].
234
+ analyze_levs : bool, optional
235
+ Whether or not to analyze the various Levs.
236
+ Default is True.
237
+ analyze_extrapolation : bool, optional
238
+ Whether or not to analyze the various extrapolation orders.
239
+ Default is True.
240
+ analyze_psi4 : bool, optional
241
+ Whether or not to analyze the psi4 constraint -h.ddot = psi4..
242
+ Default is True.
243
+ ASDs : dict of funcs, optional
244
+ Dictionary of functions mapping frequencies to the ASD of a detector(s).
245
+ Default is no frequency-domain mismatch is calculated.
246
+ total_masses : list of floats, optional
247
+ Total masses in solar masses to use for frequency-domain mismatches.
248
+ Default is 1.
249
+ nprocs : int, optional
250
+ Number of cpus to use. Default is maximum number. If -1 is provided,
251
+ then no multiprocessing is performed.
252
+
253
+ Returns
254
+ -------
255
+ errors : dict
256
+ Dictionary of the errors described above.
257
+ """
258
+ errors = {}
259
+ if nprocs != -1:
260
+ with multiprocessing.Pool(processes=nprocs) as pool:
261
+ results = pool.starmap(
262
+ analyze_simulation,
263
+ [
264
+ (sim_name, analyze_levs, analyze_extrapolation, analyze_psi4, ASDs, total_masses, -1)
265
+ for sim_name in sim_names
266
+ ],
267
+ )
268
+ for i, sim_name in enumerate(sim_names):
269
+ errors[sim_name] = results[i]
270
+ else:
271
+ for sim_name in sim_names:
272
+ errors[sim_name] = analyze_simulation(
273
+ sim_name, analyze_levs, analyze_extrapolation, analyze_psi4, ASDs, total_masses, -1
274
+ )
275
+
276
+ return errors
@@ -100,18 +100,22 @@ def Simulation(location, *args, **kwargs):
100
100
  arguments other than those listed above.
101
101
 
102
102
  """
103
+ import numpy as np
104
+ from packaging.version import Version
103
105
  from .. import load, sxs_directory
104
106
  from ..metadata.metric import MetadataMetric
105
107
 
106
108
  # Load the simulation catalog
107
109
  simulations = load("simulations")
110
+ v = Version(simulations.tag)
111
+ latest_version = f"v{v.major}.{v.minor}"
108
112
 
109
113
  # Extract the simulation ID, version, and Lev from the location string
110
114
  simulation_id, input_version = sxs_id_and_version(location)
111
115
  if not simulation_id:
112
116
  if location.split("/Lev")[0] in simulations:
113
117
  simulation_id = location.split("/Lev")[0]
114
- input_version = "v0.0"
118
+ input_version = latest_version
115
119
  else:
116
120
  raise ValueError(f"Invalid SXS ID in '{simulation_id}'")
117
121
  input_lev_number = lev_number(location) # Will be `None` if not present
@@ -132,8 +136,8 @@ def Simulation(location, *args, **kwargs):
132
136
 
133
137
  # Check if the specified version exists in the simulation catalog
134
138
  if not hasattr(metadata, "DOI_versions"):
135
- input_version = "v0.0" # A fake version, to signal this sim doesn't know about DOIs
136
- if input_version != "v0.0" and input_version not in metadata.DOI_versions:
139
+ input_version = latest_version
140
+ if input_version != latest_version and input_version not in metadata.DOI_versions:
137
141
  raise ValueError(f"Version '{input_version}' not found in simulation catalog for '{simulation_id}'")
138
142
 
139
143
  # Set various pieces of information about the simulation
@@ -206,17 +210,31 @@ def Simulation(location, *args, **kwargs):
206
210
  # Note the deprecation status in the kwargs, even if ignoring deprecation
207
211
  kwargs["deprecated"] = deprecated
208
212
 
209
- # TODO: Default to not downloading file info
210
- # TODO: In that case, deal with Lev numbers somehow
211
-
212
213
  # We want to do this *after* deprecation checking, to avoid possibly unnecessary web requests
214
+ if 1 <= float(version[1:]) < 3.0 and "files" in metadata:
215
+ # The simulation metadata is points to files with a different version
216
+ del metadata["files"]
213
217
  files = get_file_info(metadata, sxs_id, download=kwargs.get("download_file_info", None))
214
218
 
215
219
  # If Lev is given as part of `location`, use it; otherwise, use the highest available
216
220
  lev_numbers = sorted({lev for f in files if (lev:=lev_number(f))})
217
- output_lev_number = input_lev_number or max(lev_numbers)
221
+ if input_lev_number is not None and lev_numbers:
222
+ if input_lev_number not in lev_numbers:
223
+ raise ValueError(
224
+ f"Lev number '{input_lev_number}' not found in simulation files for {sxs_id}"
225
+ )
226
+ max_lev_number = max(lev_numbers, default=np.nan)
227
+ output_lev_number = input_lev_number or max_lev_number
218
228
  location = f"{sxs_id_stem}{version}/Lev{output_lev_number}"
219
229
 
230
+ # Keep the metadata around unless we're asking for an old version
231
+ # or a less-than-maximal Lev
232
+ if (
233
+ version != latest_version
234
+ or (lev_numbers and output_lev_number != max_lev_number)
235
+ ):
236
+ metadata = None
237
+
220
238
  # Finally, figure out which version of the simulation to load and dispatch
221
239
  version_number = float(version[1:])
222
240
  if 1 <= version_number < 2.0:
@@ -227,7 +245,7 @@ def Simulation(location, *args, **kwargs):
227
245
  sim = Simulation_v2(
228
246
  metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, output_lev_number, location, *args, **kwargs
229
247
  )
230
- elif 3 <= version_number < 4.0 or version == "v0.0":
248
+ elif 3 <= version_number < 4.0 or version == latest_version:
231
249
  sim = Simulation_v3(
232
250
  metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, output_lev_number, location, *args, **kwargs
233
251
  )
@@ -245,8 +263,9 @@ class SimulationBase:
245
263
 
246
264
  Attributes
247
265
  ----------
248
- metadata : Metadata
249
- Metadata object for the simulation
266
+ metadata : Metadata or None
267
+ Metadata object for the simulation. If `None`, the metadata
268
+ will be loaded automatically.
250
269
  series : pandas.Series
251
270
  The metadata, as extracted from the `simulations.dataframe`,
252
271
  meaning that it has columns consistent with other simulations,
@@ -307,7 +326,6 @@ class SimulationBase:
307
326
  metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, lev_number, location,
308
327
  *args, **kwargs
309
328
  ):
310
- self.metadata = metadata
311
329
  self.series = series
312
330
  self.version = version
313
331
  self.sxs_id_stem = sxs_id_stem
@@ -318,6 +336,7 @@ class SimulationBase:
318
336
  self.lev_number = lev_number
319
337
  self.location = location
320
338
  self.deprecated = kwargs.get("deprecated", False)
339
+ self.metadata = metadata or self.load_metadata()
321
340
 
322
341
  def __repr__(self):
323
342
  chi1 = self.series["reference_dimensionless_spin1"]
@@ -400,7 +419,7 @@ class SimulationBase:
400
419
  warning_threshold : float, optional
401
420
  Threshold distance above which a warning will be issued
402
421
  that the closest simulation is fairly distant. Default is
403
- 1e-3.
422
+ 1e-2.
404
423
 
405
424
  Returns
406
425
  -------
@@ -430,12 +449,34 @@ class SimulationBase:
430
449
 
431
450
  @property
432
451
  def lev(self):
433
- return f"Lev{self.lev_number}"
452
+ if self.lev_number is None:
453
+ return ""
454
+ else:
455
+ return f"Lev{self.lev_number}"
434
456
 
435
457
  @property
436
458
  def Lev(self):
437
459
  return self.lev
438
460
 
461
+ @property
462
+ def metadata_path(self):
463
+ for separator in [":", "/"]:
464
+ for ending in [".json", ".txt"]:
465
+ prefix = f"{self.lev}{separator}" if self.lev else ""
466
+ if (fn := f"{prefix}metadata{ending}") in self.files:
467
+ return fn
468
+ raise ValueError(
469
+ f"Metadata file not found in simulation files for {self.location}"
470
+ )
471
+
472
+ def load_metadata(self):
473
+ from .. import load
474
+ metadata_path = self.metadata_path
475
+ metadata_location = self.files.get(metadata_path)["link"]
476
+ sxs_id_path = Path(self.sxs_id)
477
+ metadata_truepath = Path(sxs_path_to_system_path(sxs_id_path / metadata_path))
478
+ return Metadata(load(metadata_location, truepath=metadata_truepath))
479
+
439
480
  def load_horizons(self):
440
481
  from .. import load
441
482
  sxs_id_path = Path(self.sxs_id)
@@ -717,7 +758,8 @@ class Simulation_v1(SimulationBase):
717
758
 
718
759
  @property
719
760
  def horizons_path(self):
720
- return f"{self.lev}/Horizons.h5"
761
+ prefix = f"{self.lev}/" if self.lev else ""
762
+ return f"{prefix}Horizons.h5"
721
763
 
722
764
  def load_horizons(self):
723
765
  from .. import load
@@ -739,25 +781,27 @@ class Simulation_v1(SimulationBase):
739
781
 
740
782
  @property
741
783
  def strain_path(self):
784
+ prefix = f"{self.lev}/" if self.lev else ""
742
785
  extrapolation = (
743
786
  f"Extrapolated_{self.extrapolation}.dir"
744
787
  if self.extrapolation != "Outer"
745
788
  else "OutermostExtraction.dir"
746
789
  )
747
790
  return (
748
- f"{self.lev}/rhOverM_Asymptotic_GeometricUnits_CoM.h5",
791
+ f"{prefix}rhOverM_Asymptotic_GeometricUnits_CoM.h5",
749
792
  extrapolation
750
793
  )
751
794
 
752
795
  @property
753
796
  def psi4_path(self):
797
+ prefix = f"{self.lev}/" if self.lev else ""
754
798
  extrapolation = (
755
799
  f"Extrapolated_{self.extrapolation}.dir"
756
800
  if self.extrapolation != "Outer"
757
801
  else "OutermostExtraction.dir"
758
802
  )
759
803
  return (
760
- f"{self.lev}/rMPsi4_Asymptotic_GeometricUnits_CoM.h5",
804
+ f"{prefix}rMPsi4_Asymptotic_GeometricUnits_CoM.h5",
761
805
  extrapolation
762
806
  )
763
807
 
@@ -795,18 +839,23 @@ class Simulation_v2(SimulationBase):
795
839
  also `SimulationBase` for the base class that this class inherits
796
840
  from.
797
841
  """
842
+ # Default extrapolation order for this simulation version
843
+ default_extrapolation = "N2"
844
+
798
845
  def __init__(self, *args, **kwargs):
799
846
  super().__init__(*args, **kwargs)
800
- self.extrapolation = kwargs.get("extrapolation", "N2")
847
+ self.extrapolation = kwargs.get("extrapolation", self.default_extrapolation)
801
848
 
802
849
  @property
803
850
  def horizons_path(self):
804
- return f"{self.lev}:Horizons.h5"
851
+ prefix = f"{self.lev}:" if self.lev else ""
852
+ return f"{prefix}Horizons.h5"
805
853
 
806
854
  @property
807
855
  def strain_path(self):
856
+ prefix = f"{self.lev}:" if self.lev else ""
808
857
  return (
809
- f"{self.lev}:Strain_{self.extrapolation}",
858
+ f"{prefix}Strain_{self.extrapolation}",
810
859
  "/"
811
860
  )
812
861
 
@@ -817,8 +866,9 @@ class Simulation_v2(SimulationBase):
817
866
  if self.extrapolation != "Outer"
818
867
  else "OutermostExtraction.dir"
819
868
  )
869
+ prefix = f"{self.lev}:" if self.lev else ""
820
870
  return (
821
- f"{self.lev}:ExtraWaveforms",
871
+ f"{prefix}ExtraWaveforms",
822
872
  f"/rMPsi4_Asymptotic_GeometricUnits_CoM_Mem/{extrapolation}"
823
873
  )
824
874
 
@@ -846,7 +896,31 @@ class Simulation_v2(SimulationBase):
846
896
 
847
897
 
848
898
  class Simulation_v3(Simulation_v2):
849
- pass
899
+ # Default extrapolation order for this simulation version
900
+ default_extrapolation = "N2"
901
+
902
+ def __init__(self, *args, **kwargs):
903
+ super().__init__(*args, **kwargs)
904
+ self.extrapolation = kwargs.get("extrapolation", self.default_extrapolation)
905
+
906
+ @property
907
+ def strain_path(self):
908
+ prefix = f"{self.lev}:" if self.lev else ""
909
+ return (
910
+ f"{prefix}Strain_{self.extrapolation}",
911
+ "/"
912
+ ) if self.extrapolation == self.default_extrapolation else (
913
+ f"{prefix}ExtraWaveforms",
914
+ f"/Strain_{self.extrapolation}.dir"
915
+ )
916
+
917
+ @property
918
+ def psi4_path(self):
919
+ prefix = f"{self.lev}:" if self.lev else ""
920
+ return (
921
+ f"{prefix}ExtraWaveforms",
922
+ f"/Psi4_{self.extrapolation}.dir"
923
+ )
850
924
 
851
925
 
852
926
  def get_file_info(metadata, sxs_id, download=None):