In this "PART 2", we quickly repeat all the steps discussed at length in part1, but using a much-finer resolution. This time, we'll start with slightly more complex initial concentrations built from 2 superposed sine waves.
We'll also explore the effects of:
-Spatial resolution ("delta x")
-Temporal resolution ("delta t")
-Alternate methods of estimating numerical derivatives
LAST REVISED: June 23, 2024 (using v. 1.0 beta34.1)
import set_path # Importing this module will add the project's home directory to sys.path
Added 'D:\Docs\- MY CODE\BioSimulations\life123-Win7' to sys.path
from life123 import BioSim1D
from life123 import ChemData as chem
from life123 import MovieArray
from life123 import Numerical as num
import numpy as np
import plotly.express as px
# We'll be considering just 1 chemical species, "A"
diffusion_rate = 10.
chem_data = chem(diffusion_rates=[diffusion_rate], names=["A"])
This will be our initial system, whose adherence to the diffusion equation we'll test. Afterwards, we'll tweak individual parameters - and observed their effect on the closeness of the approximation
# Parameters of the simulation run (the diffusion rate got set earlier, and will never vary)
delta_t = 0.01
n_bins = 300
delta_x = 2 # Note that the number of bins also define the fraction of the sine wave cycle in each bin
algorithm = None # "Explicit, with 3+1 stencil"
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
fig = px.line(data_frame=bio.system_snapshot(), y=["A"],
title= "Initial System State",
color_discrete_sequence = ['red'],
labels={"value":"concentration", "variable":"Chemical", "index":"Bin number"})
fig.show()
# All the system state will get collected in this object
history = MovieArray()
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 300)
# Compute time derivatives (for each bin)
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
df_dt_all_bins.shape
(5, 300)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(300,)
# Computer the second spatial derivative (simple method)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(300,)
at all bins (except 2 at each edge), at the middle simulation time t2
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
lhs.shape
(300,)
rhs = diffusion_rate*second_gradient_x_at_t2
rhs.shape
(300,)
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.023163289760024783
The above number is a measure of the discrepancy from the perfect match (zero distance) that an ideal solution would provide.
Notice how vastly closer to zero it is than the counterpart value we got with the very coarse simulation in experiment "validate_diffusion_1"
We'll now tweak some parameters, and observe the effect on the estimate of the discrepancy from the exact diffusion equation
The baseline distance was 0.023163289760024783
Expectation: greater discrepancy
n_bins = 100 # Reducing the spatial resolution
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 100)
# Compute time derivatives (for each bin) : simple method, using central differences
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(100,)
# Computer the second spatial derivative (simple method, using central differences)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(100,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.0705630110277808
Expectation: smaller discrepancy
n_bins = 900 # Increasing the spatial resolution (from the baseline of 300)
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 900)
# Compute time derivatives (for each bin) : simple method, using central differences
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(900,)
# Computer the second spatial derivative (simple method, using central differences)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(900,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.007721522026370068
Expectation: greater discrepancy
n_bins = 300 # Restoring the baseline number of bins
delta_t = 0.02 # Reducing the time resolution (from baselin 0.01)
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 300)
# Compute time derivatives (for each bin) : simple method, using central differences
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(300,)
# Computer the second spatial derivative (simple method, using central differences)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(300,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.04453006107638132
Expectation: smaller discrepancy
delta_t = 0.005 # Increasing the time resolution (from baselin 0.01)
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 300)
# Compute time derivatives (for each bin) : simple method, using central differences
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(300,)
# Computer the second spatial derivative (simple method, using central differences)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(300,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.011808914990091101
Expectation: presumably smaller discrepancy (unless dwarfed by errors from approximations in the simulation)
n_bins = 300 # Restoring the baseline number of bins
delta_t = 0.01 # Reducing the baseline time resolution
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 300)
# Compute time derivatives (for each bin) : simple method, using central differences
df_dt_all_bins = np.apply_along_axis(np.gradient, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(300,)
# Computer the second spatial derivative (MORE SOPHISTICATED METHOD, using 5-point stencils)
gradient_x_at_t2 = num.gradient_order4_1d(arr=f_at_t2, dx=delta_x)
second_gradient_x_at_t2 = num.gradient_order4_1d(arr=gradient_x_at_t2, dx=delta_x)
second_gradient_x_at_t2.shape
(300,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.006831207619477847
Expectation: presumably smaller discrepancy (unless dwarfed by errors from approximations in the simulation)
(Note: we revert to the original method for the spatial derivatives)
# Initialize the system
bio = BioSim1D(n_bins=n_bins, chem_data=chem_data)
# Initialize the concentrations to 2 superposed sine waves
bio.inject_sine_conc(species_name="A", frequency=1, amplitude=10, bias=50)
bio.inject_sine_conc(species_name="A", frequency=2, amplitude=8)
history = MovieArray() # All the system state will get collected in this object
# Store the initial state
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Do the 4 rounds of single-step diffusion; accumulate all data in the history object
for _ in range(4):
bio.diffuse(time_step=delta_t, n_steps=1, delta_x=delta_x , algorithm=algorithm)
arr = bio.lookup_species(species_index=0, copy=True)
history.store(par=bio.system_time, data_snapshot=arr, caption=f"State at time {bio.system_time}")
# Now, let's examine the data collected at the 5 time points
all_history = history.get_array()
all_history.shape
(5, 300)
# Compute time derivatives (for each bin) : MORE SOPHISTICATED METHOD, using 5-point stencils
df_dt_all_bins = np.apply_along_axis(num.gradient_order4_1d, 0, all_history, delta_t)
# Let's consider the state at the midpoint in time (t2)
f_at_t2 = all_history[2] # The middle of the 5 time snapshots
f_at_t2.shape
(300,)
# Computer the second spatial derivative (BACK TO SIMPLE METHOD, using central differences)
gradient_x_at_t2 = np.gradient(f_at_t2, delta_x)
second_gradient_x_at_t2 = np.gradient(gradient_x_at_t2, delta_x)
second_gradient_x_at_t2.shape
(300,)
# Compare the left and right hand sides of the diffusion equation
lhs = df_dt_all_bins[2] # t2 is the middle point of the 5
rhs = diffusion_rate*second_gradient_x_at_t2
num.compare_vectors(lhs, rhs, trim_edges=2) # Euclidean distance, ignoring 2 edge points at each end
0.023351269997869635