Skip to content

Commit f5908f4

Browse files
Merge pull request #79 from dnv-opensource/dev_basic_example
Added extra plotting of extreme response locations and example convergence to basic example
2 parents 26350f0 + b6b3bdb commit f5908f4

12 files changed

Lines changed: 385 additions & 140 deletions

File tree

examples/basic_example_usecase/problem/brute_force.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@
2525
from scipy.stats import gumbel_r
2626
from torch.utils.data import DataLoader, RandomSampler, TensorDataset
2727

28+
torch.set_default_dtype(torch.float64)
29+
2830
# This allows us to run as interactive and as a module.
2931
if __name__ == "__main__":
3032
from simulator import _true_loc_func, _true_scale_func # type: ignore[import-not-found]
3133
else:
3234
from .simulator import _true_loc_func, _true_scale_func
33-
34-
torch.set_default_dtype(torch.float64)
35-
3635
# for typing
3736
_: Any
3837

@@ -49,12 +48,13 @@ class ResultsObject:
4948
# statistics are optional
5049
statistics: dict[str, float]
5150
samples: list[float]
51+
env_data: list[float]
5252

5353
@classmethod
54-
def from_samples(cls, samples: torch.Tensor) -> "ResultsObject":
54+
def from_samples(cls, samples: torch.Tensor, env_data: torch.Tensor) -> "ResultsObject":
5555
"""Create the object directly from samples."""
5656
statistics = {"median": float(samples.median()), "mean": float(samples.mean())}
57-
return ResultsObject(statistics=statistics, samples=samples.tolist())
57+
return ResultsObject(statistics=statistics, samples=samples.tolist(), env_data=env_data.tolist())
5858

5959

6060
# %%
@@ -63,7 +63,7 @@ def _result_file_name(period_length: int) -> str:
6363
return f"n_sample_per_period_{period_length}"
6464

6565

66-
def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
66+
def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000) -> tuple[torch.Tensor, torch.Tensor]:
6767
"""Return a saved result for the desired length of time if available, otherwise calculate the result.
6868
6969
New results will also be saved within this directory.
@@ -74,30 +74,40 @@ def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000)
7474
num_estimates: The number of brute force estimates of the QoI. A new period is drawn for each estimate.
7575
7676
Returns:
77-
The QoI values calculated for each period. Shape (num_estimates,)
77+
Tuple of:
78+
ERD samples: (num_estimates,) samples of the ERD for that period length. QoIs can be calculated from this.
79+
X_max: (num_estimates, d) The location in the environments space that produced the ERD sample.
7880
"""
7981
results_path = _results_dir / f"{_result_file_name(period_length)}.json"
8082

8183
samples = torch.tensor([])
84+
max_location = torch.tensor([])
8285

8386
if results_path.exists():
8487
with results_path.open() as fp:
8588
results = json.load(fp)
8689
samples = torch.tensor(results["samples"])
90+
max_location = torch.tensor(results["env_data"])
8791

8892
# make any additional samples required
8993
if len(samples) < num_estimates:
90-
new_samples = brute_force(period_length, num_estimates - len(samples))
94+
new_samples, new_max_location = brute_force(period_length, num_estimates - len(samples))
9195

9296
samples = torch.concat([samples, new_samples])
97+
max_location = torch.concat([max_location, new_max_location])
98+
9399
# save results:
94100
with results_path.open("w") as fp:
95-
json.dump(asdict(ResultsObject.from_samples(samples)), fp)
101+
json.dump(asdict(ResultsObject.from_samples(samples, max_location)), fp)
96102

97-
return samples
103+
elif len(samples) > num_estimates:
104+
samples = samples[:num_estimates]
105+
max_location = max_location[:num_estimates]
98106

107+
return samples, max_location
99108

100-
def brute_force(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
109+
110+
def brute_force(period_length: int, num_estimates: int = 2_000) -> tuple[torch.Tensor, torch.Tensor]:
101111
"""Produces brute force samples of the Extreme Response Distibtuion.
102112
103113
Args:
@@ -119,7 +129,9 @@ def brute_force(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
119129
return _brute_force_calc(dataloader, num_estimates)
120130

121131

122-
def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_estimates: int = 2_000) -> torch.Tensor:
132+
def _brute_force_calc(
133+
dataloader: DataLoader[tuple[torch.Tensor, ...]], num_estimates: int = 2_000
134+
) -> tuple[torch.Tensor, torch.Tensor]:
123135
"""Calculate the QOI by brute force.
124136
125137
Args:
@@ -131,9 +143,15 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
131143
num_estimates: The number of brute force estimates of the QoI. A new period is drawn for each estimate.
132144
133145
Returns:
134-
The QoI values calculated for each period. Shape (num_estimates,)
146+
Tuple of:
147+
ERD samples: (num_estimates,) samples of the ERD for that period length. QoIs can be calculated from this.
148+
X_max: (num_estimates, d) The location in the environments space that produced the ERD sample.
135149
"""
136150
maxs = torch.zeros(num_estimates)
151+
152+
_, d = next(iter(dataloader))[0].shape
153+
maxs_location = torch.zeros(num_estimates, d)
154+
137155
for i in tqdm.tqdm(range(num_estimates)):
138156
current_max = float("-inf")
139157

@@ -144,11 +162,17 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
144162

145163
gumbel_samples: np.ndarray[tuple[int,], Any] = gumbel_r.rvs(loc=loc, scale=scale) # type: ignore # noqa: PGH003
146164

147-
current_max = max(current_max, gumbel_samples.max())
165+
simulator_samples_max = gumbel_samples.max()
166+
if simulator_samples_max > current_max:
167+
current_max = simulator_samples_max
168+
169+
# Get env data corresponding to max(c_max)
170+
max_index = np.argmax(gumbel_samples)
171+
maxs_location[i] = torch.tensor(samples[max_index, :])
148172

149173
maxs[i] = current_max
150174

151-
return maxs
175+
return maxs, maxs_location
152176

153177

154178
# %%
@@ -161,7 +185,7 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
161185
N_ENV_SAMPLES_PER_PERIOD = N_YEARS_IN_PERIOD * N_SECONDS_IN_YEAR // N_SECONDS_IN_TIME_STEP
162186
N_ENV_SAMPLES_PER_PERIOD = 1000
163187

164-
samples = collect_or_calculate_results(N_ENV_SAMPLES_PER_PERIOD, 300_000)
188+
samples, x_max = collect_or_calculate_results(N_ENV_SAMPLES_PER_PERIOD, 300_000)
165189

166190
_ = plt.hist(samples, bins=100, density=True)
167191
_ = plt.title(
@@ -176,6 +200,9 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
176200
)
177201
plt.show()
178202

203+
_ = plt.scatter(x_max[:, 0], x_max[:, 1])
204+
plt.show()
205+
179206
# %%
180207
"""Here we explore the noise in the brute force result as more samples are collected.
181208
Binary file not shown.

examples/basic_example_usecase/problem/env_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,6 @@ def collect_data() -> pd.DataFrame:
6767

6868
# %%
6969
if __name__ == "__main__":
70-
generate_and_save_data(n_samples=10_000_000, seed=42)
70+
generate_and_save_data(n_samples=50_000, seed=42)
7171

7272
# %%
124 Bytes
Loading

examples/basic_example_usecase/problem/results/brute_force/n_sample_per_period_1000.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

examples/demo2d/problem/brute_force.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@
2525
from scipy.stats import gumbel_r
2626
from torch.utils.data import DataLoader, RandomSampler, TensorDataset
2727

28+
torch.set_default_dtype(torch.float64)
29+
2830
# This allows us to run as interactive and as a module.
2931
if __name__ == "__main__":
3032
from simulator import _true_loc_func, _true_scale_func # type: ignore[import-not-found]
3133
else:
3234
from .simulator import _true_loc_func, _true_scale_func
33-
34-
torch.set_default_dtype(torch.float64)
35-
3635
# for typing
3736
_: Any
3837

@@ -49,12 +48,13 @@ class ResultsObject:
4948
# statistics are optional
5049
statistics: dict[str, float]
5150
samples: list[float]
51+
env_data: list[float]
5252

5353
@classmethod
54-
def from_samples(cls, samples: torch.Tensor) -> "ResultsObject":
54+
def from_samples(cls, samples: torch.Tensor, env_data: torch.Tensor) -> "ResultsObject":
5555
"""Create the object directly from samples."""
5656
statistics = {"median": float(samples.median()), "mean": float(samples.mean())}
57-
return ResultsObject(statistics=statistics, samples=samples.tolist())
57+
return ResultsObject(statistics=statistics, samples=samples.tolist(), env_data=env_data.tolist())
5858

5959

6060
# %%
@@ -63,7 +63,7 @@ def _result_file_name(period_length: int) -> str:
6363
return f"n_sample_per_period_{period_length}"
6464

6565

66-
def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
66+
def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000) -> tuple[torch.Tensor, torch.Tensor]:
6767
"""Return a saved result for the desired length of time if available, otherwise calculate the result.
6868
6969
New results will also be saved within this directory.
@@ -74,30 +74,40 @@ def collect_or_calculate_results(period_length: int, num_estimates: int = 2_000)
7474
num_estimates: The number of brute force estimates of the QoI. A new period is drawn for each estimate.
7575
7676
Returns:
77-
The QoI values calculated for each period. Shape (num_estimates,)
77+
Tuple of:
78+
ERD samples: (num_estimates,) samples of the ERD for that period length. QoIs can be calculated from this.
79+
X_max: (num_estimates, d) The location in the environments space that produced the ERD sample.
7880
"""
7981
results_path = _results_dir / f"{_result_file_name(period_length)}.json"
8082

8183
samples = torch.tensor([])
84+
max_location = torch.tensor([])
8285

8386
if results_path.exists():
8487
with results_path.open() as fp:
8588
results = json.load(fp)
8689
samples = torch.tensor(results["samples"])
90+
max_location = torch.tensor(results["env_data"])
8791

8892
# make any additional samples required
8993
if len(samples) < num_estimates:
90-
new_samples = brute_force(period_length, num_estimates - len(samples))
94+
new_samples, new_max_location = brute_force(period_length, num_estimates - len(samples))
9195

9296
samples = torch.concat([samples, new_samples])
97+
max_location = torch.concat([max_location, new_max_location])
98+
9399
# save results:
94100
with results_path.open("w") as fp:
95-
json.dump(asdict(ResultsObject.from_samples(samples)), fp)
101+
json.dump(asdict(ResultsObject.from_samples(samples, max_location)), fp)
96102

97-
return samples
103+
elif len(samples) > num_estimates:
104+
samples = samples[:num_estimates]
105+
max_location = max_location[:num_estimates]
98106

107+
return samples, max_location
99108

100-
def brute_force(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
109+
110+
def brute_force(period_length: int, num_estimates: int = 2_000) -> tuple[torch.Tensor, torch.Tensor]:
101111
"""Produces brute force samples of the Extreme Response Distibtuion.
102112
103113
Args:
@@ -119,7 +129,9 @@ def brute_force(period_length: int, num_estimates: int = 2_000) -> torch.Tensor:
119129
return _brute_force_calc(dataloader, num_estimates)
120130

121131

122-
def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_estimates: int = 2_000) -> torch.Tensor:
132+
def _brute_force_calc(
133+
dataloader: DataLoader[tuple[torch.Tensor, ...]], num_estimates: int = 2_000
134+
) -> tuple[torch.Tensor, torch.Tensor]:
123135
"""Calculate the QOI by brute force.
124136
125137
Args:
@@ -131,9 +143,15 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
131143
num_estimates: The number of brute force estimates of the QoI. A new period is drawn for each estimate.
132144
133145
Returns:
134-
The QoI values calculated for each period. Shape (num_estimates,)
146+
Tuple of:
147+
ERD samples: (num_estimates,) samples of the ERD for that period length. QoIs can be calculated from this.
148+
X_max: (num_estimates, d) The location in the environments space that produced the ERD sample.
135149
"""
136150
maxs = torch.zeros(num_estimates)
151+
152+
_, d = next(iter(dataloader))[0].shape
153+
maxs_location = torch.zeros(num_estimates, d)
154+
137155
for i in tqdm.tqdm(range(num_estimates)):
138156
current_max = float("-inf")
139157

@@ -144,11 +162,17 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
144162

145163
gumbel_samples: np.ndarray[tuple[int,], Any] = gumbel_r.rvs(loc=loc, scale=scale) # type: ignore # noqa: PGH003
146164

147-
current_max = max(current_max, gumbel_samples.max())
165+
simulator_samples_max = gumbel_samples.max()
166+
if simulator_samples_max > current_max:
167+
current_max = simulator_samples_max
168+
169+
# Get env data corresponding to max(c_max)
170+
max_index = np.argmax(gumbel_samples)
171+
maxs_location[i] = torch.tensor(samples[max_index, :])
148172

149173
maxs[i] = current_max
150174

151-
return maxs
175+
return maxs, maxs_location
152176

153177

154178
# %%
@@ -161,7 +185,7 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
161185
N_ENV_SAMPLES_PER_PERIOD = N_YEARS_IN_PERIOD * N_SECONDS_IN_YEAR // N_SECONDS_IN_TIME_STEP
162186
N_ENV_SAMPLES_PER_PERIOD = 1000
163187

164-
samples = collect_or_calculate_results(N_ENV_SAMPLES_PER_PERIOD, 300_000)
188+
samples, x_max = collect_or_calculate_results(N_ENV_SAMPLES_PER_PERIOD, 300_000)
165189

166190
_ = plt.hist(samples, bins=100, density=True)
167191
_ = plt.title(
@@ -176,6 +200,9 @@ def _brute_force_calc(dataloader: DataLoader[tuple[torch.Tensor, ...]], num_esti
176200
)
177201
plt.show()
178202

203+
_ = plt.scatter(x_max[:, 0], x_max[:, 1])
204+
plt.show()
205+
179206
# %%
180207
"""Here we explore the noise in the brute force result as more samples are collected.
181208
-206 Bytes
Loading

examples/demo2d/problem/results/brute_force/n_sample_per_period_1000.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/axtreme/plotting/doe.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def plot_qoi_estimates_from_experiment(
6262
ax: None | Axes = None,
6363
points_between_ests: int = 1,
6464
name: str | None = None,
65+
trial_index: int | None = None,
6566
**kwargs: Any, # noqa: ANN401
6667
) -> Axes:
6768
"""Plot how the QoI estimates changes over the DoE process from a given experiment with the QoI metric attached.
@@ -72,6 +73,7 @@ def plot_qoi_estimates_from_experiment(
7273
points_between_ests: This should be used if multiple DoE iterations are used between qoi estimates
7374
(e.g if the estimate is expensive). It adjusts the scale of the x axis.
7475
name: optional name that should be added to the legend information for this plot
76+
trial_index: If provided, only plot data up to this trial index
7577
kwargs: kwargs that should be passed to matplotlib. Must be applicable to `ax.plot` and `ax.fill_between`
7678
7779
Returns:
@@ -80,8 +82,13 @@ def plot_qoi_estimates_from_experiment(
8082
metrics = experiment.fetch_data()
8183
qoi_metrics = metrics.df[metrics.df["metric_name"] == "QoIMetric"]
8284

85+
# Filter by trial index if provided
86+
if trial_index is not None:
87+
qoi_metrics = qoi_metrics[qoi_metrics["trial_index"] <= trial_index]
88+
8389
qoi_means = qoi_metrics["mean"]
8490
qoi_sems = qoi_metrics["sem"]
91+
8592
if ax is None:
8693
_, ax = plt.subplots()
8794

0 commit comments

Comments
 (0)