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.
Files changed (103) hide show
  1. gwsim/__init__.py +11 -0
  2. gwsim/__main__.py +8 -0
  3. gwsim/cli/__init__.py +0 -0
  4. gwsim/cli/config.py +88 -0
  5. gwsim/cli/default_config.py +56 -0
  6. gwsim/cli/main.py +101 -0
  7. gwsim/cli/merge.py +150 -0
  8. gwsim/cli/repository/__init__.py +0 -0
  9. gwsim/cli/repository/create.py +91 -0
  10. gwsim/cli/repository/delete.py +51 -0
  11. gwsim/cli/repository/download.py +54 -0
  12. gwsim/cli/repository/list_depositions.py +63 -0
  13. gwsim/cli/repository/main.py +38 -0
  14. gwsim/cli/repository/metadata/__init__.py +0 -0
  15. gwsim/cli/repository/metadata/main.py +24 -0
  16. gwsim/cli/repository/metadata/update.py +58 -0
  17. gwsim/cli/repository/publish.py +52 -0
  18. gwsim/cli/repository/upload.py +74 -0
  19. gwsim/cli/repository/utils.py +47 -0
  20. gwsim/cli/repository/verify.py +61 -0
  21. gwsim/cli/simulate.py +220 -0
  22. gwsim/cli/simulate_utils.py +596 -0
  23. gwsim/cli/utils/__init__.py +85 -0
  24. gwsim/cli/utils/checkpoint.py +178 -0
  25. gwsim/cli/utils/config.py +347 -0
  26. gwsim/cli/utils/hash.py +23 -0
  27. gwsim/cli/utils/retry.py +62 -0
  28. gwsim/cli/utils/simulation_plan.py +439 -0
  29. gwsim/cli/utils/template.py +56 -0
  30. gwsim/cli/utils/utils.py +149 -0
  31. gwsim/cli/validate.py +255 -0
  32. gwsim/data/__init__.py +8 -0
  33. gwsim/data/serialize/__init__.py +9 -0
  34. gwsim/data/serialize/decoder.py +59 -0
  35. gwsim/data/serialize/encoder.py +44 -0
  36. gwsim/data/serialize/serializable.py +33 -0
  37. gwsim/data/time_series/__init__.py +3 -0
  38. gwsim/data/time_series/inject.py +104 -0
  39. gwsim/data/time_series/time_series.py +355 -0
  40. gwsim/data/time_series/time_series_list.py +182 -0
  41. gwsim/detector/__init__.py +8 -0
  42. gwsim/detector/base.py +156 -0
  43. gwsim/detector/detectors/E1_2L_Aligned_Sardinia.interferometer +22 -0
  44. gwsim/detector/detectors/E1_2L_Misaligned_Sardinia.interferometer +22 -0
  45. gwsim/detector/detectors/E1_Triangle_EMR.interferometer +19 -0
  46. gwsim/detector/detectors/E1_Triangle_Sardinia.interferometer +19 -0
  47. gwsim/detector/detectors/E2_2L_Aligned_EMR.interferometer +22 -0
  48. gwsim/detector/detectors/E2_2L_Misaligned_EMR.interferometer +22 -0
  49. gwsim/detector/detectors/E2_Triangle_EMR.interferometer +19 -0
  50. gwsim/detector/detectors/E2_Triangle_Sardinia.interferometer +19 -0
  51. gwsim/detector/detectors/E3_Triangle_EMR.interferometer +19 -0
  52. gwsim/detector/detectors/E3_Triangle_Sardinia.interferometer +19 -0
  53. gwsim/detector/noise_curves/ET_10_HF_psd.txt +3000 -0
  54. gwsim/detector/noise_curves/ET_10_full_cryo_psd.txt +3000 -0
  55. gwsim/detector/noise_curves/ET_15_HF_psd.txt +3000 -0
  56. gwsim/detector/noise_curves/ET_15_full_cryo_psd.txt +3000 -0
  57. gwsim/detector/noise_curves/ET_20_HF_psd.txt +3000 -0
  58. gwsim/detector/noise_curves/ET_20_full_cryo_psd.txt +3000 -0
  59. gwsim/detector/noise_curves/ET_D_psd.txt +3000 -0
  60. gwsim/detector/utils.py +90 -0
  61. gwsim/glitch/__init__.py +7 -0
  62. gwsim/glitch/base.py +69 -0
  63. gwsim/mixin/__init__.py +8 -0
  64. gwsim/mixin/detector.py +203 -0
  65. gwsim/mixin/gwf.py +192 -0
  66. gwsim/mixin/population_reader.py +175 -0
  67. gwsim/mixin/randomness.py +107 -0
  68. gwsim/mixin/time_series.py +295 -0
  69. gwsim/mixin/waveform.py +47 -0
  70. gwsim/noise/__init__.py +19 -0
  71. gwsim/noise/base.py +134 -0
  72. gwsim/noise/bilby_stationary_gaussian.py +117 -0
  73. gwsim/noise/colored_noise.py +275 -0
  74. gwsim/noise/correlated_noise.py +257 -0
  75. gwsim/noise/pycbc_stationary_gaussian.py +112 -0
  76. gwsim/noise/stationary_gaussian.py +44 -0
  77. gwsim/noise/white_noise.py +51 -0
  78. gwsim/repository/__init__.py +0 -0
  79. gwsim/repository/zenodo.py +269 -0
  80. gwsim/signal/__init__.py +11 -0
  81. gwsim/signal/base.py +137 -0
  82. gwsim/signal/cbc.py +61 -0
  83. gwsim/simulator/__init__.py +7 -0
  84. gwsim/simulator/base.py +315 -0
  85. gwsim/simulator/state.py +85 -0
  86. gwsim/utils/__init__.py +11 -0
  87. gwsim/utils/datetime_parser.py +44 -0
  88. gwsim/utils/et_2l_geometry.py +165 -0
  89. gwsim/utils/io.py +167 -0
  90. gwsim/utils/log.py +145 -0
  91. gwsim/utils/population.py +48 -0
  92. gwsim/utils/random.py +69 -0
  93. gwsim/utils/retry.py +75 -0
  94. gwsim/utils/triangular_et_geometry.py +164 -0
  95. gwsim/version.py +7 -0
  96. gwsim/waveform/__init__.py +7 -0
  97. gwsim/waveform/factory.py +83 -0
  98. gwsim/waveform/pycbc_wrapper.py +37 -0
  99. gwsim-0.1.0.dist-info/METADATA +157 -0
  100. gwsim-0.1.0.dist-info/RECORD +103 -0
  101. gwsim-0.1.0.dist-info/WHEEL +4 -0
  102. gwsim-0.1.0.dist-info/entry_points.txt +2 -0
  103. gwsim-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,38 @@
1
+ """CLI for managing Zenodo repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ # Create the repository subcommand app
8
+ repository_app = typer.Typer(
9
+ name="repository",
10
+ help="Manage Zenodo repositories for simulation data.",
11
+ rich_markup_mode="rich",
12
+ )
13
+
14
+
15
+ # Import and register commands after app is created
16
+ def register_commands() -> None:
17
+ """Register all CLI commands."""
18
+
19
+ from gwsim.cli.repository.create import create_command # pylint: disable=import-outside-toplevel
20
+ from gwsim.cli.repository.delete import delete_command # pylint: disable=import-outside-toplevel
21
+ from gwsim.cli.repository.download import download_command # pylint: disable=import-outside-toplevel
22
+ from gwsim.cli.repository.list_depositions import ( # pylint: disable=import-outside-toplevel
23
+ list_depositions_command,
24
+ )
25
+ from gwsim.cli.repository.metadata.main import metadata_app # pylint: disable=import-outside-toplevel
26
+ from gwsim.cli.repository.upload import upload_command # pylint: disable=import-outside-toplevel
27
+ from gwsim.cli.repository.verify import verify_command # pylint: disable=import-outside-toplevel
28
+
29
+ repository_app.command("create")(create_command)
30
+ repository_app.command("upload")(upload_command)
31
+ repository_app.command("download")(download_command)
32
+ repository_app.command("list")(list_depositions_command)
33
+ repository_app.command("delete")(delete_command)
34
+ repository_app.command("verify")(verify_command)
35
+ repository_app.add_typer(metadata_app, name="metadata", help="Manage Zenodo metadata")
36
+
37
+
38
+ register_commands()
File without changes
@@ -0,0 +1,24 @@
1
+ """CLI for managing Zenodo metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ # Create the metadata subcommand app
8
+ metadata_app = typer.Typer(
9
+ name="metadata",
10
+ help="Manage Zenodo metadata for simulation data.",
11
+ rich_markup_mode="rich",
12
+ )
13
+
14
+
15
+ # Import and register commands after app is created
16
+ def register_commands() -> None:
17
+ """Register all CLI commands."""
18
+
19
+ from gwsim.cli.repository.metadata.update import update_command # pylint: disable=import-outside-toplevel
20
+
21
+ metadata_app.command("update")(update_command)
22
+
23
+
24
+ register_commands()
@@ -0,0 +1,58 @@
1
+ """CLI for updating 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 update_command(
12
+ deposition_id: Annotated[str, typer.Argument(help="Deposition ID")],
13
+ metadata_file: Annotated[
14
+ Path | None, typer.Option("--metadata-file", help="YAML file with metadata to update")
15
+ ] = None,
16
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Use sandbox environment")] = False,
17
+ token: Annotated[str | None, typer.Option("--token", help="Zenodo access token")] = None,
18
+ ) -> None:
19
+ """Update metadata for a deposition.
20
+
21
+ Examples:
22
+ gwsim repository update 123456 --metadata-file metadata.yaml
23
+ """
24
+ import logging # pylint: disable=import-outside-toplevel
25
+
26
+ import yaml # pylint: disable=import-outside-toplevel
27
+ from rich.console import Console # 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 metadata_file:
35
+ metadata_file = Path(typer.prompt("Path to metadata YAML file"))
36
+
37
+ if not metadata_file.exists():
38
+ console.print(f"[red]Error:[/red] File not found: {metadata_file}")
39
+ raise typer.Exit(1)
40
+
41
+ with metadata_file.open("r", encoding="utf-8") as f:
42
+ metadata = yaml.safe_load(f)
43
+
44
+ if not metadata:
45
+ console.print("[red]Error:[/red] Metadata file is empty.")
46
+ raise typer.Exit(1)
47
+
48
+ client = get_zenodo_client(sandbox=sandbox, token=token)
49
+
50
+ console.print(f"[bold blue]Updating metadata for deposition {deposition_id}...[/bold blue]")
51
+ try:
52
+ client.update_metadata(deposition_id, metadata)
53
+ console.print("[green]✓ Metadata updated successfully[/green]")
54
+ console.print(f"[cyan]Next:[/cyan] gwsim repository publish {deposition_id}")
55
+ except Exception as e:
56
+ console.print(f"[red]✗ Failed to update metadata: {e}[/red]")
57
+ logger.error("Update metadata failed: %s", e)
58
+ raise typer.Exit(1) from e
@@ -0,0 +1,52 @@
1
+ """CLI for publishing Zenodo repository depositions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+
10
+ def publish( # pylint: disable=import-outside-toplevel
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
+ ) -> None:
15
+ """Publish a deposition to Zenodo.
16
+
17
+ Warning: Publishing is permanent and cannot be undone.
18
+
19
+ Examples:
20
+ gwsim repository publish 123456
21
+ """
22
+ import logging
23
+
24
+ from rich.console import Console
25
+
26
+ from gwsim.cli.repository.utils import get_zenodo_client
27
+
28
+ logger = logging.getLogger("gwsim")
29
+ console = Console()
30
+
31
+ if not typer.confirm(
32
+ f"[yellow]Publish deposition {deposition_id}?[/yellow] This action is permanent and cannot be undone."
33
+ ):
34
+ console.print("[yellow]Cancelled.[/yellow]")
35
+ raise typer.Exit(0)
36
+
37
+ client = get_zenodo_client(sandbox=sandbox, token=token)
38
+
39
+ console.print(f"[bold blue]Publishing deposition {deposition_id}...[/bold blue]")
40
+ try:
41
+ result = client.publish_deposition(deposition_id)
42
+ doi = result.get("doi")
43
+ console.print("[green]✓ Published successfully![/green]")
44
+ console.print(f" [cyan]DOI:[/cyan] {doi}")
45
+ if sandbox:
46
+ console.print(
47
+ "[yellow]Note:[/yellow] This is a sandbox record. Use [bold]--sandbox[/bold] to access it later."
48
+ )
49
+ except Exception as e:
50
+ console.print(f"[red]✗ Failed to publish: {e}[/red]")
51
+ logger.error("Publish failed: %s", e)
52
+ raise typer.Exit(1) from e
@@ -0,0 +1,74 @@
1
+ """CLI for uploading files to Zenodo 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 upload_command( # pylint: disable=import-outside-toplevel,too-many-locals
12
+ deposition_id: Annotated[str, typer.Argument(help="Deposition ID")],
13
+ files: Annotated[list[str] | None, typer.Option("--file", help="Files to upload (repeat for multiple)")] = None,
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
+ """Upload files to a deposition.
18
+
19
+ Examples:
20
+ # Single file
21
+ gwsim repository upload 123456 --file data.gwf
22
+
23
+ # Multiple files
24
+ gwsim repository upload 123456 --file data1.gwf --file data2.gwf --file metadata.yaml
25
+ """
26
+ import logging
27
+
28
+ from rich.console import Console
29
+ from rich.progress import Progress
30
+
31
+ from gwsim.cli.repository.utils import get_zenodo_client
32
+
33
+ logger = logging.getLogger("gwsim")
34
+ console = Console()
35
+
36
+ if not files:
37
+ console.print("[red]Error:[/red] No files specified. Use [bold]--file <path>[/bold] to specify files.")
38
+ raise typer.Exit(1)
39
+
40
+ client = get_zenodo_client(sandbox=sandbox, token=token)
41
+
42
+ console.print(f"[bold blue]Uploading {len(files)} file(s) to deposition {deposition_id}...[/bold blue]")
43
+
44
+ failed_count = 0
45
+ with Progress() as progress:
46
+ task = progress.add_task("Uploading", total=len(files))
47
+
48
+ for file_path_str in files:
49
+ file_path = Path(file_path_str)
50
+ if not file_path.exists():
51
+ console.print(f"[red]✗ File not found:[/red] {file_path}")
52
+ failed_count += 1
53
+ progress.update(task, advance=1)
54
+ continue
55
+
56
+ try:
57
+ file_size_mb = file_path.stat().st_size / (1024 * 1024)
58
+ client.upload_file(deposition_id, file_path, auto_timeout=True)
59
+ console.print(f"[green]✓ {file_path.name}[/green] ({file_size_mb:.2f} MB)")
60
+ progress.update(task, advance=1)
61
+ except Exception as e: # pylint: disable=broad-exception-caught
62
+ console.print(f"[red]✗ Failed to upload {file_path.name}:[/red] {e}")
63
+ logger.error("Upload failed for %s: %s", file_path, e)
64
+ failed_count += 1
65
+ progress.update(task, advance=1)
66
+
67
+ if failed_count == 0:
68
+ if sandbox:
69
+ console.print("[cyan]Next:[/cyan] gwsim repository update <id> --metadata-file <file> --sandbox")
70
+ else:
71
+ console.print("[cyan]Next:[/cyan] gwsim repository update <id> --metadata-file <file>")
72
+ else:
73
+ console.print(f"[yellow]Warning:[/yellow] {failed_count} file(s) failed to upload.")
74
+ raise typer.Exit(1)
@@ -0,0 +1,47 @@
1
+ """Utility functions for repository CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from gwsim.repository.zenodo import ZenodoClient
11
+
12
+ console = Console()
13
+
14
+
15
+ def get_zenodo_client(sandbox: bool = False, token: str | None = None) -> ZenodoClient:
16
+ """Get a ZenodoClient instance with token from env or argument.
17
+
18
+ Args:
19
+ sandbox: Use sandbox environment for testing.
20
+ token: Access token (defaults to ZENODO_TOKEN env var).
21
+
22
+ Returns:
23
+ Configured ZenodoClient.
24
+
25
+ Raises:
26
+ typer.Exit: If no token is provided or found in environment.
27
+ """
28
+ if token is None:
29
+ if sandbox:
30
+ token = os.environ.get("ZENODO_SANDBOX_API_TOKEN")
31
+ else:
32
+ token = os.environ.get("ZENODO_API_TOKEN")
33
+ if not token:
34
+ if sandbox:
35
+ console.print(
36
+ "[red]Error:[/red] No Zenodo Sandbox access token provided.\n"
37
+ "Set [bold]ZENODO_SANDBOX_API_TOKEN[/bold] environment variable or use [bold]--token[/bold] option.\n"
38
+ "Get a token from: https://sandbox.zenodo.org/account/settings/applications/tokens/new"
39
+ )
40
+ else:
41
+ console.print(
42
+ "[red]Error:[/red] No Zenodo access token provided.\n"
43
+ "Set [bold]ZENODO_API_TOKEN[/bold] environment variable or use [bold]--token[/bold] option.\n"
44
+ "Get a token from: https://zenodo.org/account/settings/applications/tokens/new"
45
+ )
46
+ raise typer.Exit(1)
47
+ return ZenodoClient(access_token=token, sandbox=sandbox)
@@ -0,0 +1,61 @@
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 verify_command( # pylint: disable=import-outside-toplevel
11
+ sandbox: Annotated[bool, typer.Option("--sandbox", help="Verify sandbox token")] = False,
12
+ token: Annotated[str | None, typer.Option("--token", help="Zenodo access token to verify")] = None,
13
+ ) -> None:
14
+ """Verify that your Zenodo API token is valid and has the correct permissions.
15
+
16
+ Examples:
17
+ # Verify production token
18
+ gwsim repository verify
19
+
20
+ # Verify sandbox token
21
+ gwsim repository verify --sandbox
22
+
23
+ # Verify with explicit token
24
+ gwsim repository verify --token your_token_here
25
+ """
26
+ from rich.console import Console
27
+
28
+ from gwsim.cli.repository.utils import get_zenodo_client
29
+
30
+ console = Console()
31
+
32
+ console.print("[bold blue]Verifying Zenodo API token...[/bold blue]")
33
+
34
+ try:
35
+ client = get_zenodo_client(sandbox=sandbox, token=token)
36
+
37
+ # Try to list depositions as a test
38
+ console.print("Testing API access...")
39
+ depositions = client.list_depositions(status="draft")
40
+
41
+ env_name = "Zenodo Sandbox" if sandbox else "Zenodo (Production)"
42
+ console.print("[green]✓ Token is valid![/green]")
43
+ console.print(f" [cyan]Environment:[/cyan] {env_name}")
44
+ console.print(f" [cyan]Found {len(depositions)} draft deposition(s)[/cyan]")
45
+
46
+ except Exception as e:
47
+ env_name = "Zenodo Sandbox" if sandbox else "Zenodo (Production)"
48
+ console.print(f"[red]✗ Token verification failed for {env_name}[/red]")
49
+ console.print(f" [yellow]Error:[/yellow] {e}")
50
+ console.print("\n[bold]Troubleshooting:[/bold]")
51
+ if sandbox:
52
+ console.print(
53
+ " 1. Get a new token from: https://sandbox.zenodo.org/account/settings/applications/tokens/new"
54
+ )
55
+ console.print(" 2. Ensure the token has 'deposit:write' and 'deposit:actions' scopes")
56
+ console.print(" 3. Set: export ZENODO_SANDBOX_API_TOKEN='your_token'")
57
+ else:
58
+ console.print(" 1. Get a new token from: https://zenodo.org/account/settings/applications/tokens/new")
59
+ console.print(" 2. Ensure the token has 'deposit:write' and 'deposit:actions' scopes")
60
+ console.print(" 3. Set: export ZENODO_API_TOKEN='your_token'")
61
+ raise typer.Exit(1) from e
gwsim/cli/simulate.py ADDED
@@ -0,0 +1,220 @@
1
+ """
2
+ A sub-command to handle data generation using simulation plans.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+
12
+ def _simulate_impl( # pylint: disable=too-many-locals, too-many-branches, too-many-statements
13
+ config_file_names: str | list[str],
14
+ output_dir: str | None = None,
15
+ metadata_dir: str | None = None,
16
+ overwrite: bool = False,
17
+ metadata: bool = True,
18
+ author: str | None = None,
19
+ email: str | None = None,
20
+ ) -> None:
21
+ """Internal implementation of simulate command that accepts both str and list[str].
22
+
23
+ Unified approach: All simulation goes through the same plan execution system.
24
+ The difference is only in how we create the initial plan:
25
+ - Config file → SimulationPlan with pre_batch_state=None (fresh simulation)
26
+ - Metadata files → SimulationPlan with pre_batch_state=<dict> (reproduce)
27
+
28
+ Both cases end up calling execute_plan with batches that may or may not have
29
+ state snapshots. The execute_plan function handles both transparently.
30
+
31
+ Args:
32
+ config_file_names: Path to YAML config file OR one or more metadata files
33
+ output_dir: Output directory override
34
+ metadata_dir: Metadata directory override (config mode only)
35
+ overwrite: Whether to overwrite existing files
36
+ metadata: Whether to save metadata files (config mode only)
37
+ author: Author name for metadata
38
+ email: Author email for metadata
39
+
40
+ Returns:
41
+ None
42
+ """
43
+ import logging # pylint: disable=import-outside-toplevel
44
+ from pathlib import Path # pylint: disable=import-outside-toplevel
45
+
46
+ from gwsim.cli.simulate_utils import ( # pylint: disable=import-outside-toplevel
47
+ execute_plan,
48
+ validate_plan,
49
+ )
50
+ from gwsim.cli.utils.config import load_config # pylint: disable=import-outside-toplevel
51
+ from gwsim.cli.utils.simulation_plan import ( # pylint: disable=import-outside-toplevel
52
+ create_plan_from_config,
53
+ create_plan_from_metadata_files,
54
+ )
55
+
56
+ logger = logging.getLogger("gwsim")
57
+ logger.setLevel(logging.DEBUG)
58
+
59
+ checkpoint_dir = Path(".gwsim_checkpoints")
60
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ try:
63
+ # ===== Normalize input: accept both string and list =====
64
+ if isinstance(config_file_names, str):
65
+ config_file_names_list = [config_file_names]
66
+ else:
67
+ config_file_names_list = config_file_names
68
+
69
+ # ===== Auto-detect mode: Config file vs Metadata files =====
70
+ if len(config_file_names_list) == 1:
71
+ # Single argument: could be YAML config or metadata file
72
+ single_path = Path(config_file_names_list[0])
73
+ is_metadata = (
74
+ single_path.suffix == ".metadata.yaml"
75
+ or (single_path.is_file() and "metadata" in single_path.name)
76
+ or single_path.is_dir() # Directory assumed to be metadata dir
77
+ )
78
+ else:
79
+ # Multiple arguments: must be metadata files
80
+ is_metadata = True
81
+
82
+ # ===== Create plan (unified approach: both modes create same data structure) =====
83
+ if is_metadata:
84
+ logger.info("Reproduction mode: %d metadata file(s)", len(config_file_names_list))
85
+
86
+ metadata_paths = [Path(f) for f in config_file_names_list]
87
+
88
+ # If single directory, load all metadata files from it
89
+ if len(metadata_paths) == 1 and metadata_paths[0].is_dir():
90
+ metadata_dir_path = metadata_paths[0]
91
+ metadata_files = list(metadata_dir_path.glob("*.metadata.yaml"))
92
+ if not metadata_files:
93
+ raise ValueError(f"No metadata files found in directory: {metadata_dir_path}")
94
+ logger.info("Found %d metadata files in directory: %s", len(metadata_files), metadata_dir_path)
95
+ plan = create_plan_from_metadata_files(metadata_files, checkpoint_dir, author=author, email=email)
96
+ else:
97
+ # Individual metadata files
98
+ plan = create_plan_from_metadata_files(metadata_paths, checkpoint_dir, author=author, email=email)
99
+
100
+ logger.info(
101
+ "Created reproduction plan from %d metadata file(s) with %d batches",
102
+ len(config_file_names_list),
103
+ plan.total_batches,
104
+ )
105
+ else:
106
+ logger.info("Config mode: %s", config_file_names_list[0])
107
+ config_path = Path(config_file_names_list[0])
108
+ config = load_config(file_name=config_path)
109
+ logger.debug("Configuration loaded successfully from %s", config_file_names_list[0])
110
+
111
+ plan = create_plan_from_config(config, checkpoint_dir, author=author, email=email)
112
+ logger.info("Created simulation plan with %d batches", plan.total_batches)
113
+
114
+ # ===== Determine output directories (same logic for both modes) =====
115
+ # Get reference config from first batch
116
+ if not plan.batches:
117
+ raise ValueError("No batches found in simulation plan")
118
+
119
+ first_batch = plan.batches[0]
120
+ globals_cfg = first_batch.globals_config
121
+ working_dir = Path(globals_cfg.working_directory or ".") # pylint: disable=no-member
122
+ output_dir_config = globals_cfg.output_directory or "output" # pylint: disable=no-member
123
+
124
+ # Handle absolute vs relative paths
125
+ config_output_dir = (
126
+ Path(output_dir_config) if Path(output_dir_config).is_absolute() else working_dir / output_dir_config
127
+ )
128
+ final_output_dir = Path(output_dir) if output_dir else config_output_dir
129
+
130
+ # Metadata directory only used in config mode (fresh simulation)
131
+ final_metadata_dir = None
132
+ if not is_metadata and metadata:
133
+ metadata_dir_config = globals_cfg.metadata_directory or "metadata" # pylint: disable=no-member
134
+ config_metadata_dir = (
135
+ Path(metadata_dir_config)
136
+ if Path(metadata_dir_config).is_absolute()
137
+ else working_dir / metadata_dir_config
138
+ )
139
+ final_metadata_dir = Path(metadata_dir) if metadata_dir else config_metadata_dir
140
+
141
+ logger.debug("Output directory: %s", final_output_dir)
142
+ if final_metadata_dir:
143
+ logger.debug("Metadata directory: %s", final_metadata_dir)
144
+
145
+ # ===== Validate and execute plan =====
146
+ validate_plan(plan)
147
+ logger.info("Simulation plan validation passed")
148
+
149
+ execute_plan(
150
+ plan=plan,
151
+ output_directory=final_output_dir,
152
+ metadata_directory=final_metadata_dir or Path("metadata"),
153
+ overwrite=overwrite,
154
+ max_retries=3,
155
+ )
156
+
157
+ logger.info("Simulation completed successfully. Output written to %s", final_output_dir)
158
+
159
+ except Exception as e:
160
+ logger.error("Simulation failed: %s", str(e), exc_info=True)
161
+ raise typer.Exit(code=1) from e
162
+
163
+
164
+ def simulate_command(
165
+ config_file_names: Annotated[
166
+ list[str],
167
+ typer.Argument(help="Configuration file (YAML) or metadata files (can specify multiple metadata files)"),
168
+ ],
169
+ output_dir: Annotated[
170
+ str | None, typer.Option("--output-dir", help="Output directory (overrides config/metadata defaults)")
171
+ ] = None,
172
+ metadata_dir: Annotated[
173
+ str | None,
174
+ typer.Option(
175
+ "--metadata-dir", help="Metadata directory (overrides config/metadata defaults, only for config mode)"
176
+ ),
177
+ ] = None,
178
+ overwrite: Annotated[bool, typer.Option("--overwrite", help="Overwrite existing files")] = False,
179
+ metadata: Annotated[bool, typer.Option("--metadata", help="Generate metadata files (only in config mode)")] = True,
180
+ author: Annotated[str | None, typer.Option("--author", help="Author name for the simulation")] = None,
181
+ email: Annotated[str | None, typer.Option("--email", help="Author email for the simulation")] = None,
182
+ ) -> None:
183
+ """Generate gravitational wave simulation data using specified simulators.
184
+
185
+ This command can run in two modes, automatically detected from the input:
186
+
187
+ 1. **Config Mode**: Provide a single YAML configuration file to create a simulation plan.
188
+ Executes all simulators with state tracking for reproducibility.
189
+ Example: `gwsim simulate config.yaml`
190
+
191
+ 2. **Reproduction Mode**: Provide one or more metadata files to reproduce specific batches.
192
+ Each metadata file contains the exact configuration and pre-batch state needed for
193
+ exact reproducibility. Users can distribute individual metadata files, and anyone
194
+ can reproduce those specific batches independently.
195
+ Example: `gwsim simulate signal-0.metadata.yaml signal-1.metadata.yaml`
196
+
197
+ Path Overrides:
198
+ - Use `--output-dir` to specify where output files should be saved
199
+ - Use `--metadata-dir` to specify where metadata should be saved (config mode only)
200
+ - These override paths from the configuration or metadata files
201
+
202
+ Args:
203
+ config_file_names: Path to YAML config file OR one or more metadata files
204
+ output_dir: Output directory override
205
+ metadata_dir: Metadata directory override (config mode only)
206
+ overwrite: Whether to overwrite existing files
207
+ metadata: Whether to save metadata files (config mode only)
208
+
209
+ Returns:
210
+ None
211
+ """
212
+ _simulate_impl(
213
+ config_file_names=config_file_names,
214
+ output_dir=output_dir,
215
+ metadata_dir=metadata_dir,
216
+ overwrite=overwrite,
217
+ metadata=metadata,
218
+ author=author,
219
+ email=email,
220
+ )