#Copyright © 2025-Present, UChicago Argonne, LLC
import numpy as np
from scipy.linalg import solve_banded
import numpy as np
def transport_circular(AR, p_reac):
"""Solve the steady state transport equation inside a circular via
Transport using Knudsen diffusion
"""
N = p_reac.shape[0]-1
ab = np.zeros((3,N+1))
#diagonal, 1,j
ab[1,:-1] = 3*(AR/N)**2
ab[1,-1] = 3/4*AR/N
ab[1,:] *= p_reac
ab[1,1:-2] += 2
ab[1,0] += 3
ab[1,-2] += 3
ab[1,-1] += 2
ab[0,1:-1] = -1
ab[0,-1] = -2
ab[2,0:-2] = -1
ab[2,-2] = -2
b = np.zeros(N+1)
b[0] = 2
return solve_banded((1,1), ab, b)
def solve(AR, N, p_stick0, p_rec0=0, p_rec1=0, target_cov=0.25, time_multiplier=2):
dt = 0.05
cov = np.zeros(N+1)
i = 0
s_index = 0
store = []
store_times = []
found = False
done = False
target_time = None
while np.min(cov) < 0.99:
p_stick_eff = (p_stick0+p_rec0)*(1-cov) + p_rec1*cov
x = transport_circular(AR, p_stick_eff)
a = x*dt
new_cov = (cov + a)/(1+a)
cov = new_cov
i += 1
if not done:
if found:
if i >= target_time:
store.append(cov[:-1])
store_times.append(i)
done = True
else:
if np.mean(cov) > target_cov:
store.append(cov[:-1])
store_times.append(i)
found = True
target_time = time_multiplier*i
store_times.append(i)
return store_times, store
def solve_until(AR, N, p_stick0, p_rec0=0, p_rec1=0, target_time=1.0, save_every=0.2, dt=0.01):
"""Solve precursor transport inside a circular via of aspect ratio AR
This function solves the precursor transport and surface reaction kinetics
inside a circular via using Knudsen diffusion. The simulation runs until
a specified target time and saves coverage profiles at regular intervals.
Args:
AR (float): Aspect ratio of the circular via
N (int): Number of discretized segments along the via depth
p_stick0 (float): Sticking probability of the self-limited process
p_rec0 (float, optional): Recombination probability on bare sites. Defaults to 0.
p_rec1 (float, optional): Recombination probability on reacted sites. Defaults to 0.
target_time (float, optional): Normalized time at which the simulation stops. Defaults to 1.0.
save_every (float, optional): Normalized time interval at which coverage profiles
are saved. Defaults to 0.2.
dt (float, optional): time increment used for the numerical integration
Returns:
tuple: A tuple containing:
- store_times (list): List of normalized times corresponding to saved profiles
- store (list): List of coverage arrays at saved time points, each of size N
Notes:
All time values are in normalized units.
"""
dt = 0.05
cov = np.zeros(N+1)
i = 0
store = []
store_times = []
next_save_time = save_every
while i*dt < target_time:
p_stick_eff = (p_stick0+p_rec0)*(1-cov) + p_rec1*cov
x = transport_circular(AR, p_stick_eff)
a = x*dt
new_cov = (cov + a)/(1+a)
cov = new_cov
i += 1
# Check if we should save this iteration
current_time = i*dt
if current_time >= next_save_time:
store.append(cov[:-1].copy())
store_times.append(current_time)
next_save_time += save_every
# Add the final coverage to store if it hasn't been added yet
final_time = i*dt
if len(store_times) == 0 or store_times[-1] < final_time:
store.append(cov[:-1].copy())
store_times.append(final_time)
return store_times, store
def solve_until_cov(AR, N, p_stick0, p_rec0=0, p_rec1=0, target_cov=0.99, save_every=0.2, dt=0.05):
"""Solve precursor transport inside a circular via of aspect ratio AR
This function solves the precursor transport and surface reaction kinetics
inside a circular via using Knudsen diffusion. The simulation runs until
a specified target coverage is reached.
Args:
AR (float): Aspect ratio of the circular via
N (int): Number of discretized segments along the via depth
p_stick0 (float): Sticking probability of the self-limited process
p_rec0 (float, optional): Recombination probability on bare sites. Defaults to 0.
p_rec1 (float, optional): Recombination probability on reacted sites. Defaults to 0.
target_cov (float, optional): Normalized time at which the simulation stops. Defaults to 0.99
save_every (float, optional): Coverage intervals at which profiles and time are saved. Defaults to 0.2.
dt (float, optional): time increment used for the numerical integration
Returns:
tuple: A tuple containing:
- store_times (list): List of normalized times corresponding to saved profiles
- store (list): List of coverage arrays
Notes:
All time values are in normalized units.
"""
cov = np.zeros(N+1)
i = 0
store = []
store_times = []
next_save_cov = save_every
mean_cov = 0
while mean_cov < target_cov:
p_stick_eff = (p_stick0+p_rec0)*(1-cov) + p_rec1*cov
x = transport_circular(AR, p_stick_eff)
a = x*dt
new_cov = (cov + a)/(1+a)
cov = new_cov
i += 1
# Check if we should save this iteration
mean_cov = np.mean(cov)
current_time = i*dt
if mean_cov >= next_save_cov:
store.append(cov[:-1].copy())
store_times.append(current_time)
next_save_cov += save_every
# Add the final coverage to store if it hasn't been added yet
final_time = i*dt
store.append(cov[:-1].copy())
store_times.append(final_time)
return store_times, store
[docs]
class DiffusionViaND:
"""Model for ALD in high aspect ratio circular vias.
Implementation of a non-dimensional model for atomic layer deposition
in high-aspect-ratio circular vias. The model uses a Knudsen diffusion transport
model and self-limited surface reaction kinetics and surface recombination.
The model assumes a first-order irreversible Langmuir kinetics
with the sticking probability value determining the reaction rate.
It also supports recombination processes on both bare and reacted
surface sites.
Parameters
----------
AR : float
Aspect ratio of the circular via (depth/diameter). Higher values
indicate deeper, narrower structures where diffusion limitations
become more significant. Must be non-negative.
p_stick0 : float
Sticking probability for the self-limited ALD process. Represents
the probability that a precursor molecule will react when it
encounters a surface site.
p_rec0 : float, optional
Recombination probability on bare (unreacted) surface sites.
Default is 0.
p_rec1 : float, optional
Recombination probability on reacted surface sites. Default is 0.
Attributes
----------
AR : float
The aspect ratio of the via.
p_stick0 : float
The sticking probability of the ALD process.
p_rec0 : float
The recombination probability on bare sites.
p_rec1 : float
The recombination probability on reacted sites.
dz : float
Size of the discretized elements, normalized to the via diameter.
Computed as 1/nsegments.
nsegments : int
Number of discretized elements per unit aspect ratio. Default is 4.
Examples
--------
Create a DiffusionViaND model for a via with aspect ratio 10:
>>> model = DiffusionViaND(AR=10, p_stick0=0.05)
>>> times, coverage = model.run(max_time=2.0)
>>> print(f"Final mean coverage: {coverage[-1].mean():.3f}")
Model with recombination effects:
>>> model = DiffusionViaND(AR=20, p_stick0=0.03, p_rec0=0.01, p_rec1=0.05)
>>> times, coverage = model.run_until_cov(max_cov=0.95)
>>> print(f"Time to reach 95% coverage: {times[-1]:.3f}")
Notes
-----
The model uses Knudsen diffusion to describe precursor transport
inside the circular via. The governing equations are solved using
a finite difference method with banded matrix solver for efficiency.
All time values in the model are normalized by a characteristic
diffusion time scale.
"""
model_kwd = ["dose", "nondim"]
def __init__(self, AR, p_stick0, p_rec0=0, p_rec1=0):
self.AR = AR
self.p_stick0 = p_stick0
self.p_rec0 = p_rec0
self.p_rec1 = p_rec1
self._nsegments = 4
@property
def dz(self):
"""size of the elements (normalized to the diameter) used to solve the transport equation"""
return 1/self._nsegments
@property
def nsegments(self):
"""Number of discretized elements per unit aspect ratio"""
return self._nsegments
[docs]
def run(self, N=None, max_time=1, save_every=0.2, dt=0.05):
"""Run simulation for a specified normalized time period
Executes the diffusion-reaction model for precursor transport and
surface coverage evolution inside a high aspect ratio circular via.
The simulation runs until the specified maximum normalized time is
reached, saving coverage profiles at regular time intervals.
Parameters
----------
N : int, optional
Number of discretized segments along the via depth. If None
(default), automatically calculated as 4 * AR to ensure adequate
spatial resolution. Higher values provide better accuracy but
increase computation time.
max_time : float, optional
Maximum normalized time for the simulation. Default is 1.0.
Represents the duration of precursor exposure in normalized units.
Must be positive.
save_every : float, optional
Normalized time interval at which coverage profiles are saved.
Default is 0.2. Smaller values provide more time resolution but
increase memory usage. Must be positive and less than max_time.
dt : float, optional
Time step size for numerical integration (dimensionless).
Default is 0.05. Smaller values improve accuracy but increase
computation time. Must be positive and smaller than save_every.
Returns
-------
times : list of float
List of normalized times corresponding to saved coverage profiles.
Length matches the coverage list.
coverage : list of ndarray
List of coverage arrays at saved time points. Each array has
shape (N,) representing the coverage profile along the via depth,
from the entrance (index 0) to the bottom (index N-1). Values
are bounded between 0 and 1.
Examples
--------
Run simulation with default parameters:
>>> model = DiffusionViaND(AR=10, p_stick0=0.05)
>>> times, coverage = model.run()
>>> print(f"Number of saved profiles: {len(coverage)}")
Number of saved profiles: 6
Run with custom time parameters and higher resolution:
>>> model = DiffusionViaND(AR=15, p_stick0=0.03)
>>> times, coverage = model.run(N=100, max_time=3.0, save_every=0.5)
>>> final_coverage = coverage[-1]
>>> print(f"Coverage at bottom: {final_coverage[-1]:.3f}")
See Also
--------
run_until_cov : Run simulation until target coverage is reached
"""
if N is None:
N = int(self._nsegments*self.AR)
return solve_until(self.AR, N, self.p_stick0, self.p_rec0, self.p_rec1, max_time, save_every, dt)
[docs]
def run_until_cov(self, N=None, max_cov=0.99, save_every=0.2, dt=0.05):
"""Run simulation until target mean coverage is reached
Executes the diffusion-reaction model for precursor transport and
surface coverage evolution inside a high aspect ratio circular via.
The simulation continues until the mean coverage across the via
reaches the specified target value, saving coverage profiles at
regular coverage intervals.
Parameters
----------
N : int, optional
Number of discretized segments along the via depth. If None
(default), automatically calculated as 4 * AR to ensure adequate
spatial resolution. Higher values provide better accuracy but
increase computation time.
max_cov : float, optional
Target mean coverage at which the simulation stops. Default is 0.99.
Represents the spatially-averaged fractional surface coverage.
Must be between 0 and 1.
save_every : float, optional
Coverage interval at which profiles and times are saved.
Default is 0.2. For example, with default value, profiles are
saved when mean coverage reaches 0.2, 0.4, 0.6, 0.8, and the
final target. Must be positive and less than max_cov.
dt : float, optional
Time step size for numerical integration (dimensionless).
Default is 0.05. Smaller values improve accuracy but increase
computation time.
Returns
-------
times : list of float
List of normalized times corresponding to saved coverage profiles.
Times increase monotonically. Length matches the coverage list.
coverage : list of ndarray
List of coverage arrays at saved coverage intervals. Each array
has shape (N,) representing the coverage profile along the via
depth, from the entrance (index 0) to the bottom (index N-1).
Values are bounded between 0 and 1.
Examples
--------
Run simulation until 90% mean coverage:
>>> model = DiffusionViaND(AR=10, p_stick0=0.5)
>>> times, coverage = model.run_until_cov(max_cov=0.9)
>>> print(f"Time to reach 90% coverage: {times[-1]:.3f}")
Time to reach 90% coverage: 2.345
Save coverage profiles every 10% increment:
>>> model = DiffusionViaND(AR=15, p_stick0=0.3, p_rec0=0.1)
>>> times, coverage = model.run_until_cov(max_cov=0.95, save_every=0.1)
See Also
--------
run : Run simulation for a specified time period
"""
if N is None:
N = int(self._nsegments*self.AR)
return solve_until_cov(self.AR, N, self.p_stick0, self.p_rec0, self.p_rec1, max_cov, save_every, dt)