Source code for rolland.methods.analytical

"""Contains analytical methods that allow comparison with FDM results.

.. autosummary::
    :toctree: analytical/

    AnalyticalMethods
    EBBCont1LSupp
    EBBCont2LSupp
    TBDiscr
    TSDiscr1LSupp
    TSDiscr2LSupp
"""
import abc

from numpy import array, exp, eye, lib, linalg, newaxis, pi, real, sqrt, squeeze, zeros
from traitlets import Float, Instance, List, Union, observe
from traittypes import Array

from rolland.track import (
    ABCHasTraits,
    ContBallastedSingleRailTrack,
    ContSlabSingleRailTrack,
    DiscrBallastedSingleRailTrack,
    DiscrSlabSingleRailTrack,
)


[docs] class AnalyticalMethods(ABCHasTraits): r"""Abstract base class for analytical methods. Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. mobility : numpy.ndarray Calculated mobility of the track :math:`[m/N]`. """ f = Array() force = Array(default_value=1.0) x_excit = Float(default_value=0.0) x = Union([Float(default_value=0.0), List()]) mobility = Array() def __init__(self, **kwargs): """ Initialize the AnalyticalMethods class. Parameters ---------- **kwargs : dict Keyword arguments to initialize the class attributes. """ super().__init__(**kwargs) self.compute_mobility() @observe('f', 'force', 'x_excit', 'x') def _update_on_change(self, change): self.compute_mobility() @observe('x_excit') def _set_default_x(self, change): if self.x == 0.0: self.x = self.x_excit @property def omega(self): """Calculate the angular frequency.""" return 2 * pi * self.f
[docs] @abc.abstractmethod def compute_mobility(self): """ Compute the mobility of the track. Raises ------ NotImplementedError If the method is not implemented in a subclass. """ message = "Subclasses must implement compute_mobility." raise NotImplementedError(message)
[docs] class EBBCont1LSupp(AnalyticalMethods): r"""Method for continuous slab single rail track according to :cite:t:`thompson2024j`. Utilizes a single-layer support with continuous track properties, applying Euler-Bernoulli beam theory. The excitation is stationary, and the corresponding method calculates the track's mobility for the postions specified. Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. track : ContSlabSingleRailTrack Track instance. """ track = Instance(ContSlabSingleRailTrack)
[docs] def compute_mobility(self): r""" Compute the mobility of the track. This method calculates the mobility of the track using the given parameters and the analytical solution for a continuous slab single rail track. Attributes ---------- mr : float Mass per unit length of the rail :math:`[kg/m]`. sp : float Stiffness of continuous pad :math:`[N/m^2]`. dp : float Viscous damping coefficient of continuous pad :math:`[Ns/m^2]`. omega_0 : float Resonance frequency rail <--> foundation :math:`[Hz]`. k_p : numpy.ndarray Wave number of propagating wave :math:`[1/m]`. abs_x : numpy.ndarray Absolute distance between given positions and the excitation point :math:`[m]`. mobility : numpy.ndarray Calculated mobility of the track :math:`[m/N]`. Returns ------- mobility : numpy.ndarray The mobility of the track :math:`[m/N]`. omega_0 : float The resonance frequency rail <--> foundation :math:`[Hz]`. """ mr = self.track.rail.mr sp = self.track.pad.sp[0] dp = self.track.pad.dp[0] # Eq. 3.5 self.omega_0 = sqrt(sp / mr) # Eq. 3.6 k_p = ((self.omega ** 2 * mr - sp - 1j * self.omega * dp) / (self.track.rail.E * self.track.rail.Iyr)) ** (1/4) abs_x = abs(array(self.x, ndmin=1)[:, None] - self.x_excit) # Broadcast x over omega term1 = exp(-1j * k_p * abs_x) term2 = -1j * exp(-k_p * abs_x) # Eq. 3.17 / 3.18 self.mobility = (self.omega / (4 * (self.track.rail.E * self.track.rail.Iyr) * k_p ** 3) * (term1 + term2)) self.mobility = squeeze(self.mobility) # Remove axes of length one
[docs] class EBBCont2LSupp(AnalyticalMethods): r"""Method for continuous ballasted single rail track according to :cite:t:`thompson2024j`. Utilizes a double-layer support with continuous track properties, applying Euler-Bernoulli beam theory. The excitation is stationary, and the corresponding method calculates the track's mobility for the postions specified. Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. track : ContBallastedSingleRailTrack Track instance. omega_0 : float Resonance frequency rail <--> foundation :math:`[Hz]`. mobility : numpy.ndarray Mobility matrix :math:`[m/N]`. """ track = Instance(ContBallastedSingleRailTrack)
[docs] def compute_mobility(self): r""" Compute the mobility of the track. This method calculates the mobility of the track using the given parameters and the analytical solution for a continuous slab single rail track. Attributes ---------- mr : float Mass per unit length of the rail :math:`[kg/m]`. sp : float Stiffness of continuous pad :math:`[N/m^2]`. sb : float Stiffness of continuous ballast :math:`[N/m^2]`. dp : float Viscous damping coefficient of continuous pad :math:`[Ns/m^2]`. db : float Viscous damping coefficient of continuous ballast :math:`[Ns/m^2]`. ms : float Mass per unit length of the sleeper :math:`[kg/m]`. Returns ------- mobility : numpy.ndarray The mobility of the track :math:`[m/N]`. omega_0 : float The resonance frequency rail <--> foundation :math:`[Hz]`. omega_1 : float The resonance frequency ballast <--> slab :math:`[Hz]`. omega_2 : float The resonance frequency rail <--> slab :math:`[Hz]`. """ mr = self.track.rail.mr sp = self.track.pad.sp[0] sb = self.track.ballast.sb[0] dp = self.track.pad.dp[0] db = self.track.ballast.db[0] ms = self.track.slab.ms self.omega_0 = sqrt(sp / mr) # Eq. 3.47 self.omega_1 = sqrt(sb / ms) # Eq. 3.44 self.omega_2 = sqrt((sp + sb) / ms) # Eq. 3.44 # Eq. 3.40 sp_tot = sp + 1j * self.omega * dp sb_tot = sb + 1j * self.omega * db s_tot = (sp_tot * (sb_tot - ms * self.omega ** 2)) / (sp_tot + sb_tot - ms * self.omega ** 2) # Eq. 3.6 k_p = ((self.omega ** 2 * mr - s_tot) / (self.track.rail.E * self.track.rail.Iyr)) ** (1/4) abs_x = abs(array(self.x, ndmin=1)[:, None] - self.x_excit) # Broadcast x over omega # Broadcast x over omega term1 = exp(-1j * k_p * abs_x) term2 = -1j * exp(-k_p * abs_x) # Eq. 3.17 / 3.18 self.mobility = (self.omega / (4 * (self.track.rail.E * self.track.rail.Iyr) * k_p ** 3) * (term1 + term2)) self.mobility = squeeze(self.mobility) # Remove axes of length one
[docs] class TBDiscr(AnalyticalMethods): r"""Base class for analytical solutions of a discrete single rail track. Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. """
[docs] @abc.abstractmethod def validate_method(self): """Validate method."""
[docs] def calc_greens_func(self, xm, xn, k_p, k_d, f_p, f_d): """ Calculate Greens function of free Timoshenko Beam (eq. 3.69). Parameters ---------- xm : numpy.ndarray Positions of the first set of points :math:`[m]`. xn : numpy.ndarray Positions of the second set of points :math:`[m]`. k_p : numpy.ndarray Wave number of propagating wave :math:`[1/m]`. k_d : numpy.ndarray Wave number of decaying wave :math:`[1/m]`. f_p : numpy.ndarray Factor for propagating wave :math:`[-]`. f_d : numpy.ndarray Factor for decaying wave :math:`[-]`. Returns ------- numpy.ndarray Calculated Greens function. """ dist = abs(xm - xn) term1 = exp(-1j * k_p * dist) term2 = exp(-1j * k_d * dist) term3 = 1 return f_p * term1 + (f_d * term2) * term3
[docs] def compute_mobility_common(self, track, ms, sb, etab): """ Compute common mobility for 1-layer and 2-layer support. Parameters ---------- track : object Track instance containing rail and pad properties. ms : float Mass per unit length of the sleeper or slab [kg/m]. sb : float Stiffness of the ballast [N/m^2]. etab : float Damping coefficient of the ballast. Attributes ---------- f_0 : float Resonance frequency rail <--> pads [Hz]. f_1 : float Resonance frequency ballast <--> sleepers [Hz]. f_2 : float Resonance frequency rail <--> sleepers [Hz]. mobility : numpy.ndarray Calculated mobility of the track [m/N]. """ mr = track.rail.mr rho = track.rail.rho etap = track.pad.etap kap = track.rail.kap[0] youm = track.rail.E shearm = track.rail.G ar = track.rail.Ar aream = track.rail.Iyr sp = track.pad.sp[0] sb = sb # Positions of point forces [m] x_n = array(list(track.mount_prop.keys())) # Resonance frequencies self.f_0 = real(sqrt(sp / mr)) / (2 * pi) self.f_1 = real(sqrt(sb / ms)) / (2 * pi) self.f_2 = real(sqrt(sp + sb)) / (2 * pi) # Dynamic stiffness (eq. 3.68) impend = ((sp * (1 + (1j * etap)) * ((sb * (1 + (1j * etab))) - (ms * (self.omega ** 2)))) / ((sp * (1 + (1j * etap))) + (sb * (1 + (1j * etab))) - (ms * (self.omega ** 2)))) # Eq. 3.72 c1 = ((shearm * kap * ar) / (youm * aream)) - ((rho * self.omega ** 2 * aream) / (youm * aream)) # Eq. 3.73 c2 = - ((mr * self.omega ** 2) / (shearm * kap * ar)) - ((rho * self.omega ** 2 * aream) / (youm * aream)) # Eq. 3.74 c3 = ((mr * self.omega ** 2) / (youm * aream)) * ((rho * self.omega ** 2 * aream) / (shearm * kap * ar) - 1) # Eq. 3.71 k__p = -1 / 2 * c2 + 1 / 2 * sqrt(c2 ** 2 - 4 * c3) k__d = -1 / 2 * c2 - 1 / 2 * sqrt(c2 ** 2 - 4 * c3) k_p = lib.scimath.sqrt(k__p) k_d = -1 * lib.scimath.sqrt(k__d) # Eq. 3.70 f_p = (-1j / (shearm * ar * kap)) * ((k_p ** 2 + c1) / (4 * k_p ** 3 + 2 * k_p * c2)) f_d = (-1j / (shearm * ar * kap)) * ((k_d ** 2 + c1) / (4 * k_d ** 3 + 2 * k_d * c2)) # Displacements at reaction points uxn = zeros((self.f.size, x_n.size), dtype=complex) # Displacements at requested points self.ux = zeros((array(self.x).size, self.f.size), dtype=complex) for f in range(self.f.size): # Greens function matrix reaction points <--> reaction points greensm_mn = self.calc_greens_func(x_n[:, newaxis], x_n[newaxis, :], k_p[f], k_d[f], f_p[f], f_d[f]) # Greens function matrix reaction points <--> excitation point greensm_exc = self.calc_greens_func(x_n, self.x_excit, k_p[f], k_d[f], f_p[f], f_d[f]) # m = I + impend * greensm_mn m = eye(x_n.size) + impend[f] * greensm_mn # Calculate displacements at reaction points (eq. 3.76) uxn[f, :] = linalg.solve(m, greensm_exc) # Calculate displacements at requested points (eq. 3.77) for p in range(array(self.x, ndmin=1).size): # Greens function matrix requested points <--> reaction points greensm_xn = self.calc_greens_func(array(self.x, ndmin=1)[p], x_n, k_p[f], k_d[f], f_p[f], f_d[f]) # Greens function matrix requested points <--> excitation point greensm_xf = self.calc_greens_func(array(self.x, ndmin=1)[p], self.x_excit, k_p[f], k_d[f], f_p[f], f_d[f]) self.ux[p, f] = - impend[f] * greensm_xn.dot(uxn[f, :]) + greensm_xf self.mobility = (self.ux * self.omega * 1j) / self.force self.mobility = squeeze(self.mobility) # Remove axes of length one
[docs] class TSDiscr1LSupp(TBDiscr): r"""Method for discrete slab track according to :cite:t:`thompson2024j` and :cite:t:`heckl1995`. Utilizes a single-layer support with discrete track properties, applying Timoshenko beam theory. The excitation is a non-moving sound source. The corresponding method calculates the track's mobility for the postions specified. .. caution:: This method is an implementation of :cite:t:`thompson2024j` which is a modified version of :cite:t:`heckl1995`. Theoretically, the results should be identical, but Heckl's work contains the following mistakes, which lead to incorrect results: 1. Missing negativ sign in the definition of the decaying wave number (Eq. 2b). 2. Shear modulus :math:`G` needs to be substituted by :math:`G * \kappa` Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. track : DiscrSlabSingleRailTrack Track instance. omega_0 : float Resonance frequency rail <--> foundation :math:`[Hz]`. """ track = Instance(DiscrSlabSingleRailTrack)
[docs] def validate_method(self): """Validate method."""
[docs] def compute_mobility(self): """ Compute the mobility of the track. This method calculates the mobility of the track using the given parameters and the analytical solution for a discrete slab track. Attributes ---------- mobility : numpy.ndarray Calculated mobility of the track :math:`[m/N]`. """ self.compute_mobility_common(self.track, self.track.slab.ms, 1e20, 0)
[docs] class TSDiscr2LSupp(TBDiscr): r"""Method for discrete ballasted track according to :cite:t:`thompson2024j`. Utilizes a double-layer support with discrete track properties, applying Timoshenko beam theory. The excitation is a non-moving sound source. The corresponding method calculates the track's mobility for the postions specified. .. caution:: This method is an implementation of :cite:t:`thompson2024j` which is a modified version of :cite:t:`heckl1995`. Theoretically, the results should be identical, but Heckl's work contains the following mistakes, which lead to incorrect results: 1. Missing negativ sign in the definition of the decaying wave number (Eq. 2b). 2. Shear modulus :math:`G` needs to be substituted by :math:`G * \kappa` Attributes ---------- f : numpy.ndarray Excitation frequencies :math:`[Hz]`. force : numpy.ndarray Force amplitude corresponding to the excitation frequencies :math:`[N]`. x : float or list Distances to the excitation point :math:`[m]`. x_excit : numpy.ndarray Excitation point :math:`[m]`. track : DiscrSlabSingleRailTrack Track instance. omega_0 : float Resonance frequency rail <--> foundation :math:`[Hz]`. omega_1 : float Resonance frequency ballast <--> slab :math:`[Hz]`. omega_2 : float Resonance frequency rail <--> slab :math:`[Hz]`. """ track = Instance(DiscrBallastedSingleRailTrack)
[docs] def validate_method(self): """Validate method."""
[docs] def compute_mobility(self): """ Compute the mobility of the track. This method calculates the mobility of the track using the given parameters and the analytical solution for a discrete ballasted track. Attributes ---------- mobility : numpy.ndarray Calculated mobility of the track :math:`[m/N]`. """ self.compute_mobility_common(self.track, self.track.sleeper.ms, self.track.ballast.sb[0], self.track.ballast.etab)