Custom Simulations#
Some features in AutoEmuluate can be run simply on data where as other require a simulation to be run. For example, features such as active learning, require the user to provide a simulator that can be run by AutoEmulate. This tutorial will step through how to do this.
The Simulator Class#
The simulator class is a Base Python Class that can be inherited to create a custom simulator. The simulator class requires the user to implement the following:
parameter_range
: A parameter set at class inititation. A dictionary of str to tuple pairs. The string is the name of the prameter and the tuple is the min and max value of the parameter. A range is given because the simulator will sample prior to running the simulation from this distribution.output_variables
: A parameter set at class initiation. A list of strings that are the names of the outputs that the simulator will return._forward
: An abstract method that must be implemented by the user. This method will define a single forward pass of the simulation, taking in the input parameters and returning the output variables. There are some important rules for this method:The input to the method must be a tensor of shape
(1, n)
wheren
is the number of input parameters.The output of th method must be a tensor of shape
(1, m)
wherem
is the number of output variables.If the simulation fails, it must output
None
.
Below is an example of a custom simulator that can be used with AutoEmulate.
The example is a projectile simulation, where we will simulate the distance travelled given a launch angle and initial velocity.
from autoemulate.simulations.base import Simulator
import torch
class Projectile(Simulator):
"""
Simulator of projectile motion.
"""
def __init__(self, param_ranges, output_names):
super().__init__(param_ranges, output_names, log_level="error")
def _forward(self, x):
"""
Calculate the horizontal distance a projectile travels using PyTorch.
Parameters:
----------
velocity: float or torch.Tensor
Initial velocity in m/s.
angle_degrees: float or torch.Tensor
Launch angle in degrees.
Returns:
-------
torch.Tensor
Distance traveled in meters.
"""
# Extract velocity and angle from input tensor
angle_degrees = x[:, 0]
velocity = x[:, 1]
# Convert angle from degrees to radians and calculate distance
angle_radians = torch.deg2rad(angle_degrees)
# Calculate the distance using the projectile motion formula
distance = (velocity ** 2) * torch.sin(2 * angle_radians) / 9.81
# Ensure the output is a 2D tensor
if distance.ndim == 1:
distance = distance.unsqueeze(1)
return distance
param_ranges = {"angle": (5, 85), "velocity": (0.0, 1000)}
output_names = ["distance"]
projectile_simulator = Projectile(param_ranges=param_ranges, output_names=output_names)
What can the simulator do?#
The first thing to do is sample inputs from the parameter space. In the following cell, we sample 10 times. This is appended into a single tensor of shape (10, 2)
where the first column is the angle and the second column is the velocity.
The input_samples
method implements latin hypercube sampling of the input parameters. However, if you have a preferred sampling method, you can simply override this method.
input_samples = projectile_simulator.sample_inputs(10)
print(input_samples.shape)
torch.Size([10, 2])
These input samples can now be passed to the simulator to run a simulation. The forward
method will simulate a single forward pass of the simulation whereas forward_batch
will simulate a batch of forward passes.
single_output = projectile_simulator.forward(input_samples[0:1])
print("Single output: ", single_output)
print("Single output shape: ", single_output.shape)
Single output: tensor([[23347.3418]])
Single output shape: torch.Size([1, 1])
multiple_output = projectile_simulator.forward_batch(input_samples)
print("Multiple output: ", multiple_output)
print("Multiple output shape: ", multiple_output.shape)
Multiple output: tensor([[23347.3418],
[ 4236.8784],
[17140.7520],
[ 2264.9624],
[29510.0898],
[21688.1543],
[ 764.8733],
[43282.7812],
[ 5986.3647],
[63973.9023]])
Multiple output shape: torch.Size([10, 1])
Lets look at the results of 10,000 simulations#
inputs = projectile_simulator.sample_inputs(10_000)
outputs = projectile_simulator.forward_batch(inputs)
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 6))
sc = plt.scatter(
inputs[:, 0].numpy(), # angle
inputs[:, 1].numpy(), # velocity
c=outputs[:, 0].numpy(), # distance
cmap='viridis',
s=6
)
plt.xlabel('Angle (degrees)')
plt.ylabel('Velocity (m/s)')
plt.title('Projectile Distance by Angle and Velocity')
plt.colorbar(sc, label='Distance (m)')
plt.show()
