JimGW 0.4.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.
Files changed (48) hide show
  1. jimgw/__init__.py +20 -0
  2. jimgw/_logging.py +19 -0
  3. jimgw/cli/__init__.py +267 -0
  4. jimgw/cli/_config.py +651 -0
  5. jimgw/cli/_data.py +135 -0
  6. jimgw/cli/_jim.py +52 -0
  7. jimgw/cli/_likelihood.py +232 -0
  8. jimgw/cli/_output.py +184 -0
  9. jimgw/cli/_prior.py +131 -0
  10. jimgw/cli/_transforms.py +445 -0
  11. jimgw/cli/_utils.py +26 -0
  12. jimgw/cli/_waveform.py +40 -0
  13. jimgw/core/__init__.py +1 -0
  14. jimgw/core/base.py +50 -0
  15. jimgw/core/constants.py +32 -0
  16. jimgw/core/jim.py +535 -0
  17. jimgw/core/prior.py +728 -0
  18. jimgw/core/single_event/__init__.py +0 -0
  19. jimgw/core/single_event/data.py +804 -0
  20. jimgw/core/single_event/detector.py +913 -0
  21. jimgw/core/single_event/likelihood.py +1752 -0
  22. jimgw/core/single_event/marginalization_config.py +29 -0
  23. jimgw/core/single_event/polarization.py +88 -0
  24. jimgw/core/single_event/prior.py +33 -0
  25. jimgw/core/single_event/time_utils.py +271 -0
  26. jimgw/core/single_event/transform_utils.py +653 -0
  27. jimgw/core/single_event/transforms.py +649 -0
  28. jimgw/core/single_event/utils.py +121 -0
  29. jimgw/core/single_event/waveform.py +39 -0
  30. jimgw/core/transforms.py +882 -0
  31. jimgw/core/utils.py +111 -0
  32. jimgw/samplers/__init__.py +137 -0
  33. jimgw/samplers/base.py +121 -0
  34. jimgw/samplers/blackjax/__init__.py +5 -0
  35. jimgw/samplers/blackjax/_acceptance_walk_kernel.py +391 -0
  36. jimgw/samplers/blackjax/_imports.py +31 -0
  37. jimgw/samplers/blackjax/ns_aw.py +309 -0
  38. jimgw/samplers/blackjax/nss.py +300 -0
  39. jimgw/samplers/blackjax/smc.py +881 -0
  40. jimgw/samplers/config.py +466 -0
  41. jimgw/samplers/flowmc.py +326 -0
  42. jimgw/samplers/periodic.py +140 -0
  43. jimgw/typing.py +10 -0
  44. jimgw-0.4.1.dist-info/METADATA +135 -0
  45. jimgw-0.4.1.dist-info/RECORD +48 -0
  46. jimgw-0.4.1.dist-info/WHEEL +4 -0
  47. jimgw-0.4.1.dist-info/entry_points.txt +2 -0
  48. jimgw-0.4.1.dist-info/licenses/LICENSE +22 -0
jimgw/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ from jimgw._logging import LOG_FORMAT
6
+
7
+ try:
8
+ __version__ = version("jimgw")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
11
+
12
+ # propagate=False isolates jimgw from the root logger to avoid duplicates
13
+ # when the application also configures logging (e.g. via basicConfig).
14
+ _log = logging.getLogger(__name__)
15
+ _log.setLevel(logging.INFO)
16
+ _log.propagate = False
17
+ if not any(isinstance(h, logging.StreamHandler) for h in _log.handlers):
18
+ _h = logging.StreamHandler()
19
+ _h.setFormatter(logging.Formatter(LOG_FORMAT))
20
+ _log.addHandler(_h)
jimgw/_logging.py ADDED
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+ LOG_FORMAT = "%(levelname)s | %(name)s | %(message)s"
4
+
5
+
6
+ def ensure_logger_handler(logger_name: str, level: int) -> None:
7
+ """Add a StreamHandler to a logger if it has none, and set propagate=False.
8
+
9
+ Mirrors the self-contained handler setup that jimgw/__init__.py applies to
10
+ the jimgw logger, so third-party loggers (e.g. flowMC) produce output in
11
+ script/notebook contexts without requiring the user to call basicConfig.
12
+ """
13
+ log = logging.getLogger(logger_name)
14
+ log.setLevel(level)
15
+ log.propagate = False
16
+ if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
17
+ _h = logging.StreamHandler()
18
+ _h.setFormatter(logging.Formatter(LOG_FORMAT))
19
+ log.addHandler(_h)
jimgw/cli/__init__.py ADDED
@@ -0,0 +1,267 @@
1
+ import logging
2
+ import tomllib
3
+ from importlib.metadata import PackageNotFoundError, version
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from pydantic import ValidationError
9
+
10
+ from jimgw._logging import LOG_FORMAT
11
+ from jimgw.cli._config import PipelineConfig
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ app = typer.Typer(
16
+ name="jim-run",
17
+ add_completion=False,
18
+ help="Run a jimgw parameter-estimation pipeline from a TOML config file.",
19
+ )
20
+
21
+ _INIT_TEMPLATE = """\
22
+ seed = 0
23
+
24
+ [data]
25
+ type = "gwosc"
26
+ detectors = ["H1", "L1"]
27
+ trigger_time = 1126259462.4
28
+ duration = 4.0
29
+ post_trigger_duration = 2.0
30
+ psd_duration = 1024.0
31
+
32
+ [waveform]
33
+ approximant = "IMRPhenomXAS"
34
+ f_ref = 20.0
35
+
36
+ [prior]
37
+ M_c = { type = "uniform", min = 10.0, max = 80.0 }
38
+ q = { type = "uniform", min = 0.125, max = 1.0 }
39
+ s1_z = { type = "uniform", min = -0.99, max = 0.99 }
40
+ s2_z = { type = "uniform", min = -0.99, max = 0.99 }
41
+ iota = { type = "sine" }
42
+ d_L = { type = "power_law", min = 1.0, max = 2000.0, alpha = 2.0 }
43
+ t_c = { type = "uniform", min = -0.1, max = 0.1 }
44
+ phase_c = { type = "uniform", min = 0.0, max = 6.283185307179586 } # 2π
45
+ psi = { type = "uniform", min = 0.0, max = 3.141592653589793 } # π
46
+ ra = { type = "uniform", min = 0.0, max = 6.283185307179586 } # 2π
47
+ dec = { type = "cosine" }
48
+
49
+ [likelihood]
50
+ f_min = 20.0
51
+ f_max = 1024.0
52
+
53
+ # Production defaults — for a quick test try: n_chains=100, n_global_steps=100, n_production_loops=2
54
+ [sampler]
55
+ type = "flowmc"
56
+ n_chains = 1000
57
+ n_local_steps = 100
58
+ n_global_steps = 1000
59
+ n_training_loops = 50
60
+ n_production_loops = 10
61
+ n_NFproposal_batch_size = 100
62
+ global_thinning = 100
63
+
64
+ [output]
65
+ dir = "output/my_run"
66
+ # save_corner requires the 'corner' package: pip install corner
67
+ save_corner = false
68
+ """
69
+
70
+
71
+ @app.command()
72
+ def run(
73
+ config: Optional[Path] = typer.Argument(None, help="Path to the TOML config file."),
74
+ init: Optional[Path] = typer.Option(
75
+ None,
76
+ "--init",
77
+ help="Write a minimal GW150914-style template config to PATH and exit.",
78
+ metavar="PATH",
79
+ ),
80
+ verbose: bool = typer.Option(
81
+ False, "--verbose", "-v", help="Enable verbose logging."
82
+ ),
83
+ ) -> None:
84
+ """Run a jimgw parameter-estimation pipeline from CONFIG."""
85
+ level = logging.DEBUG if verbose else logging.INFO
86
+ logging.basicConfig(level=level, format=LOG_FORMAT)
87
+ # jimgw logger is isolated (propagate=False), so set its level directly.
88
+ logging.getLogger("jimgw").setLevel(level)
89
+
90
+ if init is not None:
91
+ if init.exists():
92
+ typer.echo(
93
+ f"Error: {init} already exists. Choose a different path.", err=True
94
+ )
95
+ raise typer.Exit(code=2)
96
+ try:
97
+ init.parent.mkdir(parents=True, exist_ok=True)
98
+ init.write_text(_INIT_TEMPLATE)
99
+ except OSError as exc:
100
+ typer.echo(f"Error: could not write template to {init}: {exc}", err=True)
101
+ raise typer.Exit(code=2) from exc
102
+ typer.echo(f"Template config written to {init}")
103
+ raise typer.Exit()
104
+
105
+ if config is None:
106
+ typer.echo(
107
+ "Error: provide a CONFIG file or use --init to create a template.", err=True
108
+ )
109
+ raise typer.Exit(code=2)
110
+
111
+ if not config.exists():
112
+ typer.echo(f"Error: config file not found: {config}", err=True)
113
+ raise typer.Exit(code=2)
114
+
115
+ try:
116
+ with open(config, "rb") as f:
117
+ raw = tomllib.load(f)
118
+ except (OSError, tomllib.TOMLDecodeError) as exc:
119
+ typer.echo(f"Error reading config {config}:\n{exc}", err=True)
120
+ raise typer.Exit(code=2) from exc
121
+
122
+ logger.info("Loaded config from %s", config)
123
+
124
+ try:
125
+ cfg = PipelineConfig.model_validate(raw)
126
+ except ValidationError as exc:
127
+ typer.echo(f"Config validation error:\n{exc}", err=True)
128
+ raise typer.Exit(code=2) from exc
129
+
130
+ _log_config_summary(cfg)
131
+ _log_versions(cfg.sampler.type)
132
+
133
+ out_dir = cfg.output.dir
134
+ if out_dir.exists() and not cfg.output.overwrite:
135
+ typer.echo(
136
+ f"Error: output directory already exists: {out_dir}. "
137
+ "Set output.overwrite = true to allow overwriting.",
138
+ err=True,
139
+ )
140
+ raise typer.Exit(code=2)
141
+
142
+ import jax
143
+
144
+ jax.config.update("jax_enable_x64", True)
145
+
146
+ from jimgw.cli._data import build_data
147
+ from jimgw.cli._jim import build_jim
148
+ from jimgw.cli._likelihood import build_likelihood
149
+ from jimgw.cli._output import write_outputs
150
+ from jimgw.cli._prior import adapt_prior_for_ns_time, build_prior
151
+ from jimgw.cli._transforms import (
152
+ infer_likelihood_transforms,
153
+ infer_sample_transforms,
154
+ )
155
+ from jimgw.cli._waveform import build_waveform
156
+
157
+ trigger_time: float = cfg.data.trigger_time
158
+
159
+ # Stage 2: waveform
160
+ waveform = build_waveform(cfg.waveform)
161
+
162
+ # Stage 3: data — injection runs receive the already-built waveform
163
+ ifos = build_data(
164
+ cfg.data,
165
+ f_min=cfg.likelihood.f_min,
166
+ f_max=cfg.likelihood.f_max,
167
+ waveform=waveform,
168
+ time_frame=cfg.sampling.time_frame,
169
+ )
170
+
171
+ # NS-AW requires all sampling-space parameters in [0, 1].
172
+ # Must run before build_prior so the built prior and
173
+ # prior_params already reflect the substitution.
174
+ if cfg.sampler.type == "blackjax-ns-aw":
175
+ modified_prior = adapt_prior_for_ns_time(cfg.prior, cfg.sampling)
176
+ if modified_prior is not None:
177
+ cfg.prior = modified_prior
178
+
179
+ # Stage 4: prior
180
+ prior = build_prior(cfg.prior)
181
+
182
+ # Stage 5: transform inference
183
+ prior_params = frozenset(prior.parameter_names)
184
+ ns_aw = cfg.sampler.type == "blackjax-ns-aw"
185
+ sample_transforms = infer_sample_transforms(
186
+ prior_params,
187
+ trigger_time,
188
+ ifos,
189
+ cfg.sampling,
190
+ unit_cube=ns_aw,
191
+ prior_cfg=cfg.prior,
192
+ )
193
+ likelihood_transforms = infer_likelihood_transforms(
194
+ prior_params,
195
+ trigger_time,
196
+ ifos,
197
+ cfg.sampling,
198
+ cfg.waveform.f_ref,
199
+ phase_marginalization=cfg.likelihood.phase_marginalization,
200
+ )
201
+
202
+ # Stage 6: likelihood
203
+ likelihood = build_likelihood(
204
+ cfg.likelihood,
205
+ ifos,
206
+ waveform,
207
+ trigger_time,
208
+ cfg.waveform.f_ref,
209
+ prior=prior,
210
+ likelihood_transforms=likelihood_transforms,
211
+ data_cfg=cfg.data,
212
+ time_frame=cfg.sampling.time_frame,
213
+ )
214
+
215
+ # Stage 7: build Jim + run sampler
216
+ jim = build_jim(
217
+ likelihood,
218
+ prior,
219
+ sample_transforms,
220
+ likelihood_transforms,
221
+ cfg,
222
+ verbose=verbose,
223
+ )
224
+
225
+ try:
226
+ jim.sample()
227
+ except Exception as exc:
228
+ logger.error("Sampling failed: %s", exc)
229
+ raise typer.Exit(code=3) from exc
230
+ logger.info("Sampling complete.")
231
+
232
+ # Stage 8: write outputs
233
+ try:
234
+ write_outputs(jim, cfg)
235
+ except FileExistsError as exc:
236
+ typer.echo(f"Error: {exc}", err=True)
237
+ raise typer.Exit(code=2) from exc
238
+
239
+
240
+ def _log_versions(sampler_type: str) -> None:
241
+ dists = ["JimGW", "rippleGW"]
242
+ if sampler_type == "flowmc":
243
+ dists.append("flowMC")
244
+ parts = []
245
+ for dist in dists:
246
+ try:
247
+ parts.append(f"{dist} {version(dist)}")
248
+ except PackageNotFoundError:
249
+ pass
250
+ if parts:
251
+ logger.info(" | ".join(parts))
252
+
253
+
254
+ def _log_config_summary(cfg: PipelineConfig) -> None:
255
+ logger.info("seed: %d", cfg.seed)
256
+ logger.info(
257
+ "data: type=%s, detectors=%s",
258
+ cfg.data.type,
259
+ cfg.data.detectors,
260
+ )
261
+ logger.info(
262
+ "waveform: %s (f_ref=%.1f Hz)", cfg.waveform.approximant, cfg.waveform.f_ref
263
+ )
264
+ param_names = list(cfg.prior.root.keys())
265
+ logger.info("prior: %d parameter(s): %s", len(param_names), param_names)
266
+ logger.info("sampler: type=%s", cfg.sampler.type)
267
+ logger.info("output: %s", cfg.output.dir)