gwsim 0.1.0__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.
- gwsim/__init__.py +11 -0
- gwsim/__main__.py +8 -0
- gwsim/cli/__init__.py +0 -0
- gwsim/cli/config.py +88 -0
- gwsim/cli/default_config.py +56 -0
- gwsim/cli/main.py +101 -0
- gwsim/cli/merge.py +150 -0
- gwsim/cli/repository/__init__.py +0 -0
- gwsim/cli/repository/create.py +91 -0
- gwsim/cli/repository/delete.py +51 -0
- gwsim/cli/repository/download.py +54 -0
- gwsim/cli/repository/list_depositions.py +63 -0
- gwsim/cli/repository/main.py +38 -0
- gwsim/cli/repository/metadata/__init__.py +0 -0
- gwsim/cli/repository/metadata/main.py +24 -0
- gwsim/cli/repository/metadata/update.py +58 -0
- gwsim/cli/repository/publish.py +52 -0
- gwsim/cli/repository/upload.py +74 -0
- gwsim/cli/repository/utils.py +47 -0
- gwsim/cli/repository/verify.py +61 -0
- gwsim/cli/simulate.py +220 -0
- gwsim/cli/simulate_utils.py +596 -0
- gwsim/cli/utils/__init__.py +85 -0
- gwsim/cli/utils/checkpoint.py +178 -0
- gwsim/cli/utils/config.py +347 -0
- gwsim/cli/utils/hash.py +23 -0
- gwsim/cli/utils/retry.py +62 -0
- gwsim/cli/utils/simulation_plan.py +439 -0
- gwsim/cli/utils/template.py +56 -0
- gwsim/cli/utils/utils.py +149 -0
- gwsim/cli/validate.py +255 -0
- gwsim/data/__init__.py +8 -0
- gwsim/data/serialize/__init__.py +9 -0
- gwsim/data/serialize/decoder.py +59 -0
- gwsim/data/serialize/encoder.py +44 -0
- gwsim/data/serialize/serializable.py +33 -0
- gwsim/data/time_series/__init__.py +3 -0
- gwsim/data/time_series/inject.py +104 -0
- gwsim/data/time_series/time_series.py +355 -0
- gwsim/data/time_series/time_series_list.py +182 -0
- gwsim/detector/__init__.py +8 -0
- gwsim/detector/base.py +156 -0
- gwsim/detector/detectors/E1_2L_Aligned_Sardinia.interferometer +22 -0
- gwsim/detector/detectors/E1_2L_Misaligned_Sardinia.interferometer +22 -0
- gwsim/detector/detectors/E1_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E1_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/detectors/E2_2L_Aligned_EMR.interferometer +22 -0
- gwsim/detector/detectors/E2_2L_Misaligned_EMR.interferometer +22 -0
- gwsim/detector/detectors/E2_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E2_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/detectors/E3_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E3_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/noise_curves/ET_10_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_10_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_15_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_15_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_20_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_20_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_D_psd.txt +3000 -0
- gwsim/detector/utils.py +90 -0
- gwsim/glitch/__init__.py +7 -0
- gwsim/glitch/base.py +69 -0
- gwsim/mixin/__init__.py +8 -0
- gwsim/mixin/detector.py +203 -0
- gwsim/mixin/gwf.py +192 -0
- gwsim/mixin/population_reader.py +175 -0
- gwsim/mixin/randomness.py +107 -0
- gwsim/mixin/time_series.py +295 -0
- gwsim/mixin/waveform.py +47 -0
- gwsim/noise/__init__.py +19 -0
- gwsim/noise/base.py +134 -0
- gwsim/noise/bilby_stationary_gaussian.py +117 -0
- gwsim/noise/colored_noise.py +275 -0
- gwsim/noise/correlated_noise.py +257 -0
- gwsim/noise/pycbc_stationary_gaussian.py +112 -0
- gwsim/noise/stationary_gaussian.py +44 -0
- gwsim/noise/white_noise.py +51 -0
- gwsim/repository/__init__.py +0 -0
- gwsim/repository/zenodo.py +269 -0
- gwsim/signal/__init__.py +11 -0
- gwsim/signal/base.py +137 -0
- gwsim/signal/cbc.py +61 -0
- gwsim/simulator/__init__.py +7 -0
- gwsim/simulator/base.py +315 -0
- gwsim/simulator/state.py +85 -0
- gwsim/utils/__init__.py +11 -0
- gwsim/utils/datetime_parser.py +44 -0
- gwsim/utils/et_2l_geometry.py +165 -0
- gwsim/utils/io.py +167 -0
- gwsim/utils/log.py +145 -0
- gwsim/utils/population.py +48 -0
- gwsim/utils/random.py +69 -0
- gwsim/utils/retry.py +75 -0
- gwsim/utils/triangular_et_geometry.py +164 -0
- gwsim/version.py +7 -0
- gwsim/waveform/__init__.py +7 -0
- gwsim/waveform/factory.py +83 -0
- gwsim/waveform/pycbc_wrapper.py +37 -0
- gwsim-0.1.0.dist-info/METADATA +157 -0
- gwsim-0.1.0.dist-info/RECORD +103 -0
- gwsim-0.1.0.dist-info/WHEEL +4 -0
- gwsim-0.1.0.dist-info/entry_points.txt +2 -0
- gwsim-0.1.0.dist-info/licenses/LICENSE +21 -0
gwsim/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""A package to simulate a population of gravitational waves."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import utils
|
|
6
|
+
from .utils.log import setup_logger
|
|
7
|
+
from .version import __version__
|
|
8
|
+
|
|
9
|
+
setup_logger()
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__", "utils"]
|
gwsim/__main__.py
ADDED
gwsim/cli/__init__.py
ADDED
|
File without changes
|
gwsim/cli/config.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions to load and save configuration files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from ..utils.io import check_file_exist, check_file_overwrite
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("gwsim")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@check_file_exist()
|
|
19
|
+
def load_config(file_name: Path, encoding: str = "utf-8") -> dict:
|
|
20
|
+
"""Load configuration file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_name (Path): File name.
|
|
24
|
+
encoding (str, optional): File encoding. Defaults to "utf-8".
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
dict: A dictionary of the configuration.
|
|
28
|
+
"""
|
|
29
|
+
with open(file_name, encoding=encoding) as f:
|
|
30
|
+
config = yaml.safe_load(f)
|
|
31
|
+
return config
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@check_file_overwrite()
|
|
35
|
+
def save_config(
|
|
36
|
+
file_name: Path, config: dict, overwrite: bool = False, encoding: str = "utf-8", backup: bool = True
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Save configuration file safely with optional backup.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
file_name (Path): File name.
|
|
42
|
+
config (dict): A dictionary of configuration.
|
|
43
|
+
overwrite (bool, optional): If True, overwrite the existing file, or otherwise raise an error.
|
|
44
|
+
Defaults to False.
|
|
45
|
+
encoding (str, optional): File encoding. Defaults to "utf-8".
|
|
46
|
+
backup (bool, optional): If True and overwriting, create a backup of the existing file.
|
|
47
|
+
Defaults to True.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
FileExistsError: If file_name exists and overwrite is False, raise an error.
|
|
51
|
+
"""
|
|
52
|
+
# Create backup if file exists and we're overwriting
|
|
53
|
+
if file_name.exists() and overwrite and backup:
|
|
54
|
+
backup_path = file_name.with_suffix(f"{file_name.suffix}.backup")
|
|
55
|
+
logger.info("Creating backup: %s", backup_path)
|
|
56
|
+
backup_path.write_text(file_name.read_text(encoding=encoding), encoding=encoding)
|
|
57
|
+
|
|
58
|
+
# Atomic write using temporary file
|
|
59
|
+
temp_file = file_name.with_suffix(f"{file_name.suffix}.tmp")
|
|
60
|
+
try:
|
|
61
|
+
with open(temp_file, "w", encoding=encoding) as f:
|
|
62
|
+
yaml.safe_dump(config, f, default_flow_style=False, sort_keys=False)
|
|
63
|
+
|
|
64
|
+
# Atomic move (rename) - this is atomic on most filesystems
|
|
65
|
+
temp_file.replace(file_name)
|
|
66
|
+
logger.info("Configuration saved to: %s", file_name)
|
|
67
|
+
|
|
68
|
+
except Exception:
|
|
69
|
+
# Clean up temp file if something went wrong
|
|
70
|
+
if temp_file.exists():
|
|
71
|
+
temp_file.unlink()
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_config_value(config: dict, key: str, default_value: Any | None = None) -> Any:
|
|
76
|
+
"""Get the argument
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config (dict): A dictionary of configuration.
|
|
80
|
+
key (str): Key of the entry.
|
|
81
|
+
default_value (Any | None, optional): Default value if key is not present. Defaults to None.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Any: Value of the corresponding key in config.
|
|
85
|
+
"""
|
|
86
|
+
if key in config:
|
|
87
|
+
return config[key]
|
|
88
|
+
return default_value
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A tool to generate default configuration file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
_DEFAULT_CONFIG = {
|
|
13
|
+
"globals": {
|
|
14
|
+
"working-directory": ".",
|
|
15
|
+
"sampling-frequency": 16384,
|
|
16
|
+
"duration": 4,
|
|
17
|
+
"output-directory": "output",
|
|
18
|
+
"metadata-directory": "metadata",
|
|
19
|
+
},
|
|
20
|
+
"simulators": {
|
|
21
|
+
"example": {
|
|
22
|
+
"class": "WhiteNoise", # Resolves to gwsim.noise.WhiteNoise
|
|
23
|
+
"arguments": {
|
|
24
|
+
"batch_size": 1,
|
|
25
|
+
"max_samples": 10,
|
|
26
|
+
"loc": 0.0,
|
|
27
|
+
"scale": 1.0,
|
|
28
|
+
"seed": 0,
|
|
29
|
+
},
|
|
30
|
+
"output": {
|
|
31
|
+
"file_name": "example-{{ start-time }}-{{ duration }}.gwf",
|
|
32
|
+
"arguments": {
|
|
33
|
+
"channel": "STRAIN",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_config_command(
|
|
42
|
+
output: Annotated[str, typer.Option("--output", help="File name of the output", prompt=True)] = "config.yaml",
|
|
43
|
+
overwrite: Annotated[bool, typer.Option("--overwrite", help="Overwrite the existing file")] = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Write the default configuration file to disk.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
output (str): Name of the output file.
|
|
49
|
+
overwrite (bool): If True, overwrite the existing file, otherwise raise an error if output already exists.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
FileExistsError: If file_name exists and overwrite is False, raise an error.
|
|
53
|
+
"""
|
|
54
|
+
from gwsim.cli.utils.config import save_config # pylint: disable=import-outside-toplevel
|
|
55
|
+
|
|
56
|
+
save_config(file_name=Path(output), config=_DEFAULT_CONFIG, overwrite=overwrite)
|
gwsim/cli/main.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main command line tool to generate mock data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import enum
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoggingLevel(str, enum.Enum):
|
|
14
|
+
"""Logging levels for the CLI."""
|
|
15
|
+
|
|
16
|
+
NOTSET = "NOTSET"
|
|
17
|
+
DEBUG = "DEBUG"
|
|
18
|
+
INFO = "INFO"
|
|
19
|
+
WARNING = "WARNING"
|
|
20
|
+
ERROR = "ERROR"
|
|
21
|
+
CRITICAL = "CRITICAL"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Create the main Typer app
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="gwsim",
|
|
27
|
+
help="Gravitational Wave Simulation Data Simulator",
|
|
28
|
+
rich_markup_mode="rich",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def setup_logging(level: LoggingLevel = LoggingLevel.INFO) -> None:
|
|
33
|
+
"""Set up logging with Rich handler."""
|
|
34
|
+
import logging # pylint: disable=import-outside-toplevel
|
|
35
|
+
|
|
36
|
+
from rich.console import Console # pylint: disable=import-outside-toplevel
|
|
37
|
+
from rich.logging import RichHandler # pylint: disable=import-outside-toplevel
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("gwsim")
|
|
40
|
+
|
|
41
|
+
logger.setLevel(level.value)
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
|
|
45
|
+
# Remove any existing handlers to ensure RichHandler is used
|
|
46
|
+
for h in logger.handlers[:]: # Use slice copy to avoid modification during iteration
|
|
47
|
+
logger.removeHandler(h)
|
|
48
|
+
# Add the RichHandler
|
|
49
|
+
if not logger.handlers:
|
|
50
|
+
handler = RichHandler(
|
|
51
|
+
console=console,
|
|
52
|
+
rich_tracebacks=True,
|
|
53
|
+
show_time=True,
|
|
54
|
+
show_level=True, # Keep level (e.g., DEBUG, INFO) for clarity
|
|
55
|
+
markup=True, # Enable Rich markup in messages for styling
|
|
56
|
+
level=level.value, # Ensure handler respects the level
|
|
57
|
+
omit_repeated_times=False,
|
|
58
|
+
log_time_format="%H:%M",
|
|
59
|
+
)
|
|
60
|
+
handler.setLevel(level.value)
|
|
61
|
+
logger.addHandler(handler)
|
|
62
|
+
|
|
63
|
+
# Prevent propagation to root logger to avoid duplicate output
|
|
64
|
+
logger.propagate = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.callback()
|
|
68
|
+
def main(
|
|
69
|
+
verbose: Annotated[
|
|
70
|
+
LoggingLevel,
|
|
71
|
+
typer.Option("--verbose", "-v", help="Set verbosity level"),
|
|
72
|
+
] = LoggingLevel.INFO,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Gravitational Wave Simulation Data Simulator.
|
|
75
|
+
|
|
76
|
+
This command-line tool provides functionality for generating
|
|
77
|
+
gravitational wave detector simulation data.
|
|
78
|
+
"""
|
|
79
|
+
setup_logging(verbose)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Import and register commands after app is created
|
|
83
|
+
def register_commands() -> None:
|
|
84
|
+
"""Register all CLI commands."""
|
|
85
|
+
|
|
86
|
+
# Fast imports
|
|
87
|
+
from gwsim.cli.default_config import default_config_command # pylint: disable=import-outside-toplevel
|
|
88
|
+
from gwsim.cli.merge import merge_command # pylint: disable=import-outside-toplevel
|
|
89
|
+
from gwsim.cli.repository.main import repository_app # pylint: disable=import-outside-toplevel
|
|
90
|
+
from gwsim.cli.simulate import simulate_command # pylint: disable=import-outside-toplevel
|
|
91
|
+
from gwsim.cli.validate import validate_command # pylint: disable=import-outside-toplevel
|
|
92
|
+
|
|
93
|
+
app.command("simulate")(simulate_command)
|
|
94
|
+
app.command("merge")(merge_command)
|
|
95
|
+
app.command("default-config")(default_config_command)
|
|
96
|
+
app.command("validate")(validate_command)
|
|
97
|
+
|
|
98
|
+
app.add_typer(repository_app, name="repository", help="Manage Zenodo repositories")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
register_commands()
|
gwsim/cli/merge.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""CLI to merge the frame files generated by gwsim simulations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def merge_command( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
12
|
+
file_names: Annotated[list[Path], typer.Argument(..., help="List of frame files to merge")],
|
|
13
|
+
channel: Annotated[str, typer.Option("--channel", help="Channel name to merge")] = "STRAIN",
|
|
14
|
+
output: Annotated[str, typer.Option("--output", help="Output merged frame file name")] = "merged.gwf",
|
|
15
|
+
output_channel: (
|
|
16
|
+
Annotated[str, typer.Option("--output-channel", help="Channel name for the output file")] | None
|
|
17
|
+
) = None,
|
|
18
|
+
metadata: Annotated[list[str], typer.Option("--metadata", help="Metadata file to use for merging")] | None = None,
|
|
19
|
+
author: Annotated[str, typer.Option("--author", help="Author of the merged file")] | None = None,
|
|
20
|
+
email: Annotated[str, typer.Option("--email", help="Email of the author")] | None = None,
|
|
21
|
+
force: Annotated[bool, typer.Option("--force", help="Bypass the requirements of providing metadata files")] = False,
|
|
22
|
+
):
|
|
23
|
+
"""Merge multiple frame files into a single file.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
file_names (list[str]): List of frame files to merge.
|
|
27
|
+
channel (str): Channel name to merge.
|
|
28
|
+
output (str): Output merged frame file name.
|
|
29
|
+
output_channel (str | None): Channel name for the output file. If None, use
|
|
30
|
+
metadata (list[str] | None): List of metadata files corresponding to the frame files.
|
|
31
|
+
author (str | None): Author of the merged file.
|
|
32
|
+
email (str | None): Email of the author.
|
|
33
|
+
force (bool): If True, bypass the requirement of providing metadata files.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If metadata files are not provided and force is False.
|
|
37
|
+
"""
|
|
38
|
+
import datetime # pylint: disable=import-outside-toplevel
|
|
39
|
+
import getpass # pylint: disable=import-outside-toplevel
|
|
40
|
+
from typing import cast # pylint: disable=import-outside-toplevel
|
|
41
|
+
|
|
42
|
+
import yaml # pylint: disable=import-outside-toplevel
|
|
43
|
+
from gwpy.timeseries import TimeSeries # pylint: disable=import-outside-toplevel
|
|
44
|
+
|
|
45
|
+
from gwsim.cli.utils.hash import compute_file_hash # pylint: disable=import-outside-toplevel
|
|
46
|
+
from gwsim.utils.log import get_dependency_versions # pylint: disable=import-outside-toplevel
|
|
47
|
+
|
|
48
|
+
if file_names is None:
|
|
49
|
+
file_names = []
|
|
50
|
+
if not isinstance(metadata, list) and not force:
|
|
51
|
+
raise ValueError("Metadata files must be provided unless --force is used.")
|
|
52
|
+
|
|
53
|
+
if isinstance(metadata, list) and not force:
|
|
54
|
+
typer.echo("Validating files...")
|
|
55
|
+
|
|
56
|
+
if len(file_names) != len(metadata or []):
|
|
57
|
+
raise ValueError("The number of metadata files must match the number of frame files.")
|
|
58
|
+
|
|
59
|
+
# Validate the files against the metadata
|
|
60
|
+
for i, file_name in enumerate(file_names):
|
|
61
|
+
metadata_file = Path(metadata[i])
|
|
62
|
+
|
|
63
|
+
# Find the corresponding metadata entry for the file_name
|
|
64
|
+
with metadata_file.open("r", encoding="utf-8") as f:
|
|
65
|
+
file_metadata = yaml.safe_load(f)
|
|
66
|
+
|
|
67
|
+
file_hashes: dict = file_metadata.get("file_hashes", {})
|
|
68
|
+
expected_hash = file_hashes.get(file_name.name)
|
|
69
|
+
|
|
70
|
+
if expected_hash is None:
|
|
71
|
+
raise ValueError(f"No hash found in metadata for file {file_name.name}")
|
|
72
|
+
|
|
73
|
+
actual_hash = compute_file_hash(file_name)
|
|
74
|
+
|
|
75
|
+
if actual_hash != expected_hash:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Hash mismatch for file {file_name.name}: expected {expected_hash}, got {actual_hash}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Placeholder for actual merging logic
|
|
81
|
+
typer.echo(f"Merging files: {file_names}")
|
|
82
|
+
|
|
83
|
+
# Read the first file
|
|
84
|
+
frame_data = cast(TimeSeries, TimeSeries.read(file_names[0], channel))
|
|
85
|
+
|
|
86
|
+
# Get the start time, duration and sampling frequency
|
|
87
|
+
start_time = frame_data.epoch
|
|
88
|
+
duration = frame_data.duration
|
|
89
|
+
sampling_frequency = frame_data.sample_rate
|
|
90
|
+
|
|
91
|
+
for i in range(1, len(file_names)):
|
|
92
|
+
next_frame_data = cast(TimeSeries, TimeSeries.read(file_names[i], channel))
|
|
93
|
+
|
|
94
|
+
if next_frame_data.epoch != start_time:
|
|
95
|
+
raise ValueError(f"Start time mismatch: {next_frame_data.epoch} != {start_time}")
|
|
96
|
+
if next_frame_data.duration != duration:
|
|
97
|
+
raise ValueError(f"Duration mismatch: {next_frame_data.duration} != {duration}")
|
|
98
|
+
if next_frame_data.sample_rate != sampling_frequency:
|
|
99
|
+
raise ValueError(f"Sampling frequency mismatch: {next_frame_data.sample_rate} != {sampling_frequency}")
|
|
100
|
+
|
|
101
|
+
frame_data = frame_data.inject(next_frame_data)
|
|
102
|
+
|
|
103
|
+
# Write the merged data to a new file
|
|
104
|
+
# Atomic write to avoid partial writes
|
|
105
|
+
# The write function is suffix sensitive, so we prepend a .tmp to the original suffix
|
|
106
|
+
temp_output = Path(output).with_suffix(".tmp" + Path(output).suffix)
|
|
107
|
+
if output_channel is not None:
|
|
108
|
+
frame_data.channel = output_channel
|
|
109
|
+
frame_data.write(temp_output)
|
|
110
|
+
temp_output.rename(output)
|
|
111
|
+
|
|
112
|
+
if metadata:
|
|
113
|
+
# If metadata is provided, create a new metadata file for the merged file
|
|
114
|
+
|
|
115
|
+
typer.echo("Creating merged metadata file...")
|
|
116
|
+
|
|
117
|
+
merged_metadata = {"type": "merged", "source_files": {}}
|
|
118
|
+
|
|
119
|
+
for i, file_name in enumerate(file_names):
|
|
120
|
+
metadata_file = Path(metadata[i])
|
|
121
|
+
|
|
122
|
+
with metadata_file.open("r", encoding="utf-8") as f:
|
|
123
|
+
file_metadata = yaml.safe_load(f)
|
|
124
|
+
|
|
125
|
+
merged_metadata["source_files"][file_name] = file_metadata
|
|
126
|
+
|
|
127
|
+
if author is None:
|
|
128
|
+
author = getpass.getuser()
|
|
129
|
+
|
|
130
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc)
|
|
131
|
+
|
|
132
|
+
merged_metadata["output_files"] = [output]
|
|
133
|
+
merged_metadata["file_hashes"] = {output: compute_file_hash(output)}
|
|
134
|
+
merged_metadata["author"] = author
|
|
135
|
+
merged_metadata["email"] = email
|
|
136
|
+
merged_metadata["timestamp"] = timestamp.isoformat()
|
|
137
|
+
merged_metadata["versions"] = get_dependency_versions()
|
|
138
|
+
|
|
139
|
+
# Atomic write of metadata file
|
|
140
|
+
merged_metadata_file = Path(output).with_suffix(".metadata.yaml")
|
|
141
|
+
temp_metadata_file = merged_metadata_file.with_suffix(".tmp")
|
|
142
|
+
with temp_metadata_file.open("w", encoding="utf-8") as f:
|
|
143
|
+
yaml.safe_dump(merged_metadata, f)
|
|
144
|
+
temp_metadata_file.rename(merged_metadata_file)
|
|
145
|
+
|
|
146
|
+
typer.echo(f"Merged metadata file created at {merged_metadata_file}")
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
typer.echo("No metadata provided for the source files.")
|
|
150
|
+
typer.echo("No metadata file will be created for the merged file.")
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""CLI for creating Zenodo repository depositions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_command( # pylint: disable=too-many-locals
|
|
12
|
+
title: Annotated[str | None, typer.Option("--title", help="Deposition title")] = None,
|
|
13
|
+
description: Annotated[str | None, typer.Option("--description", help="Deposition description")] = None,
|
|
14
|
+
metadata_file: Annotated[
|
|
15
|
+
Path | None, typer.Option("--metadata-file", help="YAML file with additional metadata")
|
|
16
|
+
] = None,
|
|
17
|
+
sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment for testing")] = False,
|
|
18
|
+
token: Annotated[
|
|
19
|
+
str | None,
|
|
20
|
+
typer.Option(
|
|
21
|
+
"--token",
|
|
22
|
+
help=(
|
|
23
|
+
"Zenodo access token (default: ZENODO_API_TOKEN env or"
|
|
24
|
+
" ZENODO_SANDBOX_API_TOKEN env for Zenodo Sandbox)"
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Create a new deposition on Zenodo.
|
|
30
|
+
|
|
31
|
+
Interactive mode: Leave options blank to be prompted.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
# Interactive mode
|
|
35
|
+
gwsim repository create
|
|
36
|
+
|
|
37
|
+
# With all options
|
|
38
|
+
gwsim repository create --title "GW Simulation Data" --description "MDC v1"
|
|
39
|
+
|
|
40
|
+
# Using metadata file
|
|
41
|
+
gwsim repository create --metadata-file metadata.yaml
|
|
42
|
+
"""
|
|
43
|
+
import logging # pylint: disable=import-outside-toplevel
|
|
44
|
+
|
|
45
|
+
import yaml # pylint: disable=import-outside-toplevel
|
|
46
|
+
from rich.console import Console # pylint: disable=import-outside-toplevel
|
|
47
|
+
|
|
48
|
+
from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("gwsim")
|
|
51
|
+
console = Console()
|
|
52
|
+
|
|
53
|
+
client = get_zenodo_client(sandbox=sandbox, token=token)
|
|
54
|
+
|
|
55
|
+
if title is None:
|
|
56
|
+
title = typer.prompt("Deposition Title")
|
|
57
|
+
|
|
58
|
+
if description is None:
|
|
59
|
+
description = typer.prompt("Deposition Description", default="")
|
|
60
|
+
|
|
61
|
+
metadata_dict = {"title": title}
|
|
62
|
+
metadata_dict["description"] = description
|
|
63
|
+
|
|
64
|
+
if metadata_file:
|
|
65
|
+
if not metadata_file.exists():
|
|
66
|
+
console.print(f"[red]Error:[/red] Metadata file not found: {metadata_file}")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
with metadata_file.open("r") as f:
|
|
69
|
+
extra = yaml.safe_load(f)
|
|
70
|
+
if extra:
|
|
71
|
+
metadata_dict.update(extra)
|
|
72
|
+
|
|
73
|
+
console.print("[bold blue]Creating deposition...[/bold blue]")
|
|
74
|
+
try:
|
|
75
|
+
result = client.create_deposition(metadata=metadata_dict)
|
|
76
|
+
deposition_id = result.get("id")
|
|
77
|
+
if sandbox:
|
|
78
|
+
console.print("[yellow]Note:[/yellow] Created in Zenodo Sandbox environment.")
|
|
79
|
+
console.print("[green]✓ Deposition created successfully![/green]")
|
|
80
|
+
console.print(f" [cyan]ID:[/cyan] {deposition_id}")
|
|
81
|
+
if sandbox:
|
|
82
|
+
console.print(
|
|
83
|
+
"[yellow]Note:[/yellow] This is a sandbox deposition. Use [bold]--sandbox[/bold] to access it later."
|
|
84
|
+
)
|
|
85
|
+
console.print(f" [cyan]Next:[/cyan] gwsim repository upload {deposition_id} --file <path> --sandbox")
|
|
86
|
+
else:
|
|
87
|
+
console.print(f" [cyan]Next:[/cyan] gwsim repository upload {deposition_id} --file <path>")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"[red]✗ Failed to create deposition: {e}[/red]")
|
|
90
|
+
logger.error("Create deposition failed: %s", e)
|
|
91
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CLI for deleting Zenodo repository depositions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def delete_command(
|
|
11
|
+
deposition_id: Annotated[str, typer.Argument(help="Deposition ID")],
|
|
12
|
+
sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
|
|
13
|
+
token: Annotated[str | None, typer.Option("--token", help="Zenodo access token")] = None,
|
|
14
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation prompt")] = False,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Delete an unpublished deposition.
|
|
17
|
+
|
|
18
|
+
Warning: Only unpublished (draft) depositions can be deleted.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
gwsim repository delete 123456
|
|
22
|
+
gwsim repository delete 123456 --force
|
|
23
|
+
"""
|
|
24
|
+
import logging # pylint: disable=import-outside-toplevel
|
|
25
|
+
|
|
26
|
+
from rich.console import Console # pylint: disable=import-outside-toplevel
|
|
27
|
+
from rich.prompt import Confirm # pylint: disable=import-outside-toplevel
|
|
28
|
+
|
|
29
|
+
from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("gwsim")
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
if not force and not Confirm.ask(
|
|
35
|
+
f"[red bold]Delete deposition {deposition_id}?[/red bold] [dim]This cannot be undone.[/dim]",
|
|
36
|
+
console=console,
|
|
37
|
+
default=False,
|
|
38
|
+
):
|
|
39
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
40
|
+
raise typer.Exit(0)
|
|
41
|
+
|
|
42
|
+
client = get_zenodo_client(sandbox=sandbox, token=token)
|
|
43
|
+
|
|
44
|
+
console.print(f"[bold blue]Deleting deposition {deposition_id}...[/bold blue]")
|
|
45
|
+
try:
|
|
46
|
+
client.delete_deposition(deposition_id)
|
|
47
|
+
console.print("[green]✓ Deposition deleted[/green]")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
console.print(f"[red]✗ Failed to delete: {e}[/red]")
|
|
50
|
+
logger.error("Delete failed: %s", e)
|
|
51
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""CLI for downloading Zenodo repository files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def download_command(
|
|
12
|
+
deposition_id: Annotated[str | None, typer.Argument(help="Deposition ID")] = None,
|
|
13
|
+
filename: Annotated[str | None, typer.Option("--file", help="Filename to download")] = None,
|
|
14
|
+
output: Annotated[Path | None, typer.Option("--output", help="Output file path")] = None,
|
|
15
|
+
sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
|
|
16
|
+
file_size_mb: Annotated[int | None, typer.Option("--file-size-mb", help="File size in MB (for timeout)")] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Download a file from a published Zenodo record.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
gwsim repository download 10.5281/zenodo.123456 --file data.gwf --output ./data.gwf
|
|
22
|
+
gwsim repository download 10.5281/zenodo.123456 --file metadata.yaml
|
|
23
|
+
"""
|
|
24
|
+
import logging # pylint: disable=import-outside-toplevel
|
|
25
|
+
|
|
26
|
+
from rich.console import Console # pylint: disable=import-outside-toplevel
|
|
27
|
+
|
|
28
|
+
from gwsim.cli.repository.utils import get_zenodo_client # pylint: disable=import-outside-toplevel
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("gwsim")
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
if not deposition_id:
|
|
34
|
+
deposition_id = typer.prompt("Deposition ID (e.g., 123456)")
|
|
35
|
+
if not filename:
|
|
36
|
+
filename = typer.prompt("Filename to download")
|
|
37
|
+
if not output and filename:
|
|
38
|
+
output = Path(filename)
|
|
39
|
+
else:
|
|
40
|
+
console.print("[red]Error:[/red] Output path must be specified.")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
client = get_zenodo_client(sandbox=sandbox, token=None) # Downloads don't require auth
|
|
44
|
+
|
|
45
|
+
console.print(f"[bold blue]Downloading {filename} from {deposition_id}...[/bold blue]")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
client.download_file(str(deposition_id), filename, output, file_size_in_mb=file_size_mb)
|
|
49
|
+
console.print("[green]✓ Downloaded successfully[/green]")
|
|
50
|
+
console.print(f" [cyan]Saved to:[/cyan] {output.resolve()}")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
console.print(f"[red]✗ Download failed: {e}[/red]")
|
|
53
|
+
logger.error("Download failed: %s", e)
|
|
54
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""CLI for managing Zenodo repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_depositions_command( # pylint: disable=import-outside-toplevel,too-many-locals
|
|
11
|
+
status: Annotated[
|
|
12
|
+
str, typer.Option("--status", help="Filter by status (draft, published, unsubmitted)")
|
|
13
|
+
] = "published",
|
|
14
|
+
sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
|
|
15
|
+
token: Annotated[str | None, typer.Option("--token", help="Zenodo access token")] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""List depositions for the authenticated user.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
gwsim repository list
|
|
21
|
+
gwsim repository list --status draft
|
|
22
|
+
gwsim repository list --status published --sandbox
|
|
23
|
+
"""
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
from rich.console import Console
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
|
|
29
|
+
from gwsim.cli.repository.utils import get_zenodo_client
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("gwsim")
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
client = get_zenodo_client(sandbox=sandbox, token=token)
|
|
35
|
+
|
|
36
|
+
console.print(f"[bold blue]Listing {status} depositions...[/bold blue]")
|
|
37
|
+
try:
|
|
38
|
+
depositions = client.list_depositions(status=status)
|
|
39
|
+
|
|
40
|
+
if not depositions:
|
|
41
|
+
console.print(f"[yellow]No {status} depositions found.[/yellow]")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
table = Table(title=f"{status.capitalize()} Depositions")
|
|
45
|
+
table.add_column("ID", style="cyan", width=12)
|
|
46
|
+
table.add_column("Title", style="green", width=40)
|
|
47
|
+
table.add_column("DOI", style="blue", width=20)
|
|
48
|
+
table.add_column("Created", style="magenta", width=12)
|
|
49
|
+
|
|
50
|
+
for dep in depositions:
|
|
51
|
+
dep_id = str(dep.get("id", "N/A"))
|
|
52
|
+
title = dep.get("metadata", {}).get("title", "N/A")
|
|
53
|
+
if len(title) > 38:
|
|
54
|
+
title = title[:35] + "..."
|
|
55
|
+
doi = dep.get("doi", "N/A")
|
|
56
|
+
created = dep.get("created", "N/A")[:10]
|
|
57
|
+
table.add_row(dep_id, title, doi, created)
|
|
58
|
+
|
|
59
|
+
console.print(table)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
console.print(f"[red]✗ Failed to list depositions: {e}[/red]")
|
|
62
|
+
logger.error("List depositions failed: %s", e)
|
|
63
|
+
raise typer.Exit(1) from e
|