"""Defines discretization classes for FDM simulation.
.. autosummary::
:toctree: discretization
Discretization
DiscretizationEBBVertic
DiscretizationEBBVerticConst
DiscretizationEBBVerticTimeDepend
"""
import abc
import warnings
from numpy import linspace, ones, zeros
from scipy.sparse import SparseEfficiencyWarning, csc_matrix, diags, eye
from traitlets import Float, Instance
from .abstract_traits import ABCHasTraits
from .boundary import PMLRailDampVertic
from .track import (
ArrangedBallastedSingleRailTrack,
ArrangedSlabSingleRailTrack,
ContBallastedSingleRailTrack,
ContSlabSingleRailTrack,
SimplePeriodicBallastedSingleRailTrack,
SimplePeriodicSlabSingleRailTrack,
Track,
)
[docs]
class Discretization(ABCHasTraits):
r"""Abstract base class for discretization classes."""
[docs]
@abc.abstractmethod
def validate_discretization(self):
"""Validate the discretization."""
[docs]
class DiscretizationEBBVertic(Discretization):
r"""Abstract base class for FDM discretization according to :cite:t:`stampka2022a`.
Discretizes the differential equation and can be applied either with constant or time-dependent
parameters, which is the case, for example, with a moving sound source.
Attributes
----------
track : Track
Track instance.
dt : float
Step size in time :math:`[s]`.
req_simt : float
Requested simulation time :math:`[s]`.
bx : float
Stability coefficient for dx calculation (must be :math:`b_x \geq 1`) :math:`[-]`.
nt : int
Number of time steps :math:`[-]`.
sim_t : float
Actual simulation time :math:`[s]`.
dx : float
Step size in space :math:`[m]`.
bx_upd : float
Updated stability coefficient :math:`[-]`.
nx : int
Number of spatial steps :math:`[-]`.
bound : PMLRailDampVertic
Boundary instance.
n_bound : int
Number of spatial steps in single sided boundary domain :math:`[-]`.
pml : numpy.ndarray
Damping array for boundary domain.
A : scipy.sparse.csc_matrix
Coefficient matrix A.
B : scipy.sparse.csc_matrix
Coefficient matrix B.
C : scipy.sparse.csc_matrix
Coefficient matrix C.
"""
track = Instance(Track)
bound = Instance(PMLRailDampVertic)
dt = Float(default_value=2e-5)
req_simt = Float(default_value=0.4)
bx = Float(default_value=1.0)
[docs]
def calc_grid(self):
"""Calculate grid parameters."""
self.nt = int(self.req_simt / self.dt)
self.sim_t = self.nt * self.dt
dx_min = (self.bx * ((self.track.rail.E * self.track.rail.Iyr) /
(6 * self.track.rail.mr)) ** (1 / 4) * self.dt ** (1 / 2))
self.dx = 0.6 / (0.6 // dx_min)
self.bx_upd = self.dx / (((self.track.rail.E * self.track.rail.Iyr) /
(6 * self.track.rail.mr)) ** (1 / 4) * self.dt ** (1 / 2))
self.nx = int(self.track.l_track / self.dx) + 1
[docs]
def calc_bound(self):
"""Calculate boundary properties."""
youm = self.track.rail.E
shearm = self.track.rail.Iyr
mr = self.track.rail.mr
# Characteristic numerical value
r = (youm * shearm) / mr * self.dt ** 2 / self.dx ** 4
# Maximum value of damping coefficient in boundary domain
drbc = r / 2 * mr / self.dt
# Lenght of single sided boundary domain
self.n_bound = int(self.bound.l_bound / self.dx)
# Grid points in boundary domain
xbc = linspace(0, self.bound.l_bound, self.n_bound)
# Calculate damping array for boundary domain
self.pml = self.bound.pml(drbc, xbc)
[docs]
def build_matrix(self, vec_dr, vec_sp, vec_dp, vec_ms, vec_sb, vec_db):
"""Build matrices A, B, and C according to :cite:t:`stampka2022a`.
Parameters
----------
vec_dr : numpy.ndarray
Rail damping vector.
vec_sp : numpy.ndarray
Pad stiffness vector.
vec_dp : numpy.ndarray
Pad damping vector.
vec_ms : numpy.ndarray
Sleeper/Slab mass vector.
vec_sb : numpy.ndarray
Ballast stiffness vector.
vec_db : numpy.ndarray
Ballast damping vector.
Returns
-------
None
"""
# Suppressing warning
warnings.simplefilter("ignore", category=SparseEfficiencyWarning)
# simplification factor
r = ((self.track.rail.E * self.track.rail.Iyr) * self.dt ** 2 /
(2 * self.track.rail.mr * self.dx ** 4))
# Coefficient matrix for x'''' (4th derivative)
D_diagonals = [ones(self.nx - 2), # noqa: N806
(-4) * ones(self.nx - 1),
6 * ones(self.nx),
(-4) * ones(self.nx - 1),
ones(self.nx - 2)]
D = diags(D_diagonals, [-2, -1, 0, 1, 2]) # noqa: N806
Eye = eye(self.nx) # noqa: N806
A11_1_diagonals = self.dt / self.track.rail.mr * (vec_dr + vec_dp) # noqa: N806
A11_1_diagonals += self.dt ** 2 / (2 * self.track.rail.mr) * vec_sp # noqa: N806
A11_1 = diags([A11_1_diagonals], [0]) # noqa: N806
A11 = (r * D + Eye + A11_1).tocsc() # noqa: N806
B11_1_diagonals = self.dt / self.track.rail.mr * (vec_dr + vec_dp) # noqa: N806
B11_1 = diags([B11_1_diagonals], [0]) # noqa: N806
B11 = (2 * Eye + B11_1).tocsc() # noqa: N806
C11_1_diagonals = self.dt ** 2 / (2 * self.track.rail.mr) * vec_sp # noqa: N806
C11_1 = diags([C11_1_diagonals], [0]) # noqa: N806
C11 = (-(Eye + C11_1 + r * D)).tocsc() # noqa: N806
A12_diagonals = -self.dt / self.track.rail.mr * vec_dp # noqa: N806
A12_diagonals += -self.dt ** 2 / (2 * self.track.rail.mr) * vec_sp # noqa: N806
A12 = diags([A12_diagonals], [0]).tocsc() # noqa: N806
A21_diagonals = -self.dt * vec_dp / vec_ms # noqa: N806
A21_diagonals += -self.dt ** 2 / (2 * vec_ms) * vec_sp # noqa: N806
A21 = diags([A21_diagonals], [0]).tocsc() # noqa: N806
A22_1_diagonals = self.dt * ((vec_dp + vec_db) / vec_ms) # noqa: N806
A22_1_diagonals += self.dt ** 2 / (2 * vec_ms) * (vec_sp + vec_sb) # noqa: N806
A22_1 = diags([A22_1_diagonals], [0]) # noqa: N806
A22 = (Eye + A22_1).tocsc() # noqa: N806
B12_diagonals = -self.dt / self.track.rail.mr * vec_dp # noqa: N806
B12 = diags([B12_diagonals], [0]).tocsc() # noqa: N806
B21_diagonals = -self.dt * vec_dp / vec_ms # noqa: N806
B21 = diags([B21_diagonals], [0]).tocsc() # noqa: N806
B22_1_diagonals = self.dt * (vec_db + vec_dp) / vec_ms # noqa: N806
B22_1 = diags([B22_1_diagonals], [0]) # noqa: N806
B22 = (2 * Eye + B22_1).tocsc() # noqa: N806
C12_diagonals = self.dt ** 2 / (2 * self.track.rail.mr) * vec_sp # noqa: N806
C12 = diags([C12_diagonals], [0]).tocsc() # noqa: N806
C21_diagonals = self.dt ** 2 / (2 * vec_ms) * vec_sp # noqa: N806
C21 = diags([C21_diagonals], [0]).tocsc() # noqa: N806
C22_1_diagonals = self.dt ** 2 * (vec_sp + vec_sb) / (2 * vec_ms) # noqa: N806
C22_1 = diags([C22_1_diagonals], [0]) # noqa: N806
C22 = (-(Eye + C22_1)).tocsc() # noqa: N806
self.A = csc_matrix((2 * self.nx, 2 * self.nx)) # noqa: N806
self.A[0:self.nx, 0:self.nx] = A11
self.A[0:self.nx, self.nx:2 * self.nx] = A12
self.A[self.nx:2 * self.nx, 0:self.nx] = A21
self.A[self.nx:2 * self.nx, self.nx:2 * self.nx] = A22
self.B = csc_matrix((2 * self.nx, 2 * self.nx)) # noqa: N806
self.B[0:self.nx, 0:self.nx] = B11
self.B[0:self.nx, self.nx:2 * self.nx] = B12
self.B[self.nx:2 * self.nx, 0:self.nx] = B21
self.B[self.nx:2 * self.nx, self.nx:2 * self.nx] = B22
self.C = csc_matrix((2 * self.nx, 2 * self.nx)) # noqa: N806
self.C[0:self.nx, 0:self.nx] = C11
self.C[0:self.nx, self.nx:2 * self.nx] = C12
self.C[self.nx:2 * self.nx, 0:self.nx] = C21
self.C[self.nx:2 * self.nx, self.nx:2 * self.nx] = C22
[docs]
@abc.abstractmethod
def validate_discretization_stampka(self):
"""Validate the discretization according to Stampka."""
[docs]
class DiscretizationEBBVerticConst(DiscretizationEBBVertic):
r"""Discretization with non-time-dependent parameters according to :cite:t:`stampka2022a`.
The parameters are constant over time. Only applicable for non-moving sound sources
and linear superstructure properties.
Attributes
----------
track : Track
Track instance.
dt : float
Step size in time :math:`[s]`.
req_simt : float
Requested simulation time :math:`[s]`.
bx : float
Stability coefficient for dx calculation (must be :math:`b_x \geq 1`) :math:`[-]`.
nt : int
Number of time steps :math:`[-]`.
sim_t : float
Actual simulation time :math:`[s]`.
dx : float
Step size in space :math:`[m]`.
bx_upd : float
Updated stability coefficient :math:`[-]`.
nx : int
Number of spatial steps :math:`[-]`.
bound : PMLRailDampVertic
Boundary instance.
A : scipy.sparse.csc_matrix
Coefficient matrix A.
B : scipy.sparse.csc_matrix
Coefficient matrix B.
C : scipy.sparse.csc_matrix
Coefficient matrix C.
vec_dr : numpy.ndarray
Rail damping vector.
vec_sp : numpy.ndarray
Pad stiffness vector.
vec_dp : numpy.ndarray
Pad damping vector.
vec_ms : numpy.ndarray
Sleeper/Slab mass vector.
vec_sb : numpy.ndarray
Ballast stiffness vector.
vec_db : numpy.ndarray
Ballast damping vector.
"""
[docs]
def validate_discretization(self):
"""Validate the discretization."""
[docs]
def validate_discretization_stampka(self):
"""Validate the discretization according to Stampka."""
def __init__(self, *args, **kwargs):
"""Calculate superstructure property vectors."""
super().__init__(*args, **kwargs)
self.calc_grid()
self.calc_bound()
self.initialize_vectors()
self.add_boundary_conditions()
self.build_superstructure_vectors()
self.build_matrix(self.vec_dr, self.vec_sp, self.vec_dp, self.vec_ms, self.vec_sb, self.vec_db)
[docs]
def initialize_vectors(self):
"""Initialize the vectors."""
self.vec_dr = self.track.rail.dr * ones(self.nx)
self.vec_sp = zeros(self.nx)
self.vec_dp = zeros(self.nx)
self.vec_ms = ones(self.nx) # ones instead of zeros to avoid division by zero
self.vec_sb = zeros(self.nx)
self.vec_db = zeros(self.nx)
[docs]
def add_boundary_conditions(self):
"""
Add boundary conditions to the rail damping vector.
This method modifies the rail damping vector (`vec_dr`) by adding the boundary conditions
from the Perfectly Matched Layer (PML) at both ends of the grid.
"""
# Boundary condition left side
self.vec_dr[:self.pml.size] += self.pml[::-1]
# Boundary condition right side
self.vec_dr[-self.pml.size:] += self.pml
[docs]
def build_superstructure_vectors(self):
"""Handle track-specific logic.
This method initializes the superstructure property vectors based on the type of track.
Parameters
----------
None
Returns
-------
None
"""
if isinstance(self.track, ContSlabSingleRailTrack):
# Properties are assigned to each grid point
self.vec_sp += self.track.pad.sp[0]
self.vec_dp += self.track.pad.dp[0]
self.vec_ms += self.track.slab.ms
elif isinstance(self.track, SimplePeriodicSlabSingleRailTrack | ArrangedSlabSingleRailTrack):
self.build_discrete_slab_track()
elif isinstance(self.track, ContBallastedSingleRailTrack):
# Properties are assigned to each grid point
self.vec_sp += self.track.pad.sp[0]
self.vec_dp += self.track.pad.dp[0]
self.vec_ms += self.track.slab.ms
self.vec_sb += self.track.ballast.sb[0]
self.vec_db += self.track.ballast.db[0]
elif isinstance(self.track, SimplePeriodicBallastedSingleRailTrack | ArrangedBallastedSingleRailTrack):
self.build_discrete_ballasted_track()
else:
msg = "Track type not recognized!"
raise ValueError(msg)
[docs]
def build_discrete_slab_track(self):
"""
Build discrete slab track.
Properties are assigned to the corresponding mounting positions.
Parameters
----------
None
Returns
-------
None
"""
# Get mounting positions as list
mount_pos = list(self.track.mount_prop.keys())
for i in mount_pos:
x_ind = int(i / self.dx)
self.vec_sp[x_ind] = self.track.mount_prop[i][0].sp[0] / self.dx
self.vec_dp[x_ind] = self.track.mount_prop[i][0].dp[0] / self.dx
self.vec_ms[x_ind] = self.track.slab.ms / self.dx
[docs]
def build_discrete_ballasted_track(self):
"""Build discrete ballasted track.
Properties are assigned to the corresponding mounting positions.
Parameters
----------
None
Returns
-------
None
"""
# Get mounting positions as list
mount_pos = list(self.track.mount_prop.keys())
for i in mount_pos:
x_ind = int(i / self.dx)
self.vec_sp[x_ind] = self.track.mount_prop[i][0].sp[0] / self.dx
self.vec_dp[x_ind] = self.track.mount_prop[i][0].dp[0] / self.dx
self.vec_ms[x_ind] = self.track.mount_prop[i][1].ms / self.dx
self.vec_sb[x_ind] = self.track.mount_prop[i][2].sb[0] / self.dx
self.vec_db[x_ind] = self.track.mount_prop[i][2].db[0] / self.dx
[docs]
class DiscretizationEBBVerticTimeDepend(DiscretizationEBBVerticConst):
"""
Discretization with time-dependent parameters based on :cite:t:`stampka2022a`.
This class extends :class:`DiscretizationFDMStampkaConst` to handle cases where the parameters
vary over time, such as with a moving sound source or non-linear superstructure properties.
This approach is a extended version of the discretization described in :cite:t:`stampka2022a`.
.. note:: This class is not implemented yet.
"""
[docs]
def validate_discretization(self):
"""Validate the discretization."""
[docs]
def validate_discretization_stampka(self):
"""Validate the discretization according to Stampka."""