From simulation to optimization
Eugeniy E. Mikhailov and Greg Bentsen
Agenda
Today
- Review 2D NumPy arrays and indexing
- Simulate a virus outbreak on a lattice
- Optimize a vaccination policy
- Simulate a forest fire and discover percolation
- Optimize tree removal to prevent fire spread
Main ideas:
- A simulation predicts what happens for a choice of
parameters.
- Optimization selects parameters to achieve a
desired outcome.
From Simulation to Optimization
Simulation and Optimization
A simulation takes a model and parameters and
predicts an outcome.
An objective function assigns a numerical cost to
that outcome:
Optimization means finding the parameters with the
lowest cost:
Examples:
- Simulating the path of a rocket from earth to mars
- Simulating the behavior of the stock market
- Simulating the flow of traffic and traffic lights
Simulation and Optimization
This builds on our iterated maps from last week:
- Simple rules
- Iteration
- Complex behavior
But this time we’ll be adding a few new ingredients
- randomness
- objective functions
- modification of parameters to optimize the objective function
Today: two spatial simulations, two objective functions.
Review: Indexing a 2D array
A 2D NumPy array uses row, column indexing:
A = np.array([
[0.3, 0.8, 0.1, 0.6, 0.9],
[0.7, 0.2, 0.5, 0.4, 0.1],
[0.9, 0.6, 0.3, 0.8, 0.2],
[0.4, 0.1, 0.7, 0.5, 0.6]
])
print(A[2, 0]) # one element
print(A[1, :]) # one row
print(A[:, 2]) # one column
print(A[1:3, 2:4]) # subarray (what size?)
print(A[4,4])
print(A[:,5])
Predict before running: What shape and value(s) does
each expression return?
Key distinction: A[i, j] selects a cell;
A[i, :] and A[:, j] select a row or
column.
Simulation 1: Virus Spreading
A population on a 2D grid
Divide a region into an
lattice. Each site contains either zero or one
person.
Let
indicate whether site
is occupied:
where
is the probability a site is occupied (and the average
number of people per site).
import numpy as np
rng = np.random.default_rng(4)
L = 50
p = 0.65
occupied = rng.random((L, L)) < p
A spatial SIR model
Use one integer-valued array state to describe each
site:
0: empty
1: susceptible
2: infected
3: recovered
4: vaccinated
An occupied site holds exactly one person, whose
state changes over time; an empty site remains empty.
Source: generated with ChatGPT
Initially, vaccinate a fraction
of occupied sites, then seed a few infections among the remaining
susceptible people.
A spatial SIR model
Control parameter:
- Fraction of population
that is vaccinated
Other parameters: (not in our control)
- Population density
- Number of initial infections (5)
- Infection probability
- Recovery probability
Transmission and recovery
Let
count infected people on the four nearest-neighbor
sites. Since there is at most one person per site,
.
A susceptible person becomes infected with probability
where
is the transmission probability per infected neighbor per step.
Each infected person recovers with probability
per step.
Update rules:
- susceptible
infected with probability
- infected
recovered with probability
- Empty, recovered, and vaccinated sites do not change.
Determine all transitions from the old state, then
apply them simultaneously.
Review: Count nearest neighbors with slicing
How can we add the four neighboring infected counts without writing a
loop over every grid cell?
infected = (state == 2) # Boolean array
k = np.zeros_like(state, dtype=int)
k[1:, :] += infected[:-1, :] # neighbor above
k[:-1, :] += infected[1:, :] # neighbor below
k[:, 1:] += infected[:, :-1] # neighbor to the left
k[:, :-1] += infected[:, 1:] # neighbor to the right
Check: Why do the two slices on each line have the
same shape?
Boundary condition: No wraparound at the edges.
Boolean masks and vectorized updates
Rather than updating sites one at a time, use Boolean
masks and vectorized operations.
susceptible = (state == 1)
infected = (state == 2)
P = 1 - (1 - beta)**k
new_infections = susceptible & (rng.random(state.shape) < P)
new_recoveries = infected & (rng.random(state.shape) < gamma)
state[new_infections] = 2
state[new_recoveries] = 3
Demo: Simulate an outbreak
Hold
and
fixed. Compare different site-occupancy probabilities
and vaccination fractions
.
- Plot infected people versus time:
.
- Animate the spatial distribution of infection.
- Compare the final number ever infected.
Predict: What happens when we increase
?
What happens when we increase
?
Optimization 1: Vaccination costs versus infection costs
Vaccination prevents infections, but vaccination itself has a
cost.
Let
be the cumulative number of people infected over the outbreak,
including initial infections. Let
be the total population and
the number vaccinated.
Define the objective
- First term: expected infected fraction
- Second term: vaccination cost, weighted by
- Choose
to minimize
Why not just minimize the number of infections?
Optimize by scanning parameters
Evaluate the objective for a set of candidate vaccination
fractions.
v_values = np.linspace(0, 1, 21)
J_values = np.array([virus_objective(v)[2] for v in v_values])
best_index = np.argmin(J_values)
Here virus_objective(v) estimates the expected cost by
averaging over many independent outbreaks.
Discussion: How would the optimum change if
vaccination became more expensive
(
increased)?
Simulation 2: Forest Fire and Percolation
A simpler spreading model
Represent a forest by a Boolean occupancy array:
Each site independently contains a tree with probability
.
p = 0.65
forest = rng.random((L, L)) < p
Fire begins on occupied sites along the left
edge.
At every step, burning trees ignite all adjacent
unburned trees, and then become burned.
Important: Fire spread is deterministic once the
forest is generated.
Demo: Vary the tree density
Try
,
,
and
.
- Does the fire reach the right edge?
- How many trees burn?
- Do repeated random forests give the same outcome?
Even though fire propagation is deterministic, the forest
itself is random.
Percolation: A connectivity transition
A connected cluster consists of trees joined by
nearest-neighbor steps.
In this model, the trees that eventually burn are the connected
clusters that are connected to the ignition
boundary.
Therefore,
For independent site percolation on a large square lattice,
Below
,
spanning is unlikely on a large lattice; above
,
it is likely.

Estimating the spanning probability
A single random forest answers yes or no. Many
trials estimate a probability.
p_values = np.linspace(0.35, 0.85, 21)
probabilities = [
np.mean([fire_spans(p) for _ in range(100)])
for p in p_values
]
Question: Why is the transition smooth for a finite
grid rather than perfectly sharp at
?
Optimization 2: Preventing forest fires
Suppose we start with a dense forest of tree density
.
Remove a fraction
of the existing trees at random. The remaining density is
Removing trees costs money; letting the fire reach the opposite side
causes damage.
Define
- First term: risk of a crossing fire
- Second term: cost of clearing a fraction
of trees
- Choose
to minimize
Predict the optimal removal fraction
From the percolation threshold,
Thus,
For
,
Prediction: Removing roughly
of the existing trees moves the remaining forest through the percolation
threshold.
Does that guarantee the minimum of
occurs at
?
No—the answer depends on
and finite-size effects.
Demo: Minimize fire risk and removal cost
For each value of
:
- Generate many forests with density
.
- Compute the fraction of fires that reach the far edge.
- Add the removal cost
.
- Plot
and locate its minimum.
q_values = np.linspace(0, 0.6, 31)
J_values = np.array([fire_objective(q) for q in q_values])
q_best = q_values[np.argmin(J_values)]
Review
Review
Virus outbreak
- Random population:
- Stochastic infection and recovery
- Choose vaccination fraction
to balance health and intervention costs
Forest fire
- Random occupancy:
- Deterministic spreading on a fixed forest
- Choose tree-removal fraction
to balance fire risk and clearing costs
Looking ahead: How do we find the best parameters?
Today, we evaluated the objective on a grid of candidate parameter
values:
But what if there are many parameters, or each
simulation is expensive?
- Can we search more efficiently than testing every possibility?
- How do noise, local minima, and constraints affect
optimization?
- How can we use derivatives to guide the search?
Next: Numerical optimization algorithms.