sxs 2023.3.2__py3-none-any.whl → 2024.0.1__py3-none-any.whl

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.
@@ -0,0 +1,521 @@
1
+ from pathlib import Path
2
+ from warnings import warn
3
+ from .. import doi_url, Metadata
4
+ from ..utilities import (
5
+ sxs_id_and_version, lev_number, sxs_path_to_system_path,
6
+ download_file, sxs_directory, read_config
7
+ )
8
+
9
+
10
+ def Simulation(location, *args, **kwargs):
11
+ """Construct a Simulation object from a location string
12
+
13
+ The location string should be an SXS ID with an optional version
14
+ number, and possibly a Lev specification, as in
15
+ "SXS:BBH:1234v2.0/Lev5". The default version number is "v2.0",
16
+ and the default Lev number is the highest available. Note that
17
+ the version number must be in the form "v2.0", not "2.0", and it
18
+ must exist, or no files will be found to load the data from.
19
+
20
+ The returned object will be either a `Simulation_v1` or
21
+ `Simulation_v2` object, depending on the version number.
22
+ Hopefully, most details of the two versions will be hidden from
23
+ the user, so that the interface is identical.
24
+
25
+ Note that some simulations are deprecated and/or superseded by
26
+ other simulations. By default, this function will raise an error
27
+ if you try to load a deprecated or superseded simulation. There
28
+ are several ways to override this behavior:
29
+
30
+ 1. Pass `ignore_deprecation=True` to completely bypass even
31
+ checking for deprecation or supersession. No warnings or
32
+ errors will be issued.
33
+ 2. Include an explicit version number in the `location`
34
+ string, as in "SXS:BBH:0001v2.0". A warning will be issued
35
+ that the simulation is deprecated, but it will be loaded
36
+ anyway.
37
+ 3. Pass `auto_supersede=True` to automatically load the
38
+ superseding simulation, if there is only one. Because no
39
+ superseding simulation can be *precisely* the same as the
40
+ deprecated one, there may be multiple superseding simulations
41
+ that have very similar parameters, in which case an error will
42
+ be raised and you must explicitly choose one. If there is
43
+ only one, a warning will be issued, but the superseding
44
+ simulation will be loaded.
45
+
46
+ Otherwise, a `ValueError` will be raised, with an explanation and
47
+ suggestions on what you might want to do.
48
+
49
+ Parameters
50
+ ----------
51
+ location : str
52
+ The location string for the simulation. This must include the
53
+ SXS ID, but can also include the version and Lev number, as
54
+ described above.
55
+
56
+ Other parameters
57
+ ----------------
58
+ ignore_deprecation : bool
59
+ If `True`, completely bypass checking for deprecation or
60
+ supersession. No warnings or errors will be issued.
61
+ auto_supersede : bool
62
+ If `True`, automatically load the superseding simulation, if
63
+ there is only one. If there are multiple superseding
64
+ simulations, an error will be raised, and you must explicitly
65
+ choose one. If there is only one, a warning will still be
66
+ issued, but the superseding simulation will be loaded. Note
67
+ that this can also be set in the configuration file with
68
+ `sxs.write_config(auto_supersede=True)`.
69
+ extrapolation : str
70
+ The extrapolation order to use for the strain and Psi4 data.
71
+ This is only relevant for versions 1 and 2 of the data format,
72
+ both of which default to "N2". Other options include "N3",
73
+ "N4", and "Outer". "Nx" refers to extrapolation by
74
+ polynomials in 1/r with degree `x`, while "Outer" refers to
75
+ data extracted at the outermost extraction radius but
76
+ corrected for time-dilation and areal-radius effects.
77
+
78
+ Returns
79
+ -------
80
+ simulation : SimulationBase
81
+ A `Simulation_v1` or `Simulation_v2` object, depending on the
82
+ version of the simulation data.
83
+
84
+ Note that all remaining arguments (including keyword arguments)
85
+ are passed on to the `SimulationBase`, `Simulation_v1`, and/or
86
+ `Simulation_v2` constructors, though none currently recognize any
87
+ arguments other than those listed above.
88
+
89
+ """
90
+ from .. import load
91
+
92
+ # Extract the simulation ID, version, and Lev from the location string
93
+ simulation_id, input_version = sxs_id_and_version(location)
94
+ if not simulation_id:
95
+ raise ValueError(f"Invalid SXS ID in '{simulation_id}'")
96
+ input_lev_number = lev_number(location) # Will be `None` if not present
97
+
98
+ # Load the simulation catalog and check if simulation ID exists in the catalog
99
+ simulations = load("simulations")
100
+ if simulation_id not in simulations:
101
+ raise ValueError(f"Simulation '{simulation_id}' not found in simulation catalog")
102
+
103
+ # Attach metadata to this object
104
+ metadata = Metadata(simulations[simulation_id])
105
+ series = simulations.dataframe.loc[simulation_id]
106
+
107
+ # Check if the specified version exists in the simulation catalog
108
+ if input_version not in metadata.DOI_versions:
109
+ raise ValueError(f"Version '{input_version}' not found in simulation catalog for '{simulation_id}'")
110
+
111
+ # Set various pieces of information about the simulation
112
+ version = input_version or max(metadata.DOI_versions)
113
+ if not version.startswith("v"):
114
+ raise ValueError(f"Invalid version string '{version}'")
115
+ sxs_id_stem = simulation_id
116
+ sxs_id = f"{sxs_id_stem}{version}"
117
+ url = f"{doi_url}{sxs_id}"
118
+
119
+ # Deal with "superseded_by" field, or "deprecated" keyword in the metadata
120
+ if not kwargs.get("ignore_deprecation", False):
121
+ auto_supersede = kwargs.get("auto_supersede", read_config("auto_supersede", False))
122
+ if (
123
+ input_version
124
+ and not auto_supersede
125
+ and ("deprecated" in metadata.get("keywords", []) or metadata.get("superseded_by", False))
126
+ ):
127
+ message = ("\n"
128
+ + f"Simulation '{sxs_id_stem}' is deprecated and/or superseded.\n"
129
+ + "Normally, this simulation should no longer be used, but you\n"
130
+ + f"explicitly requested version '{input_version}', so it is being used.\n"
131
+ )
132
+ warn(message)
133
+ else:
134
+ if "superseded_by" in metadata:
135
+ superseded_by = metadata["superseded_by"]
136
+ if auto_supersede and isinstance(superseded_by, list):
137
+ raise ValueError(
138
+ f"`auto_supersede` is enabled, but simulation '{sxs_id}' is\n"
139
+ + "superseded by multiple simulations. You must choose one\n"
140
+ + "explicitly from the list:\n"
141
+ + "\n".join(f" {s}" for s in superseded_by)
142
+ + "\nAlternatively, you could pass `ignore_deprecation=True` or\n"
143
+ + "specify a version to load this waveform anyway."
144
+ )
145
+ elif auto_supersede and isinstance(superseded_by, str):
146
+ message = f"\nSimulation '{sxs_id}' is being automatically superseded by '{superseded_by}'."
147
+ warn(message)
148
+ new_location = f"{superseded_by}{input_version}"
149
+ if input_lev_number:
150
+ new_location += f"/Lev{input_lev_number}"
151
+ return Simulation(new_location, *args, **kwargs)
152
+ elif isinstance(superseded_by, list):
153
+ raise ValueError(
154
+ f"Simulation '{sxs_id}' is superseded by multiple simulations.\n"
155
+ + "Even if you enable `auto_supersede`, with multiple options, you\n"
156
+ + "must choose one explicitly from the list:\n"
157
+ + "\n".join(f" {s}" for s in superseded_by)
158
+ + "\nAlternatively, you could pass `ignore_deprecation=True` or\n"
159
+ + "specify a version to load this waveform anyway."
160
+ )
161
+ elif isinstance(superseded_by, str):
162
+ raise ValueError(
163
+ f"Simulation '{sxs_id}' is superseded by '{superseded_by}'.\n"
164
+ + "Note that you could enable `auto_supersede` to automatically\n"
165
+ + "load the superseding simulation. Alternatively, you could\n"
166
+ + "pass `ignore_deprecation=True` or specify a version to load\n"
167
+ + "this waveform anyway."
168
+ )
169
+ else:
170
+ raise ValueError(
171
+ f"Simulation '{sxs_id}' is superseded by '{superseded_by}'.\n"
172
+ + "Note that you could pass `ignore_deprecation=True` or\n"
173
+ + "specify a version to load this waveform anyway."
174
+ )
175
+ if "deprecated" in metadata.get("keywords", []):
176
+ raise ValueError(
177
+ f"Simulation '{sxs_id}' is deprecated but has no superseding simulation.\n"
178
+ + "Note that you could pass `ignore_deprecation=True` or specify a version\n"
179
+ + "to to load this waveform anyway."
180
+ )
181
+
182
+ # We want to do this *after* deprecation checking, to avoid possibly unnecessary web requests
183
+ files = get_file_info(metadata, sxs_id)
184
+
185
+ # If Lev is given as part of `location`, use it; otherwise, use the highest available
186
+ lev_numbers = sorted({lev for f in files if (lev:=lev_number(f))})
187
+ output_lev_number = input_lev_number or max(lev_numbers)
188
+ location = f"{sxs_id_stem}{version}/Lev{output_lev_number}"
189
+
190
+ # Finally, figure out which version of the simulation to load and dispatch
191
+ version_number = float(version[1:])
192
+ if 1 <= version_number < 2.0:
193
+ return Simulation_v1(
194
+ metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, output_lev_number, location, *args, **kwargs
195
+ )
196
+ elif 2 <= version_number < 3.0:
197
+ return Simulation_v2(
198
+ metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, output_lev_number, location, *args, **kwargs
199
+ )
200
+ else:
201
+ raise ValueError(f"Version '{version}' not yet supported")
202
+
203
+
204
+ class SimulationBase:
205
+ """Base class for Simulation objects
206
+
207
+ Note that users almost certainly never need to call this function;
208
+ see the `Simulation` function or `sxs.load` function instead.
209
+
210
+ Attributes
211
+ ----------
212
+ metadata : Metadata
213
+ Metadata object for the simulation
214
+ series : pandas.Series
215
+ The metadata, as extracted from the `simulations.dataframe`,
216
+ meaning that it has columns consistent with other simulations,
217
+ even when the underlying Metadata objects do not. Note that
218
+ `metadata` is an alias for this attribute, just based on the
219
+ use of that name for `simulations`, but technically `pandas`
220
+ distinguishes a single row like this as a `Series` object.
221
+ version : str
222
+ Version number of the simulation
223
+ sxs_id_stem : str
224
+ SXS ID without the version number or Lev
225
+ sxs_id : str
226
+ SXS ID with the version number
227
+ location : str
228
+ Location string for the simulation, including the SXS ID,
229
+ version number, and Lev number.
230
+ url : str
231
+ URL for the DOI of the simulation
232
+ files : dict
233
+ Dictionary of file information for the simulation. The keys
234
+ are of the form "Lev5:Horizons.h5", and the values are
235
+ dictionaries with keys "checksum", "size", and "link".
236
+ lev_numbers : list
237
+ List of available Lev numbers for the simulation.
238
+ lev_number : int
239
+ Chosen Lev number for the simulation.
240
+ horizons : Horizons
241
+ Horizons object for the simulation
242
+ strain : Waveform
243
+ Strain Waveform object for the simulation. Note that `h` is
244
+ an alias for this attribute, both of which are case
245
+ insensitive: `Strain` and `H` are also acceptable.
246
+ psi4 : Waveform
247
+ Psi4 Waveform object for the simulation. Note that this
248
+ attribute is also case insensitive: `Psi4` is also acceptable.
249
+ psi3 : Waveform
250
+ Psi3 Waveform object for the simulation. Note that this
251
+ attribute is also case insensitive: `Psi3` is also acceptable.
252
+ In versions 1 and 2, this attribute will raise an error
253
+ because the data is not available.
254
+ psi2 : Waveform
255
+ Psi2 Waveform object for the simulation. Note that this
256
+ attribute is also case insensitive: `Psi2` is also acceptable.
257
+ In versions 1 and 2, this attribute will raise an error
258
+ because the data is not available.
259
+ psi1 : Waveform
260
+ Psi1 Waveform object for the simulation. Note that this
261
+ attribute is also case insensitive: `Psi1` is also acceptable.
262
+ In versions 1 and 2, this attribute will raise an error
263
+ because the data is not available.
264
+ psi0 : Waveform
265
+ Psi0 Waveform object for the simulation. Note that this
266
+ attribute is also case insensitive: `Psi0` is also acceptable.
267
+ In versions 1 and 2, this attribute will raise an error
268
+ because the data is not available.
269
+ """
270
+ def __init__(self,
271
+ metadata, series, version, sxs_id_stem, sxs_id, url, files, lev_numbers, lev_number, location,
272
+ *args, **kwargs
273
+ ):
274
+ self.metadata = metadata
275
+ self.series = series
276
+ self.version = version
277
+ self.sxs_id_stem = sxs_id_stem
278
+ self.sxs_id = sxs_id
279
+ self.url = url
280
+ self.files = files
281
+ self.lev_numbers = lev_numbers
282
+ self.lev_number = lev_number
283
+ self.location = location
284
+
285
+ def __repr__(self):
286
+ return f"""{type(self).__qualname__}("{self.sxs_id}")"""
287
+
288
+ def __str__(self):
289
+ return repr(self)
290
+
291
+ @property
292
+ def dataframe(self):
293
+ return self.series
294
+
295
+ @property
296
+ def versions(self):
297
+ return self.metadata.DOI_versions
298
+
299
+ @property
300
+ def lev(self):
301
+ return f"Lev{self.lev_number}"
302
+
303
+ @property
304
+ def Lev(self):
305
+ return self.lev
306
+
307
+ def load_horizons(self):
308
+ from .. import load
309
+ sxs_id_path = Path(sxs_path_to_system_path(self.sxs_id))
310
+ horizons_path = self.horizons_path
311
+ horizons_location = self.files.get(horizons_path)["link"]
312
+ horizons_truepath = sxs_id_path / sxs_path_to_system_path(horizons_path)
313
+ return load(horizons_location, truepath=horizons_truepath)
314
+
315
+ @property
316
+ def horizons(self):
317
+ if not hasattr(self, "_horizons"):
318
+ self._horizons = self.load_horizons()
319
+ return self._horizons
320
+
321
+ @property
322
+ def strain(self):
323
+ if not hasattr(self, "_strain"):
324
+ self._strain = self.load_waveform(*self.strain_path)
325
+ return self._strain
326
+ Strain = strain
327
+ h = strain
328
+ H = strain
329
+
330
+ # I'm not entirely sure about the conjugations and factors of 2 in
331
+ # shear and news in our conventions. These will have to wait for
332
+ # later.
333
+ #
334
+ # @property
335
+ # def shear(self):
336
+ # if not hasattr(self, "_shear"):
337
+ # self._shear = self.strain.bar / 2
338
+ # return self._shear
339
+ # sigma = shear
340
+ # σ = shear
341
+ # Shear = shear
342
+ # Sigma = shear
343
+ # Σ = shear
344
+ #
345
+ # @property
346
+ # def news(self):
347
+ # if not hasattr(self, "_news"):
348
+ # self._news = self.strain.dot
349
+ # return self._news
350
+ # News = news
351
+
352
+ @property
353
+ def psi4(self):
354
+ if not hasattr(self, "_psi4"):
355
+ self._psi4 = self.load_waveform(*self.psi4_path)
356
+ return self._psi4
357
+ Psi4 = psi4
358
+
359
+ @property
360
+ def psi3(self):
361
+ raise AttributeError(f"Psi3 is not available for version {self.version} of the data")
362
+ Psi3 = psi3
363
+
364
+ @property
365
+ def psi2(self):
366
+ raise AttributeError(f"Psi2 is not available for version {self.version} of the data")
367
+ Psi2 = psi2
368
+
369
+ @property
370
+ def psi1(self):
371
+ raise AttributeError(f"Psi1 is not available for version {self.version} of the data")
372
+ Psi1 = psi1
373
+
374
+ @property
375
+ def psi0(self):
376
+ raise AttributeError(f"Psi0 is not available for version {self.version} of the data")
377
+ Psi0 = psi0
378
+
379
+
380
+ class Simulation_v1(SimulationBase):
381
+ """Simulation object for version 1 of the data format
382
+
383
+ Note that users almost certainly never need to call this function;
384
+ see the `Simulation` function or `sxs.load` function instead. See
385
+ also `SimulationBase` for the base class that this class inherits
386
+ from.
387
+ """
388
+ # We have to deal with the fact that some early file paths on
389
+ # Zenodo included the SXS ID as a prefix, while others did not.
390
+ # This means that we have to check for both possibilities in
391
+ # `load_horizons` and `load_waveform`.
392
+
393
+ def __init__(self, *args, **kwargs):
394
+ super().__init__(*args, **kwargs)
395
+ self.extrapolation = kwargs.get("extrapolation", "N2")
396
+
397
+ @property
398
+ def horizons_path(self):
399
+ return f"{self.lev}/Horizons.h5"
400
+
401
+ def load_horizons(self):
402
+ from .. import load
403
+ sxs_id_path = Path(sxs_path_to_system_path(self.sxs_id))
404
+ horizons_path = self.horizons_path
405
+ if horizons_path in self.files:
406
+ horizons_location = self.files.get(horizons_path)["link"]
407
+ else:
408
+ if (extended_horizons_path := f"{self.sxs_id_stem}/{horizons_path}") in self.files:
409
+ horizons_location = self.files.get(extended_horizons_path)["link"]
410
+ else:
411
+ raise ValueError(f"File '{horizons_path}' not found in simulation files")
412
+ horizons_truepath = sxs_id_path / sxs_path_to_system_path(horizons_path)
413
+ return load(horizons_location, truepath=horizons_truepath)
414
+
415
+ @property
416
+ def strain_path(self):
417
+ extrapolation = (
418
+ f"Extrapolated_{self.extrapolation}.dir"
419
+ if self.extrapolation != "Outer"
420
+ else "OutermostExtraction.dir"
421
+ )
422
+ return (
423
+ f"{self.lev}/rhOverM_Asymptotic_GeometricUnits_CoM.h5",
424
+ extrapolation
425
+ )
426
+
427
+ @property
428
+ def psi4_path(self):
429
+ extrapolation = (
430
+ f"Extrapolated_{self.extrapolation}.dir"
431
+ if self.extrapolation != "Outer"
432
+ else "OutermostExtraction.dir"
433
+ )
434
+ return (
435
+ f"{self.lev}/rMPsi4_Asymptotic_GeometricUnits_CoM.h5",
436
+ extrapolation
437
+ )
438
+
439
+ def load_waveform(self, file_name, group):
440
+ from .. import load
441
+ if file_name in self.files:
442
+ location = self.files.get(file_name)["link"]
443
+ else:
444
+ if (extended_file_name := f"{self.sxs_id_stem}/{file_name}") in self.files:
445
+ location = self.files.get(extended_file_name)["link"]
446
+ else:
447
+ raise ValueError(f"File '{file_name}' not found in simulation files")
448
+ sxs_id_path = Path(sxs_path_to_system_path(self.sxs_id))
449
+ truepath = sxs_id_path / sxs_path_to_system_path(file_name)
450
+ w = load(location, truepath=truepath, extrapolation_order=group)
451
+ w.metadata = self.metadata
452
+ return w
453
+
454
+
455
+ class Simulation_v2(SimulationBase):
456
+ """Simulation object for version 2 of the data format
457
+
458
+ Note that users almost certainly never need to call this function;
459
+ see the `Simulation` function or `sxs.load` function instead. See
460
+ also `SimulationBase` for the base class that this class inherits
461
+ from.
462
+ """
463
+ def __init__(self, *args, **kwargs):
464
+ super().__init__(*args, **kwargs)
465
+ self.extrapolation = kwargs.get("extrapolation", "N2")
466
+
467
+ @property
468
+ def horizons_path(self):
469
+ return f"{self.lev}:Horizons.h5"
470
+
471
+ @property
472
+ def strain_path(self):
473
+ return (
474
+ f"{self.lev}:Strain_{self.extrapolation}",
475
+ "/"
476
+ )
477
+
478
+ @property
479
+ def psi4_path(self):
480
+ extrapolation = (
481
+ f"Extrapolated_{self.extrapolation}.dir"
482
+ if self.extrapolation != "Outer"
483
+ else "OutermostExtraction.dir"
484
+ )
485
+ return (
486
+ f"{self.lev}:ExtraWaveforms",
487
+ f"/rMPsi4_Asymptotic_GeometricUnits_CoM_Mem/{extrapolation}"
488
+ )
489
+
490
+ def load_waveform(self, file_name, group):
491
+ from .. import load
492
+ # Note that `name` should not have the file ending on input,
493
+ # but we will strip it regardless with `.stem`.
494
+ file_name = Path(file_name).stem
495
+ sxs_id_path = Path(sxs_path_to_system_path(self.sxs_id))
496
+ h5_path = f"{file_name}.h5"
497
+ json_path = f"{file_name}.json"
498
+ h5_location = self.files.get(h5_path)["link"]
499
+ json_location = self.files.get(json_path)["link"]
500
+ h5_truepath = sxs_id_path / sxs_path_to_system_path(h5_path)
501
+ json_truepath = sxs_id_path / sxs_path_to_system_path(json_path)
502
+ if not json_truepath.exists():
503
+ download_file(json_location, sxs_directory("cache") / json_truepath)
504
+ return load(h5_location, truepath=h5_truepath, group=group, metadata=self.metadata)
505
+
506
+
507
+ def get_file_info(metadata, sxs_id):
508
+ from .. import load_via_sxs_id
509
+ if "files" in metadata:
510
+ return metadata["files"]
511
+ truepath = Path(sxs_path_to_system_path(sxs_id)) / "zenodo_metadata.json"
512
+ record = load_via_sxs_id(sxs_id, "export/json", truepath=truepath)
513
+ entries = record["files"]["entries"]
514
+ return {
515
+ str(filename): {
516
+ "checksum": entry["checksum"],
517
+ "size": entry["size"],
518
+ "link": entry["links"]["content"],
519
+ }
520
+ for filename, entry in entries.items()
521
+ }