Skip to content

Commit d1a0c09

Browse files
committed
added system test
1 parent 61af7a7 commit d1a0c09

5 files changed

Lines changed: 197 additions & 4 deletions

File tree

tests/qoi/data/importance_sampling/brute_force_solution.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
156 KB
Binary file not shown.
95.6 KB
Binary file not shown.
48.4 KB
Binary file not shown.

tests/qoi/test_marginal_cdf_extrapolation_with_importance_sampling.py

Lines changed: 196 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
"""
2+
Tests for MarginalCDFExtrapolation with importance sampling.
3+
24
Plan:
35
- Unit test:
46
- _parameter_estimates: input and output weights are attached to the same samples
57
- Integration test:
68
- successful run of minimal version of the qoi using a deterministic model with importance sampling
9+
- System Test
10+
- Show that that the QoI estimation requires less samples with importance samples compared to when using
11+
the regular approach of including the whole environment dataset.
712
"""
813

14+
# %%
15+
import json
16+
from pathlib import Path
17+
18+
import numpy as np
19+
import pandas as pd
20+
import pytest
921
import torch
1022
from botorch.models.deterministic import GenericDeterministicModel
1123
from botorch.sampling.index_sampler import IndexSampler
24+
from numpy.typing import NDArray
25+
from torch.distributions import MultivariateNormal
1226
from torch.utils.data import DataLoader
1327

14-
from axtreme.data import ImportanceAddedWrapper, MinimalDataset
15-
from axtreme.qoi.marginal_cdf_extrapolation import MarginalCDFExtrapolation
28+
from axtreme.data import FixedRandomSampler, ImportanceAddedWrapper, MinimalDataset
29+
from axtreme.eval.qoi_job import QoIJob
30+
from axtreme.qoi import MarginalCDFExtrapolation
1631

1732

1833
def test_parameter_estimates_consistency_of_weights(gp_passthrough_1p: GenericDeterministicModel):
1934
"""
2035
Run a minimal version of the qoi using a deterministic model and a short period_len to test that the importance
2136
weights are still connected to the correct samples when using MarginalCDFExtrapolation._parameter_estimates.
2237
23-
Notes:
24-
- gp_passthrough_1p is defined in conftest.py. It creates a deterministic GP which always produces identical
38+
Args:
39+
gp_passthrough_1p is defined in conftest.py. It creates a deterministic GP which always produces identical
2540
posterior samples. The output location is a direct pass through of the given input data, and the scale is set
2641
to 1e-6.
42+
43+
Notes:
2744
- _parameter_estimates needs the importance dataset with specific dimensions by using ImportanceAddedWrapper and
2845
DataLoader the correct format is ensured and in addition it can be tested that these two functions do not change
2946
the relation between samples and weights.
@@ -64,6 +81,11 @@ def test_qoi_runs_with_importance_sampling(gp_passthrough_1p: GenericDeterminist
6481
Run a minimal version of the qoi using a deterministic model and a short period_len to test that it successfully
6582
runs with importance sampling.
6683
84+
Args:
85+
gp_passthrough_1p is defined in conftest.py. It creates a deterministic GP which always produces identical
86+
posterior samples. The output location is a direct pass through of the given input data, and the scale is set
87+
to 1e-6.
88+
6789
Notes:
6890
- The difference to test_parameter_estimates_consistency_of_weights is that the model is passed directly
6991
to the QoI estimator not qoi_estimator._parameter_estimates.
@@ -88,3 +110,173 @@ def test_qoi_runs_with_importance_sampling(gp_passthrough_1p: GenericDeterminist
88110

89111
# This only tests if the functions runs successfully and does not concern itself with the output
90112
_ = qoi_estimator(gp_passthrough_1p)
113+
114+
115+
# These are helper functions to define the loc and scale which are used in the simulator used for the system test
116+
def _true_loc_func(x: NDArray[np.float64]) -> NDArray[np.float64]:
117+
# For this toy example we use a MultivariateNormal distribution
118+
dist_mean, dist_cov = torch.tensor([1, 1]), torch.tensor([[0.03, 0], [0, 0.03]])
119+
120+
dist = MultivariateNormal(loc=dist_mean, covariance_matrix=dist_cov)
121+
122+
return np.exp(dist.log_prob(torch.tensor(x)).numpy())
123+
124+
125+
def _true_scale_func(x: NDArray[np.float64]) -> NDArray[np.float64]:
126+
# For this toy example we use a constant scale for simplicity
127+
return np.ones(x.shape[0]) * 0.1
128+
129+
130+
# Ruff does not allow default arguments in test functions. Using a decorator circumvents that.
131+
@pytest.mark.parametrize("params", [(1, False)])
132+
def test_system_marginal_cdf_with_importance_sampling(
133+
params: tuple[float, bool],
134+
) -> tuple[pd.DataFrame, float] | None:
135+
"""
136+
Show that that the QoI estimation requires less samples with importance samples compared to when using
137+
the regular approach of including the whole environment dataset.
138+
139+
Args:
140+
params: tuple of
141+
- The error allowed in assertions is multiplied by this number.
142+
- Bool to specify if the QoI results shall be returned for further testing or plotting.
143+
144+
Returns:
145+
If return_results==True: the QoI results are returned in a pandas Dataframe
146+
147+
A deterministic GP is used to remove most of the uncertainty related to the GP. Some uncertainty remains as the GP
148+
is not fit on the whole dataset.
149+
150+
Expectation:
151+
As we use a deterministic GP the QoI estimator will not be a distribution but a point representing the mean.
152+
The std of the means of several runs of the QoI estimator should be lower with uncertainty sampling than without.
153+
By visual inspection a threshold for the std for importance sampling is chosen.
154+
"""
155+
error_tolerance, return_results = params
156+
157+
# Load precalculated importance samples and weights
158+
importance_samples = torch.load(
159+
Path(__file__).parent / "data" / "importance_sampling" / "importance_samples.pt", weights_only=True
160+
)
161+
importance_weights = torch.load(
162+
Path(__file__).parent / "data" / "importance_sampling" / "importance_weights.pt", weights_only=True
163+
)
164+
165+
importance_dataset = ImportanceAddedWrapper(MinimalDataset(importance_samples), MinimalDataset(importance_weights))
166+
167+
# Load environment data
168+
# The environment is chosen such that it is concentrated in one region of the search space and only few samples
169+
# cover the subregion where the extreme response occurs
170+
env_data = np.load(Path(__file__).parent / "data" / "importance_sampling" / "environment_distribution.npy")
171+
172+
# Get brute force estimate
173+
brute_force_path = Path(__file__).parent / "data" / "importance_sampling" / "brute_force_solution.json"
174+
with brute_force_path.open() as file:
175+
brute_force_qoi = json.load(file)["statistics"]["median"]
176+
177+
# Set up a deterministic GP which uses the true underlying functions of loc and scale used in the simulator
178+
def true_underlying_func(x: torch.Tensor) -> torch.Tensor:
179+
locs = torch.from_numpy(_true_loc_func(x.numpy())).unsqueeze(-1)
180+
scales = torch.from_numpy(_true_scale_func(x.numpy())).unsqueeze(-1)
181+
return torch.concat([locs, scales], dim=-1)
182+
183+
gp_deterministic = GenericDeterministicModel(true_underlying_func, num_outputs=2)
184+
185+
# Create jobs with with and without importance sampling
186+
qoi_jobs = []
187+
datasets = {"full": env_data, "importance_sample": importance_dataset}
188+
for dataset_name, dataset in datasets.items():
189+
for i in range(200):
190+
# A fixed random sampler selects the same samples if the seed is the same which allows the results to be
191+
# compared if this function is run multiple times
192+
dataset_size = 800
193+
sampler = FixedRandomSampler(dataset, num_samples=dataset_size, seed=i, replacement=True)
194+
dataloader = DataLoader(dataset, sampler=sampler, batch_size=100)
195+
196+
qoi_estimator = MarginalCDFExtrapolation(
197+
env_iterable=dataloader,
198+
period_len=1_000,
199+
quantile=torch.tensor(0.5),
200+
quantile_accuracy=torch.tensor(0.01),
201+
# IndexSampler needs to be used with GenericDeterministicModel. Each sample just selects the mean.
202+
# As we use a deterministic model all posterior samples are identical and hence we only use one.
203+
posterior_sampler=IndexSampler(torch.Size([1])),
204+
)
205+
206+
qoi_jobs.append(
207+
QoIJob(
208+
name=f"qoi_{dataset_name}_{dataset_size}_{i}",
209+
qoi=qoi_estimator,
210+
model=gp_deterministic,
211+
tags={
212+
"dataset_name": dataset_name,
213+
"dataset_size": dataset_size,
214+
},
215+
)
216+
)
217+
218+
jobs_output_file = None
219+
qoi_results = [job(output_file=jobs_output_file) for job in qoi_jobs]
220+
221+
df_jobs = pd.json_normalize([item.to_dict() for item in qoi_results], max_level=1)
222+
df_jobs.columns = df_jobs.columns.str.removeprefix("tags.")
223+
224+
# The aim of this test is to show that QoI estimation with importance sampling requires less samples
225+
# (i.e. num_samples) to converge to the solution than with using the full environment dataset.
226+
# All absolute values in the following asserts are chosen based on visual inspection of the QoI results.
227+
# There are two criteria we use to judge the convergence:
228+
# 1. the mean of the QoI means is close to the brute force solution and in particular the one with
229+
# importance sampling is closer than the one without
230+
assert (
231+
abs(df_jobs.loc[df_jobs["dataset_name"] == "importance_sample", "mean"].mean() - brute_force_qoi)
232+
<= 0.2 * error_tolerance
233+
)
234+
235+
assert abs(df_jobs.loc[df_jobs["dataset_name"] == "importance_sample", "mean"].mean() - brute_force_qoi) <= abs(
236+
df_jobs.loc[df_jobs["dataset_name"] == "full", "mean"].mean() - brute_force_qoi
237+
)
238+
239+
# 2. the std of the QoI means is small with importance sampling
240+
assert df_jobs.loc[df_jobs["dataset_name"] == "importance_sample", "mean"].std() <= 0.25 * error_tolerance
241+
242+
if return_results:
243+
return df_jobs, brute_force_qoi
244+
245+
return None
246+
247+
248+
# %% Can be used to get plots for test_system_marginal_cdf_with_importance_sampling
249+
if __name__ == "__main__":
250+
# %% Calculate QoI
251+
from typing import cast
252+
253+
qoi_results, brute_force_qoi = cast(
254+
"tuple[pd.DataFrame, float]", test_system_marginal_cdf_with_importance_sampling((1, True))
255+
)
256+
257+
# %% Plot results
258+
import matplotlib.pyplot as plt
259+
260+
fig, ax = plt.subplots(1, 2, figsize=(10, 5), sharex=True, sharey=True)
261+
262+
# Plot results for full env data
263+
qoi_results[qoi_results["dataset_name"] == "full"].hist(column="mean", ax=ax[0], grid=False)
264+
ax[0].axvline(brute_force_qoi, c="orange", label=f"Brute force ({brute_force_qoi:.2f})")
265+
ax[0].set_title(
266+
"Dataset: full, "
267+
"mean={qoi_results.loc[qoi_results['dataset_name'] == 'full', 'mean'].mean():.2f}, "
268+
"std={qoi_results.loc[qoi_results['dataset_name'] == 'full', 'mean'].std():.2f}"
269+
)
270+
ax[0].legend()
271+
272+
# Plot results for importance sampling
273+
qoi_results[qoi_results["dataset_name"] == "importance_sample"].hist(column="mean", ax=ax[1], grid=False)
274+
ax[1].axvline(brute_force_qoi, c="orange", label=f"Brute force ({brute_force_qoi:.2f})")
275+
ax[1].set_title(
276+
"Dataset: importance sample, "
277+
"mean={qoi_results.loc[qoi_results['dataset_name'] == 'importance_sample', 'mean'].mean():.2f}, "
278+
"std={qoi_results.loc[qoi_results['dataset_name'] == 'importance_sample', 'mean'].std():.2f}"
279+
)
280+
ax[0].legend()
281+
282+
# %%

0 commit comments

Comments
 (0)