Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dingo/core/utils/trainutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(
self.counter = 0
self.best_score = None
self.early_stop = False
self.val_loss_min = np.Inf
self.val_loss_min = np.inf
self.delta = delta
if metric not in ["training", "validation"]:
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion dingo/gw/gwutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def get_extrinsic_prior_dict(extrinsic_prior):
TODO: Move to dingo.gw.prior.py?"""
extrinsic_prior_dict = default_extrinsic_dict.copy()
for k, v in extrinsic_prior.items():
if v.lower() != "default":
if not isinstance(v, str) or v.lower() != "default":
extrinsic_prior_dict[k] = v
return extrinsic_prior_dict

Expand Down
52 changes: 41 additions & 11 deletions dingo/gw/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,13 +415,18 @@ def sample_calibration_parameters(self, calibration_sampling_kwargs: dict):
self.calibration_sampling_kwargs = calibration_sampling_kwargs

# Handle correction_type defaults
correction_type = self.calibration_sampling_kwargs.get("correction_type", "data")
correction_type = self.calibration_sampling_kwargs.get(
"correction_type", "data"
)
if correction_type is None:
correction_type_dict = {
ifo: CALIBRATION_CORRECTION_TYPE_LOOKUP[ifo] for ifo in self.interferometers
ifo: CALIBRATION_CORRECTION_TYPE_LOOKUP[ifo]
for ifo in self.interferometers
}
elif correction_type == "data" or correction_type == "template":
correction_type_dict = {ifo: correction_type for ifo in self.interferometers}
correction_type_dict = {
ifo: correction_type for ifo in self.interferometers
}
elif isinstance(correction_type, dict):
correction_type_dict = correction_type
else:
Expand All @@ -440,8 +445,8 @@ def sample_calibration_parameters(self, calibration_sampling_kwargs: dict):
)

# Removing the delta function priors on the frequency nodes, amplitude and phase.
# Usually the frequency nodes are set to delta functions, but we also remove the
# the amplitude and phase delta functions if present.
# Usually the frequency nodes are set to delta functions, but we also remove the
# amplitude and phase delta functions if present.
# This avoids large log probs and log priors, since the density of a delta function
# at the sampled point is infinite. The delta functions do not affect the sampling,
# since they just fix certain parameters to constant values.
Expand All @@ -457,9 +462,9 @@ def sample_calibration_parameters(self, calibration_sampling_kwargs: dict):
delta_log_prob = np.zeros(num_samples)

# Here we will sample the calibration parameters from the prior.
# We treat the *prior as the proposal* distribution and
# therefore add the log_prob of the sampled calibration parameters
# to the existing log_prob. We also will update the prior
# We treat the *prior as the proposal* distribution and
# therefore add the log_prob of the sampled calibration parameters
# to the existing log_prob. We also will update the prior
# to include the calibration priors using the importance_sampling_metadata
prior_update = self.importance_sampling_metadata.get("prior_update", {})
for ifo, prior in calibration_priors.items():
Expand All @@ -475,6 +480,12 @@ def sample_calibration_parameters(self, calibration_sampling_kwargs: dict):
# Update prior_update dict with calibration parameters. This is for
# persistence when saving to hdf5
for param_name, prior_obj in prior.items():
# bilby's Prior.__repr__ isn't parseable for numpy scalars on numpy>2.0
# Upstream fix: https://github.com/bilby-dev/bilby/pull/1108
# Can be removed once dingo requires a bilby release that includes it.
for attr, value in prior_obj.get_instantiation_dict().items():
if isinstance(value, np.generic):
setattr(prior_obj, attr, value.item())
prior_update[param_name] = repr(prior_obj)

# Store prior_update for persistence on save/reload
Expand Down Expand Up @@ -560,8 +571,25 @@ def sample_synthetic_phase(
# Restrict to samples that are within the prior.
param_keys = [k for k, v in self.prior.items() if not isinstance(v, Constraint)]
theta = self.samples[param_keys]
log_prior = self.prior.ln_prob(theta, axis=0)
constraints = self.prior.evaluate_constraints(theta)

# Compute log_prior only for non-DeltaFunction parameters. DeltaFunction
# priors return ln_prob = +inf at the peak, which causes check_ln_prob to
# skip constraint evaluation and return +inf for every sample, so
# np.isfinite(log_prior) would be False for all samples. Additionally, RA
# corrections (trigger_time vs model ref_time) can shift fixed parameters by
# tiny amounts, making DeltaFunction ln_prob = -inf for all samples.
prior_keys_for_lp = [
k for k, v in self.prior.items()
if not isinstance(v, Constraint) and not isinstance(v, DeltaFunction)
]
log_prior = self.prior.ln_prob(self.samples[prior_keys_for_lp], axis=0)
# Pass a plain dict so bilby's evaluate_constraints handles the argument
# correctly. bilby's evaluate_constraints mishandles a DataFrame argument:
# its internal .values() call raises TypeError (DataFrame.values is a
# property, not a method), causing the try/except inside bilby to fall
# through to ``np.ones_like(out_sample)``, which returns a 2-D array and
# causes a shape-broadcast error in the subsequent element-wise multiplication.
constraints = self.prior.evaluate_constraints(dict(theta))
np.putmask(log_prior, constraints == 0, -np.inf)
within_prior = np.isfinite(log_prior)

Expand Down Expand Up @@ -690,7 +718,9 @@ def get_samples_bilby_phase(self, num_processes=1):
num_processes=num_processes,
)

def get_pesummary_samples(self, num_processes=1, resampling_method="clip+rejection"):
def get_pesummary_samples(
self, num_processes=1, resampling_method="clip+rejection"
):
"""Samples in a form suitable for PESummary.

These samples are adjusted to undo certain conventions used internally by
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ write_to = "dingo/_version.py"
markers = [
"slow: marks tests as slow",
"asimov: marks tests for Asimov/LIGO pipeline integration",
"heavy: marks heavy end-to-end tests (train + importance sample; GPU; minutes-long). Deselected by default; run with -m heavy.",
]
addopts = "-m 'not asimov'"
addopts = "-m 'not asimov and not heavy'"
pythonpath = ["."]

[project.urls]
homepage = "https://github.com/dingo-gw/dingo"
Expand Down
24 changes: 24 additions & 0 deletions tests/core/test_backward_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest

from dingo.core.utils.backward_compatibility import check_minimum_version


def test_check_minimum_version_passes_for_recent_version():
# A version well above the compatibility threshold must not raise.
assert check_minimum_version("dingo=99.0.0", raise_exception=True) is None


def test_check_minimum_version_raises_for_old_version():
with pytest.raises(ValueError, match="backwards compatibility"):
check_minimum_version("dingo=0.0.1", raise_exception=True)


def test_check_minimum_version_treats_none_as_oldest():
# A "None" version string is treated as 0.0.0, i.e. below the threshold.
with pytest.raises(ValueError):
check_minimum_version("None", raise_exception=True)


def test_check_minimum_version_warns_but_does_not_raise_by_default():
# With raise_exception=False (default), an old version only warns.
assert check_minimum_version("dingo=0.0.1") is None
87 changes: 87 additions & 0 deletions tests/core/test_build_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import numpy as np
import pytest

from dingo.core.posterior_models.build_model import (
autocomplete_model_kwargs,
build_model_from_kwargs,
)
from dingo.core.posterior_models.normalizing_flow import NormalizingFlowPosteriorModel


BASE_TRANSFORM_KWARGS = {
"hidden_dim": 8,
"num_transform_blocks": 1,
"activation": "elu",
"dropout_probability": 0.0,
"batch_norm": False,
"num_bins": 4,
"base_transform_type": "rq-coupling",
}


def _settings(posterior_model_type="normalizing_flow"):
return {
"train_settings": {
"model": {
"posterior_model_type": posterior_model_type,
"posterior_kwargs": {
"input_dim": 3,
"context_dim": None,
"num_flow_steps": 2,
"base_transform_kwargs": BASE_TRANSFORM_KWARGS,
},
}
}
}


def test_build_model_dispatches_to_normalizing_flow():
model = build_model_from_kwargs(settings=_settings(), device="cpu")
assert isinstance(model, NormalizingFlowPosteriorModel)


def test_build_model_dispatch_is_case_insensitive():
model = build_model_from_kwargs(
settings=_settings("Normalizing_Flow"), device="cpu"
)
assert isinstance(model, NormalizingFlowPosteriorModel)


def test_build_model_requires_exactly_one_of_filename_or_settings():
# Neither provided.
with pytest.raises(ValueError, match="filename or a settings"):
build_model_from_kwargs()
# Both provided.
with pytest.raises(ValueError, match="filename or a settings"):
build_model_from_kwargs(filename="x.pt", settings=_settings())


def test_build_model_rejects_unknown_type():
with pytest.raises(ValueError, match="No valid posterior model type"):
build_model_from_kwargs(settings=_settings("not_a_model"), device="cpu")


def test_autocomplete_model_kwargs_without_gnpe_proxies():
model_kwargs = {"embedding_kwargs": {"output_dim": 8}, "posterior_kwargs": {}}
# data_sample = [parameters, GW data] (no gnpe proxies)
autocomplete_model_kwargs(
model_kwargs, data_sample=[np.zeros(4), np.zeros((2, 3, 20))]
)

assert model_kwargs["embedding_kwargs"]["input_dims"] == [2, 3, 20]
assert model_kwargs["posterior_kwargs"]["input_dim"] == 4
assert model_kwargs["embedding_kwargs"]["added_context"] is False
# context_dim == embedding output_dim.
assert model_kwargs["posterior_kwargs"]["context_dim"] == 8


def test_autocomplete_model_kwargs_with_gnpe_proxies():
model_kwargs = {"embedding_kwargs": {"output_dim": 8}, "posterior_kwargs": {}}
# data_sample = [parameters, GW data, gnpe_proxies (len 2)]
autocomplete_model_kwargs(
model_kwargs, data_sample=[np.zeros(4), np.zeros((2, 3, 20)), np.zeros(2)]
)

assert model_kwargs["embedding_kwargs"]["added_context"] is True
# context_dim == output_dim + gnpe_proxy_dim == 8 + 2.
assert model_kwargs["posterior_kwargs"]["context_dim"] == 10
61 changes: 61 additions & 0 deletions tests/core/test_density_interpolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import numpy as np

from dingo.core.density.interpolation import (
interpolated_log_prob,
interpolated_log_prob_multi,
interpolated_sample_and_log_prob,
interpolated_sample_and_log_prob_multi,
)


# A uniform distribution on [0, W] discretized on a grid: density = 1/W, so the
# (normalized) log prob at any interior point is -log(W). The Interped wrapper
# normalizes internally, so the input `values` need not be normalized.
WIDTH = 2.0
SAMPLE_POINTS = np.linspace(0.0, WIDTH, 500)
UNIFORM_VALUES = np.ones_like(SAMPLE_POINTS)


def test_interpolated_log_prob_normalizes_uniform_distribution():
log_prob = interpolated_log_prob(SAMPLE_POINTS, UNIFORM_VALUES, WIDTH / 2)
assert log_prob == np.float64(log_prob) # scalar
# A constant density integrates exactly under the trapezoidal normalization, so
# ln_prob = -log(W) holds to floating-point precision (observed residual 0.0).
np.testing.assert_allclose(log_prob, -np.log(WIDTH), atol=1e-10)


def test_interpolated_sample_and_log_prob_in_range_and_consistent():
np.random.seed(0)
sample, log_prob = interpolated_sample_and_log_prob(SAMPLE_POINTS, UNIFORM_VALUES)
assert 0.0 <= sample <= WIDTH
# Both calls build the same Interped from the same data, so the returned log_prob
# equals interpolated_log_prob at the drawn sample to floating-point precision.
np.testing.assert_allclose(
log_prob,
interpolated_log_prob(SAMPLE_POINTS, UNIFORM_VALUES, sample),
atol=1e-10,
)


def test_interpolated_log_prob_multi_matches_single():
batch_values = np.stack([UNIFORM_VALUES, UNIFORM_VALUES])
eval_points = np.array([0.5, 1.5])
result = interpolated_log_prob_multi(
SAMPLE_POINTS, batch_values, eval_points, num_processes=1
)
assert result.shape == (2,)
expected = [
interpolated_log_prob(SAMPLE_POINTS, UNIFORM_VALUES, p) for p in eval_points
]
np.testing.assert_allclose(result, expected)


def test_interpolated_sample_and_log_prob_multi_shapes():
np.random.seed(0)
batch_values = np.stack([UNIFORM_VALUES] * 4)
samples, log_probs = interpolated_sample_and_log_prob_multi(
SAMPLE_POINTS, batch_values, num_processes=1
)
assert samples.shape == (4,)
assert log_probs.shape == (4,)
assert np.all((samples >= 0.0) & (samples <= WIDTH))
48 changes: 48 additions & 0 deletions tests/core/test_gnpeutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import numpy as np

from dingo.core.utils.gnpeutils import IterationTracker


def test_pvalue_min_is_minus_inf_before_two_updates():
tracker = IterationTracker()
assert tracker.pvalue_min == -np.inf # no ks_result yet

tracker.update({"a": np.random.default_rng(0).normal(size=100)})
# After a single update there is still no KS comparison.
assert tracker.pvalue_min == -np.inf


def test_first_update_stores_data_with_leading_axis():
tracker = IterationTracker()
tracker.update({"a": np.zeros(50), "b": np.ones(50)})
assert tracker.data["a"].shape == (1, 50)
assert tracker.data["b"].shape == (1, 50)


def test_second_update_computes_ks_pvalue():
rng = np.random.default_rng(0)
tracker = IterationTracker()
tracker.update({"a": rng.normal(size=200)})
tracker.update({"a": rng.normal(size=200)})
# Two samples from the same distribution -> a finite, valid p-value.
assert tracker.ks_result is not None
assert np.isfinite(tracker.pvalue_min)
assert 0.0 <= tracker.pvalue_min <= 1.0


def test_store_data_true_accumulates_rows():
rng = np.random.default_rng(0)
tracker = IterationTracker(store_data=True)
for _ in range(3):
tracker.update({"a": rng.normal(size=20)})
# store_data=True keeps every iteration along axis 0.
assert tracker.data["a"].shape == (3, 20)


def test_store_data_false_keeps_only_latest():
rng = np.random.default_rng(0)
tracker = IterationTracker(store_data=False)
for _ in range(3):
tracker.update({"a": rng.normal(size=20)})
# store_data=False replaces the stored data each iteration.
assert tracker.data["a"].shape == (1, 20)
Loading