"""Defines track structure and arrangement.
.. autosummary::
:toctree: track
Track
SingleRailTrack
SlabSingleRailTrack
ContSlabSingleRailTrack
DiscrSlabSingleRailTrack
SimplePeriodicSlabSingleRailTrack
ArrangedSlabSingleRailTrack
BallastedSingleRailTrack
ContBallastedSingleRailTrack
DiscrBallastedSingleRailTrack
SimplePeriodicBallastedSingleRailTrack
ArrangedBallastedSingleRailTrack
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from decimal import Decimal
from .arrangement import Arrangement
from .components import Ballast, ContPad, DiscrPad, Rail, Slab, Sleeper
[docs]
class Track(ABC):
r"""Abstract base class for track classes."""
@abstractmethod
def _abstract(self) -> None:
"""Prevents instantiation of abstract classes."""
[docs]
@dataclass(kw_only=True)
class SingleRailTrack(Track):
r"""Abstract base class for single rail track classes.
Attributes
----------
rail : Rail
Rail instance.
"""
rail: Rail
[docs]
@dataclass(kw_only=True)
class SlabSingleRailTrack(SingleRailTrack):
r"""Abstract base class for slab single rail track classes.
Slab mass is set to a very large number to avoid displacement and simulate a rigid slab.
Attributes
----------
rail : Rail
Rail instance.
slab : Slab, default=Slab(ms=1e20)
Slab instance.
"""
slab: Slab = field(default_factory=lambda: Slab(ms=1e20), metadata={'default_repr': 'Slab(ms=1e20)'})
[docs]
@dataclass(kw_only=True)
class ContSlabSingleRailTrack(SlabSingleRailTrack):
r"""Single rail slab track with continuous support.
All superstructure properties are continuous along the track. The slab is assumed to be rigid.
+------------------+-----------+--------------------+-------------+
| Layer of Support | Component | Condition | Variability |
+==================+===========+====================+=============+
| / | rail | continuous | no |
+------------------+-----------+--------------------+-------------+
| 1st | pads | continuous | no |
+------------------+-----------+--------------------+-------------+
| 1st/2nd | slab | continuous (rigid) | no |
+------------------+-----------+--------------------+-------------+
| 2nd | ballast | / | / |
+------------------+-----------+--------------------+-------------+
Attributes
----------
rail : Rail
Rail instance.
slab : Slab
Slab instance.
pad : ContPad
Continuous pad instance.
l_track : float, default=100.0
Track length :math:`[m]`. (May change slightly after discretization.
The inclusion of boundary and calculation domain is required).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import ContPad, Slab
>>> from rolland.track import ContSlabSingleRailTrack
>>> thepad = ContPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> theslab = Slab(ms = 250)
>>> track = ContSlabSingleRailTrack(rail = UIC60, pad = thepad, slab = theslab, l_track = 145)
...
"""
pad: ContPad
l_track: float = 100.0
def _abstract(self) -> None:
pass
[docs]
@dataclass(kw_only=True)
class DiscrSlabSingleRailTrack(SlabSingleRailTrack):
r"""Abstract base class for discrete slab single rail track classes.
The pad and sleeper properties are discrete and the slab is assumed to be rigid.
Attributes
----------
rail : Rail
Rail instance.
slab : Slab
Slab instance.
pad : DiscrPad
Discrete pad instance.
mount_prop : dict[float, tuple[DiscrPad, None, None]]
Dictionary for discrete mounting positions (x-> (Pad, None)).
"""
pad: DiscrPad
# Dictionary for discrete mounting positions (x-> (Pad)).
# May have nonuniform properties.
# None values are placeholders
mount_prop: dict[float, tuple[DiscrPad, None, None]] = field(default_factory=dict, metadata={"default_repr": "{}"})
def __repr__(self):
"""Represent mounting properties as string."""
st = ""
for x in sorted(self.mount_prop.keys()):
p, s, b= self.mount_prop[x]
st += f'{x}, {p.sp}, {s.ms}, {b.sb} \n'
return st
def _on_critical_change(self, change):
"""Invalidate cached results when critical parameters change."""
if hasattr(self, '_cache'):
self._cache.clear()
[docs]
@dataclass(kw_only=True)
class SimplePeriodicSlabSingleRailTrack(DiscrSlabSingleRailTrack):
r"""Single rail slab track with simple periodic support.
All mounting properties are uniform and no variation is allowed. Slab is assumed to be rigid.
+---------+-----------+------------------+-------------+
| Layer | Component | Condition | Variability |
+=========+===========+==================+=============+
| / | rail | continuous | no |
+---------+-----------+------------------+-------------+
| 1st | pads | discrete | no |
+---------+-----------+------------------+-------------+
| 1st/2nd | slab | discrete (rigid) | no |
+---------+-----------+------------------+-------------+
| 2nd | ballast | / | / |
+---------+-----------+------------------+-------------+
Attributes
----------
rail : Rail
Rail instance.
slab : Slab
Slab instance.
pad : DiscrPad
Discrete pad instance.
distance : float, default=0.6
Distance between mounting positions.
num_mount : int, default=100
Number of mounting positions.
mount_prop : dict[float, tuple[DiscrPad, None, None]]
Dictionary for discrete mounting positions (x-> (Pad, None)).
l_track : float, default=100.0
Track length :math:`[m]`. (May change slightly after discretization.
Results from the number of mounting positions and the mounting distances).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import DiscrPad, Slab
>>> from rolland.track import SimplePeriodicSlabSingleRailTrack
>>> thepad = DiscrPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> theslab = Slab(ms = 250)
>>> track = SimplePeriodicSlabSingleRailTrack(
... rail=UIC60,
... pad=thepad,
... slab=theslab,
... distance=0.6,
... num_mount=100)
...
"""
distance: float = 0.6
num_mount: int = 100
def __post_init__(self, *args, **kwargs):
"""post_init method to calculate mounting properties after initialization."""
self.calc_mount_prop()
[docs]
def calc_mount_prop(self, change=None):
"""Calculate the mounting properties."""
self.mount_prop = {}
for _i in range(self.num_mount):
x = float(Decimal(str(_i)) * Decimal(str(self.distance)))
self.mount_prop[x] = (self.pad, None, None)
self.l_track = max(self.mount_prop.keys())
def _abstract(self) -> None:
pass
[docs]
@dataclass(kw_only=True)
class ArrangedSlabSingleRailTrack(DiscrSlabSingleRailTrack):
"""Single rail slab track with varying periodic support.
Variations in the form of periodicaly or stochasticaly varying mounting properties are allowed.
Slab is assumed to be rigid.
+---------+-----------+------------------+---------------------+
| Layer | Component | Condition | Variability |
+=========+===========+==================+=====================+
| / | rail | continuous | no |
+---------+-----------+------------------+---------------------+
| 1st | pads | discrete | periodic/stochastic |
+---------+-----------+------------------+---------------------+
| 1st/2nd | slab | discrete (rigid) | periodic/stochastic |
+---------+-----------+------------------+---------------------+
| 2nd | ballast | / | / |
+---------+-----------+------------------+---------------------+
Attributes
----------
rail : Rail
Rail instance.
slab : Slab
Slab instance.
pad : Arrangement
Arrangement instance containing multiple pads.
distance : Arrangement
Arrangement instance containing multiple distances.
num_mount : int, default=100
Number of mounting positions.
mount_prop : dict[float, tuple[DiscrPad, None, None]]
Dictionary for discrete mounting positions (x-> (Pad, None)).
l_track : float, default=100.0
Track length :math:`[m]`. (May change slightly after discretization.
Results from the number of mounting positions and the mounting distances).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import DiscrPad, Slab
>>> from rolland.arrangement import PeriodicArrangement
>>> from rolland.track import ArrangedSlabSingleRailTrack
>>> thepadA = DiscrPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> thepadB = DiscrPad(sp = [400*10**6, 0], dp = [40000, 0])
>>> theslab = Slab(ms = 250)
>>> pad = PeriodicArrangement(item=[thepadA, thepadB])
>>> distance = PeriodicArrangement(item=[0.65, 0.5])
>>> track = ArrangedSlabSingleRailTrack(
... rail=UIC60,
... pad=pad,
... slab=theslab,
... distance=distance,
... num_mount=100)
...
"""
pad: Arrangement
distance: Arrangement
num_mount: int = 100
def __post_init__(self, *args, **kwargs):
"""post_init method to calculate mounting properties after initialization."""
self.calc_mount_prop()
[docs]
def calc_mount_prop(self, change=None):
"""Calculate the mounting properties."""
x = Decimal(str(0))
for p, d in zip(self.pad.generate(self.num_mount),
self.distance.generate(self.num_mount), strict=False):
self.mount_prop[float(Decimal(str(x)))] = (p, None, None)
x += Decimal(str(d))
self.l_track = max(self.mount_prop.keys())
def _abstract(self) -> None:
pass
[docs]
@dataclass(kw_only=True)
class BallastedSingleRailTrack(SingleRailTrack):
r"""Abstract base class for ballasted single rail track classes.
Attributes
----------
rail : Rail
Rail instance.
ballast : Ballast
Ballast instance.
"""
ballast: Ballast
def _on_critical_change(self, change):
"""Invalidate cached results when critical parameters change."""
if hasattr(self, '_cache'):
self._cache.clear()
[docs]
@dataclass(kw_only=True)
class ContBallastedSingleRailTrack(BallastedSingleRailTrack):
r"""Single rail slab track with ballasted support.
All superstructure properties are continuous along the track.
.. note:: Properties of ballast need to be defined as continious values (per meter).
+---------+-----------+------------+-------------+
| Layer | Component | Condition | Variability |
+=========+===========+============+=============+
| / | rail | continuous | no |
+---------+-----------+------------+-------------+
| 1st | pads | continuous | no |
+---------+-----------+------------+-------------+
| 1st/2nd | slab | continuous | no |
+---------+-----------+------------+-------------+
| 2nd | ballast | continuous | no |
+---------+-----------+------------+-------------+
Attributes
----------
rail : Rail
Rail instance.
pad : ContPad
Continuous pad instance.
slab : Slab
Slab instance.
ballast : Ballast
Ballast instance.
l_track : float, default=100.0
Track length :math:`[m]`. (May change slightly after discretization.
The inclusion of boundary and calculation domain is required).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import ContPad, Slab
>>> from rolland.track import ContBallastedSingleRailTrack
>>> thepad = ContPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> theslab = Slab(ms = 250)
>>> track = ContBallastedSingleRailTrack(rail = UIC60, pad = thepad, slab = theslab)
...
"""
pad: ContPad
slab: Slab
l_track: float = 100.0
def _on_critical_change(self, change):
"""Invalidate cached results when critical parameters change."""
if hasattr(self, '_cache'):
self._cache.clear()
def _abstract(self) -> None:
pass
[docs]
@dataclass(kw_only=True)
class DiscrBallastedSingleRailTrack(BallastedSingleRailTrack):
"""Abstract base class for discrete ballasted single rail track classes.
.. note:: The pad, sleeper and ballast properties are discrete.
Attributes
----------
rail : Rail
Rail instance.
ballast : Ballast
Ballast instance.
mount_prop : dict[float, tuple[DiscrPad, None, None]]
Dictionary for discrete mounting positions (x-> (Pad, Sleeper)).
"""
# Pads and sleepers may have nonuniform properties Dictionary (x-> (Pad, Sleeper))
mount_prop: dict[float, tuple[DiscrPad, None, None]] = field(default_factory=dict, metadata={"default_repr": "{}"})
def __repr__(self):
"""Represent mounting properties as string."""
st = ""
for x in sorted(self.mount_prop.keys()):
p, s, b = self.mount_prop[x]
st += f'{x}, {p.sp}, {s.ms}, {b.sb} \n'
return st
def _on_critical_change(self, change):
"""Invalidate cached results when critical parameters change."""
if hasattr(self, '_cache'):
self._cache.clear()
[docs]
@dataclass(kw_only=True)
class SimplePeriodicBallastedSingleRailTrack(DiscrBallastedSingleRailTrack):
"""Single rail ballasted track with simple periodic support.
All mounting properties are uniform and no variation is allowed.
.. note:: Properties of ballast need to be defined as discrete values.
+---------+-----------+------------+-------------+
| Layer | Component | Condition | Variability |
+=========+===========+============+=============+
| / | rail | continuous | no |
+---------+-----------+------------+-------------+
| 1st | pads | discrete | no |
+---------+-----------+------------+-------------+
| 1st/2nd | sleeper | discrete | no |
+---------+-----------+------------+-------------+
| 2nd | ballast | discrete | no |
+---------+-----------+------------+-------------+
Attributes
----------
rail : Rail
Rail instance.
ballast : Ballast
Ballast instance.
pad : ContPad
Continuous pad instance.
sleeper : Instance of :class:`~rolland.components.sleeper` class
Sleeper instance.
distance : float, default=0.6
Distance between mounting positions.
num_mount : int, default=100
Number of mounting positions.
mount_prop : dict
Dictionary for discrete mounting positions (x-> (Pad, Sleeper)).
l_track : float
Track length :math:`[m]`. (May change slightly after discretization.
Results from the number of mounting positions and the mounting distances).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import DiscrPad, Sleeper
>>> from rolland.track import SimplePeriodicBallastedSingleRailTrack
>>> thepad = DiscrPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> thesleeper = Sleeper(ms = 150)
>>> distance = 0.6
>>> tr = SimplePeriodicBallastedSingleRailTrack(
... rail=UIC60,
... pad=thepad,
... sleeper=thesleeper,
... distance=distance)
"""
sleeper: Sleeper
pad: DiscrPad
ballast: Ballast
distance: float = field(default=0.6)
num_mount: int = field(default=100)
def __post_init__(self, *args, **kwargs):
"""post_init method to calculate mounting properties after initialization."""
self.calc_mount_prop()
[docs]
def calc_mount_prop(self, change=None):
"""Calculate the mounting properties."""
for _i in range(self.num_mount):
# Calculate the mounting position
# Use Decimal to avoid floating-point representation errors
x = float(Decimal(str(_i)) * Decimal(str(self.distance)))
self.mount_prop[x] = (self.pad, self.sleeper, self.ballast)
self.l_track = max(self.mount_prop.keys())
def _abstract(self) -> None:
pass
[docs]
@dataclass(kw_only=True)
class ArrangedBallastedSingleRailTrack(DiscrBallastedSingleRailTrack):
"""Single rail ballasted track with varying periodic support.
Variations in the form of periodicaly or stochasticaly varying mounting properties are allowed.
.. note:: Properties of ballast need to be defined as discrete values.
+---------+-----------+------------+---------------------+
| Layer | Component | Condition | Variability |
+=========+===========+============+=====================+
| / | rail | continuous | no |
+---------+-----------+------------+---------------------+
| 1st | pads | discrete | periodic/stochastic |
+---------+-----------+------------+---------------------+
| 1st/2nd | sleepers | discrete | periodic/stochastic |
+---------+-----------+------------+---------------------+
| 2nd | ballast | discrete | no |
+---------+-----------+------------+---------------------+
Attributes
----------
rail : Rail
Rail instance.
ballast : Ballast
Ballast instance.
pad : Arrangement
Arrangement instance containing multiple pads.
sleeper : Arrangement
Arrangement instance containing multiple sleepers.
distance : Arrangement
Arrangement instance containing multiple distances.
num_mount : int, default=100
Number of mounting positions.
mount_prop : dict
Dictionary for discrete mounting positions (x-> (Pad, Sleeper)).
l_track : float
Track length :math:`[m]`. (May change slightly after discretization.
Results from the number of mounting positions and the mounting distances).
Example
--------
>>> from rolland.database.rail.db_rail import UIC60
>>> from rolland.components import DiscrPad, Sleeper
>>> from rolland.arrangement import PeriodicArrangement
>>> from rolland.track import ArrangedBallastedSingleRailTrack
>>> thepadA = DiscrPad(sp = [300*10**6, 0], dp = [30000, 0])
>>> thepadB = DiscrPad(sp = [400*10**6, 0], dp = [40000, 0])
>>> thesleeperA = Sleeper(ms = 150)
>>> thesleeperB = Sleeper(ms = 200)
>>> pad = PeriodicArrangement(item=[thepadA, thepadB])
>>> distance = PeriodicArrangement(item=[0.65, 0.5])
>>> sleeper = PeriodicArrangement(item=[thesleeperA, thesleeperB])
>>> track = ArrangedBallastedSingleRailTrack(
... rail=UIC60,
... pad=pad,
... sleeper=sleeper,
... distance=distance)
"""
sleeper: Arrangement
pad: Arrangement
ballast: Arrangement
distance: Arrangement
num_mount: int = field(default=100)
def __post_init__(self, *args, **kwargs):
"""post_init method to calculate mounting properties after initialization."""
self.calc_mount_prop()
[docs]
def calc_mount_prop(self, change=None):
"""Calculate the mounting properties."""
x = Decimal(str(0))
for s, p, b, d in zip(self.sleeper.generate(self.num_mount),
self.pad.generate(self.num_mount),
self.ballast.generate(self.num_mount),
self.distance.generate(self.num_mount), strict=False):
self.mount_prop[float(Decimal(str(x)))] = (p, s, b)
x += Decimal(str(d))
self.l_track = max(self.mount_prop.keys())
def _on_critical_change(self, change):
"""Invalidate cached results when critical parameters change."""
if hasattr(self, '_cache'):
self._cache.clear()
def _abstract(self) -> None:
pass