Skip to content

Commit 97df2d7

Browse files
authored
Merge pull request #520 from alan-turing-institute/experimental_sim
Update Simulator base class
2 parents bd3697e + 4f587d7 commit 97df2d7

File tree

7 files changed

+579
-1284
lines changed

7 files changed

+579
-1284
lines changed

autoemulate/experimental/learners/base.py

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,74 +9,11 @@
99

1010
from autoemulate.experimental.data.utils import ValidationMixin
1111
from autoemulate.experimental.emulators.base import Emulator
12+
from autoemulate.experimental.simulations.base import Simulator
1213

1314
from ..types import GaussianLike, TensorLike
1415

1516

16-
@dataclass(kw_only=True)
17-
class Simulator(ValidationMixin, ABC):
18-
"""
19-
Simulator abstract class for generating outputs from inputs.
20-
21-
This class defines the interface for a simulator that produces samples based on
22-
input X.
23-
24-
Parameters
25-
----------
26-
(No additional parameters)
27-
"""
28-
29-
def sample(self, X: TensorLike) -> TensorLike:
30-
"""
31-
Generate samples from input data using the simulator.
32-
33-
Parameters
34-
----------
35-
X : TensorLike
36-
Input tensor of shape (n_samples, n_features).
37-
38-
Returns
39-
-------
40-
TensorLike
41-
Simulated output tensor.
42-
"""
43-
Y = self.check_matrix(self.sample_forward(self.check_matrix(X)))
44-
X, Y = self.check_pair(X, Y)
45-
return Y
46-
47-
@abstractmethod
48-
def sample_forward(self, X: TensorLike) -> TensorLike:
49-
"""
50-
Abstract method to perform the forward simulation.
51-
52-
Parameters
53-
----------
54-
X : TensorLike
55-
Input tensor.
56-
57-
Returns
58-
-------
59-
TensorLike
60-
Simulated output tensor.
61-
"""
62-
63-
@abstractmethod
64-
def sample_inputs(self, n: int) -> TensorLike:
65-
"""
66-
Abstract method to generate random input samples.
67-
68-
Parameters
69-
----------
70-
n : int
71-
Number of input samples to generate.
72-
73-
Returns
74-
-------
75-
TensorLike
76-
Random input tensor.
77-
"""
78-
79-
8017
@dataclass(kw_only=True)
8118
class Learner(ValidationMixin, ABC):
8219
"""
@@ -193,7 +130,7 @@ def fit(self, *args):
193130

194131
if X is not None:
195132
# If X is not, we skip the point (typically for Stream learners)
196-
Y_true = self.simulator.sample(X)
133+
Y_true = self.simulator.forward(X)
197134
self.X_train = torch.cat([self.X_train, X])
198135
self.Y_train = torch.cat([self.Y_train, Y_true])
199136
self.emulator.fit(self.X_train, self.Y_train)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from abc import ABC, abstractmethod
2+
3+
import torch
4+
from tqdm import tqdm
5+
6+
from autoemulate.experimental.data.utils import ValidationMixin
7+
from autoemulate.experimental.types import TensorLike
8+
from autoemulate.experimental_design import LatinHypercube
9+
10+
11+
class Simulator(ABC, ValidationMixin):
12+
"""
13+
Base class for simulations. All simulators should inherit from this class.
14+
This class provides the interface and common functionality for different
15+
simulation implementations.
16+
"""
17+
18+
def __init__(
19+
self, parameters_range: dict[str, tuple[float, float]], output_names: list[str]
20+
):
21+
"""
22+
Parameters
23+
----------
24+
parameters_range : dict[str, tuple[float, float]]
25+
Dictionary mapping input parameter names to their (min, max) ranges.
26+
output_names: list[str]
27+
List of output parameters' names.
28+
"""
29+
self._parameters_range = parameters_range
30+
self._param_names = list(parameters_range.keys())
31+
self._param_bounds = list(parameters_range.values())
32+
self._output_names = output_names
33+
self._in_dim = len(self.param_names)
34+
self._out_dim = len(self.output_names)
35+
self._has_sample_forward = False
36+
37+
@property
38+
def parameters_range(self) -> dict[str, tuple[float, float]]:
39+
"""Dictionary mapping input parameter names to their (min, max) ranges."""
40+
return self._parameters_range
41+
42+
@property
43+
def param_names(self) -> list[str]:
44+
"""List of parameter names."""
45+
return self._param_names
46+
47+
@property
48+
def param_bounds(self) -> list[tuple[float, float]]:
49+
"""List of parameter bounds."""
50+
return self._param_bounds
51+
52+
@property
53+
def output_names(self) -> list[str]:
54+
"""List of output parameter names."""
55+
return self._output_names
56+
57+
@property
58+
def in_dim(self) -> int:
59+
"""Input dimensionality."""
60+
return self._in_dim
61+
62+
@property
63+
def out_dim(self) -> int:
64+
"""Output dimensionality."""
65+
return self._out_dim
66+
67+
def sample_inputs(self, n_samples: int) -> TensorLike:
68+
"""
69+
Generate random samples using Latin Hypercube Sampling.
70+
71+
Parameters
72+
----------
73+
n_samples: int
74+
Number of samples to generate.
75+
76+
Returns
77+
-------
78+
TensorLike
79+
Parameter samples (column order is given by self.param_names)
80+
"""
81+
82+
lhd = LatinHypercube(self.param_bounds)
83+
sample_array = lhd.sample(n_samples)
84+
# TODO: have option to set dtype and ensure consistency throughout codebase?
85+
# added here as method was returning float64 and elsewhere had tensors of
86+
# float32 and this caused issues
87+
return torch.tensor(sample_array, dtype=torch.float32)
88+
89+
@abstractmethod
90+
def _forward(self, x: TensorLike) -> TensorLike:
91+
"""
92+
Abstract method to perform the forward simulation.
93+
94+
Parameters
95+
----------
96+
x : TensorLike
97+
Input parameters into the simulation forward run.
98+
99+
Returns
100+
-------
101+
TensorLike
102+
Simulated output tensor. Shape = (1, self.out_dim).
103+
For example, if the simulator outputs two simulated variables,
104+
then the shape would be (1, 2).
105+
"""
106+
107+
def forward(self, x: TensorLike) -> TensorLike:
108+
"""
109+
Generate samples from input data using the simulator. Combines the
110+
abstract method `_forward` with some validation checks.
111+
112+
Parameters
113+
----------
114+
x : TensorLike
115+
Input tensor of shape (n_samples, self.in_dim).
116+
117+
Returns
118+
-------
119+
TensorLike
120+
Simulated output tensor.
121+
"""
122+
y = self.check_matrix(self._forward(self.check_matrix(x)))
123+
x, y = self.check_pair(x, y)
124+
return y
125+
126+
def forward_batch(self, samples: TensorLike) -> TensorLike:
127+
"""
128+
Run multiple simulations with different parameters.
129+
130+
Parameters
131+
----------
132+
samples: TensorLike
133+
Tensor of input parameters to make predictions for.
134+
135+
Returns:
136+
-------
137+
TensorLike
138+
Tensor of simulation results of shape (n_batch, self.out_dim).
139+
"""
140+
results = []
141+
successful = 0
142+
143+
# Process each sample with progress tracking
144+
for i in tqdm(range(len(samples)), desc="Running simulations"):
145+
result = self.forward(samples[i : i + 1])
146+
if result is not None:
147+
results.append(result)
148+
successful += 1
149+
150+
# Report results
151+
print(
152+
f"Successfully completed {successful}/{len(samples)}"
153+
f" simulations ({successful / len(samples) * 100:.1f}%)"
154+
)
155+
156+
# stack results into a 2D array on first dim using torch
157+
return torch.cat(results, dim=0)
158+
159+
def get_parameter_idx(self, name: str) -> int:
160+
"""
161+
Get the index of a specific parameter.
162+
163+
Parameters
164+
----------
165+
name : str
166+
Name of the parameter to retrieve.
167+
168+
Returns
169+
-------
170+
float
171+
Index of the specified parameter.
172+
"""
173+
if name not in self._param_names:
174+
raise ValueError(f"Parameter {name} not found.")
175+
return self._param_names.index(name)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import torch
2+
3+
from autoemulate.experimental.simulations.base import Simulator
4+
from autoemulate.experimental.types import TensorLike
5+
from autoemulate.simulations.epidemic import simulate_epidemic
6+
7+
8+
class Epidemic(Simulator):
9+
"""
10+
Simulator of infectious disease spread (SIR).
11+
"""
12+
13+
def __init__(
14+
self,
15+
param_ranges=None,
16+
output_names=None,
17+
):
18+
if param_ranges is None:
19+
param_ranges = {"beta": (0.1, 0.5), "gamma": (0.01, 0.2)}
20+
if output_names is None:
21+
output_names = ["infection_rate"]
22+
super().__init__(param_ranges, output_names)
23+
24+
def _forward(self, x: TensorLike) -> TensorLike:
25+
"""
26+
Parameters
27+
----------
28+
x : TensorLike
29+
input parameter values to simulate [beta, gamma]:
30+
- `beta`: the transimission rate per day
31+
- `gamma`: the recovery rate per day
32+
33+
Returns
34+
-------
35+
TensorLike
36+
Peak infection rate.
37+
"""
38+
y = simulate_epidemic(x.numpy()[0])
39+
return torch.tensor([y]).view(-1, 1)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import torch
2+
3+
from autoemulate.experimental.simulations.base import Simulator
4+
from autoemulate.experimental.types import TensorLike
5+
from autoemulate.simulations.projectile import (
6+
simulate_projectile,
7+
simulate_projectile_multioutput,
8+
)
9+
10+
11+
class Projectile(Simulator):
12+
"""
13+
Simulator of projectile motion.
14+
"""
15+
16+
def __init__(
17+
self,
18+
param_ranges=None,
19+
output_names=None,
20+
):
21+
if param_ranges is None:
22+
param_ranges = {"c": (-5.0, 1.0), "v0": (0.0, 1000)}
23+
if output_names is None:
24+
output_names = ["distance"]
25+
super().__init__(param_ranges, output_names)
26+
27+
def _forward(self, x: TensorLike) -> TensorLike:
28+
"""
29+
Parameters
30+
----------
31+
x : TensorLike
32+
Dictionary of input parameter values to simulate:
33+
- `c`: the drag coefficient on a log scale
34+
- `v0`: velocity
35+
36+
Returns
37+
-------
38+
TensorLike
39+
Distance travelled by projectile.
40+
"""
41+
y = simulate_projectile(x.numpy()[0])
42+
return torch.tensor([y]).view(-1, 1)
43+
44+
45+
class ProjectileMultioutput(Simulator):
46+
"""
47+
Simulator of projectile motion.
48+
"""
49+
50+
def __init__(
51+
self,
52+
param_ranges=None,
53+
output_names=None,
54+
):
55+
if param_ranges is None:
56+
param_ranges = {"c": (-5.0, 1.0), "v0": (0.0, 1000)}
57+
if output_names is None:
58+
output_names = ["distance", "impact_velocity"]
59+
super().__init__(param_ranges, output_names)
60+
61+
def _forward(self, x: TensorLike) -> TensorLike:
62+
"""
63+
Parameters
64+
----------
65+
x : TensorLike
66+
Dictionary of input parameter values to simulate:
67+
- `c`: the drag coefficient on a log scale
68+
- `v0`: velocity
69+
70+
Returns
71+
-------
72+
TensorLike
73+
Distance travelled by projectile and impact velocity.
74+
"""
75+
y = simulate_projectile_multioutput(x.numpy()[0])
76+
return torch.tensor([y])

0 commit comments

Comments
 (0)