UVEC 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uvec-1.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023-2025, StemVibrations
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
uvec-1.1.0/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.2
2
+ Name: UVEC
3
+ Version: 1.1.0
4
+ Summary: User defined VEhiCle models [UVEC]
5
+ Author: attr: UVEC.__author__
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: BSD License
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Classifier: Operating System :: Unix
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=2.0.2
14
+ Requires-Dist: matplotlib>=3.9.3
15
+ Provides-Extra: testing
16
+ Requires-Dist: pytest>=8.3; extra == "testing"
17
+ Requires-Dist: pytest-cov>=6.0; extra == "testing"
18
+ Requires-Dist: tox>=4.23; extra == "testing"
19
+ Requires-Dist: mypy>=1.13; extra == "testing"
20
+ Requires-Dist: yapf>=0.43; extra == "testing"
21
+
22
+ ![Tests](https://github.com/StemVibrations/vehicle_models/actions/workflows/workflow.yml/badge.svg)
23
+ [![codecov](https://codecov.io/gh/StemVibrations/vehicle_models/graph/badge.svg?token=0SCPA6LAGT)](https://codecov.io/gh/StemVibrations/vehicle_models)
24
+
25
+
26
+ # User defined VEhiCle models [UVEC]
27
+
28
+ This repository contains the User defined VEhiCle models [UVEC] for STEM.
29
+
30
+ At the moment, the following vehicle models are available:
31
+
32
+ * `ten_dof_vehicle_2D`: 2-dimensional 10 degrees-of-freedom vehicle model (based on [Zhai and Sun, 1994](https://www.tandfonline.com/doi/abs/10.1080/00423119308969544))
33
+
34
+ * `two_dof_vehicle_2D`: 2-dimensional 2 degrees-of-freedom vehicle model (based on [Lou and Zeng, 2004](https://onlinelibrary.wiley.com/doi/epdf/10.1002/nme.1207))
35
+
36
+
37
+ Project structure:
38
+
39
+ UVEC
40
+ ├── README.md
41
+ ├── requirements.txt
42
+ ├── setup.cfg
43
+ ├── UVEC
44
+ │ |── __init__.py
45
+ │ ├── uvec_ten_dof_vehicle_2D
46
+ │ │ ├── __init__.py
47
+ │ │ ├── ten_dof_vehicle_2D.py
48
+ │ │ └── base_model.py
49
+ │ ├── uvec_two_dof_vehicle_2D
50
+ │ │ ├── __init__.py
51
+ │ │ ├── ten_dof_vehicle_2D.py
52
+ │ │ └── base_model.py
uvec-1.1.0/README.md ADDED
@@ -0,0 +1,31 @@
1
+ ![Tests](https://github.com/StemVibrations/vehicle_models/actions/workflows/workflow.yml/badge.svg)
2
+ [![codecov](https://codecov.io/gh/StemVibrations/vehicle_models/graph/badge.svg?token=0SCPA6LAGT)](https://codecov.io/gh/StemVibrations/vehicle_models)
3
+
4
+
5
+ # User defined VEhiCle models [UVEC]
6
+
7
+ This repository contains the User defined VEhiCle models [UVEC] for STEM.
8
+
9
+ At the moment, the following vehicle models are available:
10
+
11
+ * `ten_dof_vehicle_2D`: 2-dimensional 10 degrees-of-freedom vehicle model (based on [Zhai and Sun, 1994](https://www.tandfonline.com/doi/abs/10.1080/00423119308969544))
12
+
13
+ * `two_dof_vehicle_2D`: 2-dimensional 2 degrees-of-freedom vehicle model (based on [Lou and Zeng, 2004](https://onlinelibrary.wiley.com/doi/epdf/10.1002/nme.1207))
14
+
15
+
16
+ Project structure:
17
+
18
+ UVEC
19
+ ├── README.md
20
+ ├── requirements.txt
21
+ ├── setup.cfg
22
+ ├── UVEC
23
+ │ |── __init__.py
24
+ │ ├── uvec_ten_dof_vehicle_2D
25
+ │ │ ├── __init__.py
26
+ │ │ ├── ten_dof_vehicle_2D.py
27
+ │ │ └── base_model.py
28
+ │ ├── uvec_two_dof_vehicle_2D
29
+ │ │ ├── __init__.py
30
+ │ │ ├── ten_dof_vehicle_2D.py
31
+ │ │ └── base_model.py
@@ -0,0 +1 @@
1
+ from .__version__ import __version__, __title__
@@ -0,0 +1,3 @@
1
+ __title__ = "UVEC"
2
+ __version__ = "1.1.0"
3
+ __author__ = "STEM team"
@@ -0,0 +1,83 @@
1
+ import os
2
+ import json
3
+ import site
4
+ import sysconfig
5
+
6
+ from .__version__ import __version__, __title__
7
+
8
+
9
+ def editable_path(folder_path: str) -> str:
10
+ """
11
+ Collect the package location when installed in editable mode.
12
+
13
+ Args:
14
+ - folder_path (str): Path to the system package folder
15
+
16
+ Returns:
17
+ - str: Path to the package installed in editable mode
18
+
19
+ """
20
+ with open(os.path.join(folder_path, "direct_url.json"), "r") as f:
21
+ data = json.load(f)
22
+
23
+ path_package = data["url"].split("file://")[1]
24
+
25
+ with open(os.path.join(folder_path, "top_level.txt"), "r") as f:
26
+ packages = f.read().splitlines()
27
+
28
+ return os.path.join(path_package, packages[0])
29
+
30
+
31
+ def is_installed_editable(package_name: str) -> bool:
32
+ """
33
+ Checks if the given package is installed in editable mode.
34
+ Returns True if installed in editable mode, otherwise False.
35
+
36
+ Args:
37
+ - package_name (str): Name of the package to check.
38
+
39
+ Returns:
40
+ - bool: True if installed in editable mode, otherwise False.
41
+ """
42
+
43
+ return os.path.isfile(os.path.join(site.getsitepackages()[0], f"__editable__.{package_name}.pth"))
44
+
45
+
46
+ def get_package_path() -> str:
47
+ """
48
+ Gets the path to the current package in the site-packages directory.
49
+
50
+ Returns:
51
+ - str: Path to the package.
52
+ """
53
+
54
+ package_name = "-".join([__title__, __version__])
55
+ site_packages_path = sysconfig.get_paths()["purelib"]
56
+ dist_info_path = os.path.join(site_packages_path, f"{package_name}.dist-info")
57
+
58
+ is_editable = is_installed_editable(package_name)
59
+
60
+ if not is_editable:
61
+ # if installed in regular mode
62
+ site_packages_path = sysconfig.get_paths()["purelib"]
63
+ package_path = os.path.join(site_packages_path, __name__.split(".")[0])
64
+ return package_path
65
+ else:
66
+ # if installed in editable mode
67
+ return editable_path(dist_info_path)
68
+
69
+
70
+ def get_path_file(uvec_name: str) -> str:
71
+ """
72
+ Sets the global path_file variable and performs the copy operation.
73
+
74
+ Args:
75
+ - uvec_name (str): Name of the uvec file to copy.
76
+
77
+ Returns:
78
+ - str: Global path to the uvec file.
79
+ """
80
+
81
+ package_path = get_package_path()
82
+
83
+ return os.path.join(package_path, uvec_name)
@@ -0,0 +1,3 @@
1
+ from UVEC.get_path_uvec import get_path_file
2
+
3
+ UVEC_NAME = "uvec_ten_dof_vehicle_2D"
@@ -0,0 +1,348 @@
1
+ import numpy as np
2
+
3
+ GRAVITY = 9.81
4
+
5
+
6
+ class Cart():
7
+ """
8
+ Cart class
9
+
10
+ Attributes:
11
+ - bogies (list): list of bogies
12
+ - dofs (list): list of degrees of freedom
13
+ """
14
+
15
+ def __init__(self):
16
+ """
17
+ Initialise the cart
18
+ """
19
+ self.bogies = []
20
+ self.dofs = []
21
+
22
+
23
+ class Bogie():
24
+ """
25
+ Bogie class
26
+
27
+ Attributes:
28
+ - wheels (list): list of wheels
29
+ - dofs (list): list of degrees of freedom
30
+ """
31
+
32
+ def __init__(self):
33
+ """
34
+ Initialise the bogie
35
+ """
36
+ self.wheels = []
37
+ self.dofs = []
38
+
39
+
40
+ class Wheel():
41
+ """
42
+ Wheel class
43
+
44
+ Attributes:
45
+ - dofs (list): list of degrees of freedom
46
+ """
47
+
48
+ def __init__(self):
49
+ self.dofs = []
50
+
51
+
52
+ class TrainModel():
53
+
54
+ def __init__(self):
55
+
56
+ self.n_carts = None
57
+ self.n_bogies = None
58
+ self.n_wheels = None
59
+
60
+ self.cart_inertia = None
61
+ self.cart_mass = None
62
+ self.cart_stiffness = None
63
+ self.cart_damping = None
64
+ self.bogie_distances = None
65
+ self.bogie_inertia = None
66
+ self.bogie_mass = None
67
+ self.wheel_inertia = None
68
+ self.wheel_mass = None
69
+ self.wheel_stiffness = None
70
+ self.wheel_damping = None
71
+ self.wheel_distances = None
72
+
73
+ self.carts = []
74
+
75
+ self.ndof = None
76
+ self.contact_dofs = []
77
+
78
+ self.contact_coeff = None
79
+ self.contact_power = None
80
+
81
+ def initialise(self):
82
+ """
83
+ Initialise the train system
84
+
85
+ :return:
86
+ """
87
+ self.generate_components()
88
+ self.set_dofs()
89
+
90
+ def calculate_n_elements(self):
91
+ """
92
+ Calculate the number of bogies and wheels in the train system
93
+
94
+ :return:
95
+ """
96
+
97
+ self.n_bogies = len(self.bogie_distances)
98
+ self.n_wheels = len(self.wheel_distances)
99
+
100
+ def generate_components(self):
101
+ """
102
+ Generate the train components, i.e. wheels, bogies and carts
103
+
104
+ :return:
105
+ """
106
+
107
+ self.calculate_n_elements()
108
+
109
+ for i in range(self.n_carts):
110
+ cart = Cart()
111
+ for j in range(self.n_bogies):
112
+ bogie = Bogie()
113
+ for k in range(len(self.wheel_distances)):
114
+ wheel = Wheel()
115
+ bogie.wheels.append(wheel)
116
+ cart.bogies.append(bogie)
117
+ self.carts.append(cart)
118
+
119
+ def set_dofs(self):
120
+ """
121
+ Set the degrees of freedom for each component and calculate the total number of degrees of freedom
122
+
123
+ :return:
124
+ """
125
+
126
+ i_dof = 0
127
+
128
+ for cart in self.carts:
129
+ # cart has displacement and rotation dof
130
+ cart.dofs = [i_dof, i_dof + 1]
131
+ i_dof += 2
132
+
133
+ for bogie in cart.bogies:
134
+ # bogie has displacement and rotation dof
135
+ bogie.dofs = [i_dof, i_dof + 1]
136
+ i_dof += 2
137
+
138
+ for wheel in bogie.wheels:
139
+ # wheel has displacement dof
140
+ wheel.dofs = [i_dof]
141
+ self.contact_dofs.append(i_dof)
142
+ i_dof += 1
143
+
144
+ self.ndof = i_dof
145
+
146
+ def generate_mass_matrix(self):
147
+ """
148
+ Generate the mass matrix
149
+ """
150
+
151
+ mass_matrix = np.zeros((self.ndof, self.ndof))
152
+
153
+ for cart in self.carts:
154
+ mass_matrix[cart.dofs[0], cart.dofs[0]] += self.cart_mass
155
+ mass_matrix[cart.dofs[1], cart.dofs[1]] += self.cart_inertia
156
+
157
+ for bogie in cart.bogies:
158
+ mass_matrix[bogie.dofs[0], bogie.dofs[0]] += self.bogie_mass
159
+ mass_matrix[bogie.dofs[1], bogie.dofs[1]] += self.bogie_inertia
160
+
161
+ for wheel in bogie.wheels:
162
+ mass_matrix[wheel.dofs[0], wheel.dofs[0]] += self.wheel_mass
163
+
164
+ return mass_matrix
165
+
166
+ def generate_stiffness_matrix(self):
167
+ """
168
+ Generate stiffness matrix
169
+ """
170
+ stiffness_matrix = np.zeros((self.ndof, self.ndof))
171
+
172
+ for cart in self.carts:
173
+ stiffness_matrix[cart.dofs[0], cart.dofs[0]] += self.cart_stiffness * len(cart.bogies)
174
+
175
+ for b, bogie in enumerate(cart.bogies):
176
+ stiffness_matrix[cart.dofs[1], cart.dofs[1]] += self.cart_stiffness * self.bogie_distances[b]**2
177
+
178
+ stiffness_matrix[cart.dofs[0], bogie.dofs[0]] += -self.cart_stiffness
179
+ stiffness_matrix[bogie.dofs[0], cart.dofs[0]] += -self.cart_stiffness
180
+
181
+ stiffness_matrix[cart.dofs[1], bogie.dofs[0]] += self.cart_stiffness * self.bogie_distances[b]
182
+ stiffness_matrix[bogie.dofs[0], cart.dofs[1]] += self.cart_stiffness * self.bogie_distances[b]
183
+
184
+ stiffness_matrix[bogie.dofs[0], bogie.dofs[0]] += self.cart_stiffness
185
+
186
+ stiffness_matrix[bogie.dofs[0], bogie.dofs[0]] += self.wheel_stiffness * len(bogie.wheels)
187
+
188
+ for w, wheel in enumerate(bogie.wheels):
189
+
190
+ stiffness_matrix[bogie.dofs[1], bogie.dofs[1]] += self.wheel_stiffness * self.wheel_distances[w]**2
191
+ stiffness_matrix[bogie.dofs[0], wheel.dofs[0]] += -self.wheel_stiffness
192
+ stiffness_matrix[wheel.dofs[0], bogie.dofs[0]] += -self.wheel_stiffness
193
+
194
+ stiffness_matrix[bogie.dofs[1], wheel.dofs[0]] += self.wheel_stiffness * self.wheel_distances[w]
195
+ stiffness_matrix[wheel.dofs[0], bogie.dofs[1]] += self.wheel_stiffness * self.wheel_distances[w]
196
+
197
+ stiffness_matrix[wheel.dofs[0], wheel.dofs[0]] += self.wheel_stiffness
198
+
199
+ return stiffness_matrix
200
+
201
+ def generate_damping_matrix(self):
202
+ """
203
+ Generate damping matrix
204
+
205
+ :return:
206
+ """
207
+
208
+ damping_matrix = np.zeros((self.ndof, self.ndof))
209
+
210
+ for cart in self.carts:
211
+ damping_matrix[cart.dofs[0], cart.dofs[0]] += self.cart_damping * len(cart.bogies)
212
+
213
+ for b, bogie in enumerate(cart.bogies):
214
+ damping_matrix[cart.dofs[1], cart.dofs[1]] += self.cart_damping * self.bogie_distances[b]**2
215
+
216
+ damping_matrix[cart.dofs[0], bogie.dofs[0]] += -self.cart_damping
217
+ damping_matrix[bogie.dofs[0], cart.dofs[0]] += -self.cart_damping
218
+
219
+ damping_matrix[cart.dofs[1], bogie.dofs[0]] += self.cart_damping * self.bogie_distances[b]
220
+ damping_matrix[bogie.dofs[0], cart.dofs[1]] += self.cart_damping * self.bogie_distances[b]
221
+
222
+ damping_matrix[bogie.dofs[0], bogie.dofs[0]] += self.cart_damping
223
+
224
+ damping_matrix[bogie.dofs[0], bogie.dofs[0]] += self.wheel_damping * len(bogie.wheels)
225
+ for w, wheel in enumerate(bogie.wheels):
226
+ damping_matrix[bogie.dofs[1], bogie.dofs[1]] += self.wheel_damping * self.wheel_distances[w]**2
227
+ damping_matrix[bogie.dofs[0], wheel.dofs[0]] += -self.wheel_damping
228
+ damping_matrix[wheel.dofs[0], bogie.dofs[0]] += -self.wheel_damping
229
+
230
+ damping_matrix[bogie.dofs[1], wheel.dofs[0]] += self.wheel_damping * self.wheel_distances[w]
231
+ damping_matrix[wheel.dofs[0], bogie.dofs[1]] += self.wheel_damping * self.wheel_distances[w]
232
+
233
+ damping_matrix[wheel.dofs[0], wheel.dofs[0]] += self.wheel_damping
234
+
235
+ return damping_matrix
236
+
237
+ def generate_force_vector(self):
238
+ """
239
+ Generate force vector
240
+ """
241
+ force_vector = np.zeros(self.ndof)
242
+
243
+ for cart in self.carts:
244
+ force_vector[cart.dofs[0]] = -self.cart_mass * GRAVITY
245
+
246
+ for bogie in cart.bogies:
247
+ force_vector[bogie.dofs[0]] = -self.bogie_mass * GRAVITY
248
+
249
+ for wheel in bogie.wheels:
250
+ force_vector[wheel.dofs[0]] = -self.wheel_mass * GRAVITY
251
+
252
+ return force_vector
253
+
254
+ def calculate_static_contact_force(self):
255
+ """
256
+ Calculate static contact force
257
+
258
+ :return:
259
+ """
260
+
261
+ contact_forces = []
262
+
263
+ # Calculate static contact force
264
+ for cart in self.carts:
265
+ distributed_load_cart = self.cart_mass * -GRAVITY / len(cart.bogies)
266
+
267
+ for bogie in cart.bogies:
268
+
269
+ distributed_load_bogie = (self.bogie_mass * -GRAVITY + distributed_load_cart) / len(bogie.wheels)
270
+
271
+ for _ in bogie.wheels:
272
+ contact_force = self.wheel_mass * -GRAVITY + distributed_load_bogie
273
+ contact_forces.append(contact_force)
274
+
275
+ return np.array(contact_forces)
276
+
277
+ def apply_dirichlet_bc(self, A, b, bc_indices):
278
+ """
279
+ Apply Dirichlet boundary conditions to a stiffness matrix and force vector.
280
+
281
+ :param A: The stiffness matrix.
282
+ :param b: The force vector.
283
+ :param bc_indices: The indices at which to apply the boundary conditions.
284
+
285
+ :returns: The modified stiffness matrix and force vector.
286
+ """
287
+
288
+ # Replace the i-th row of A with the unit vector.
289
+ for index in bc_indices:
290
+ # Replace the i-th row of A with the unit vector.
291
+ A[index, :] = 0.0
292
+ A[:, index] = 0.0
293
+ A[index, index] = 1.0
294
+
295
+ b[index] = 0.0
296
+
297
+ return A, b
298
+
299
+ def calculate_initial_displacement(self, K, F, u_wheels):
300
+ """
301
+ Calculate initial displacement
302
+
303
+ :param K: stiffness matrix
304
+ :param F: force vector
305
+ :param u_wheels: initial displacement of the wheels
306
+ :return:
307
+ """
308
+
309
+ # apply dirichlet boundary condition
310
+ u = np.zeros(self.ndof)
311
+ u[self.contact_dofs] = u_wheels
312
+
313
+ F_internal = K.dot(u)
314
+ F_total = F - F_internal
315
+
316
+ K_constr, F_constr, = self.apply_dirichlet_bc(np.copy(K), np.copy(F_total), self.contact_dofs)
317
+
318
+ # Solve the linear system.
319
+ u = np.linalg.solve(K_constr, F_constr)
320
+
321
+ # # add dirichlet boundary condition
322
+ u[self.contact_dofs] = u_wheels
323
+
324
+ return u
325
+
326
+ def generate_global_matrices(self):
327
+ """
328
+ Generate global matrices
329
+
330
+ and apply dirichlet boundary condition on elements without mass or inertia term
331
+ :return:
332
+ """
333
+
334
+ K = self.generate_stiffness_matrix()
335
+ C = self.generate_damping_matrix()
336
+ M = self.generate_mass_matrix()
337
+ F = self.generate_force_vector()
338
+
339
+ # dirichlet constraint on elements without mass or intertia term
340
+ tolerance = 1e-10
341
+ mask = np.abs(np.diag(M)) < tolerance
342
+
343
+ np.fill_diagonal(M, np.where(mask, 1, np.diag(M)))
344
+ np.fill_diagonal(K, np.where(mask, 1, np.diag(K)))
345
+ np.fill_diagonal(C, np.where(mask, 1, np.diag(C)))
346
+ F[mask] = 0
347
+
348
+ return K, C, M, F
@@ -0,0 +1,45 @@
1
+ import numpy as np
2
+
3
+
4
+ class HertzianContact():
5
+ """
6
+ Hertzian contact model
7
+
8
+ Attributes:
9
+ - contact_coeff (float): contact coefficient
10
+ - contact_power (float): contact power
11
+ """
12
+
13
+ def __init__(self):
14
+ """
15
+ Constructor of the HertzianContact class
16
+ """
17
+ self.contact_coeff = None # contact coefficient
18
+ self.contact_power = None # contact power
19
+
20
+ def calculate_contact_force(self, du: np.ndarray) -> np.ndarray:
21
+ """
22
+ Calculate contact force
23
+
24
+ Args:
25
+ - du (np.ndarray): contact deformation
26
+
27
+ Returns:
28
+ - np.ndarray: contact force
29
+ """
30
+
31
+ contact_force = np.sign(-du) * np.nan_to_num((1 / self.contact_coeff * -du)**self.contact_power)
32
+ return contact_force
33
+
34
+ def calculate_contact_deformation(self, F: np.ndarray) -> np.ndarray:
35
+ """
36
+ Calculate contact deformation
37
+
38
+ Args:
39
+ - F (np.ndarray): contact force
40
+
41
+ Returns:
42
+ - np.ndarray: contact deformation
43
+ """
44
+
45
+ return np.sign(F) * self.contact_coeff * np.abs(F)**(1 / self.contact_power)
@@ -0,0 +1,59 @@
1
+ import numpy as np
2
+
3
+
4
+ def calculate_rail_irregularity(x: float,
5
+ f_min: float = 2,
6
+ f_max: float = 500,
7
+ N: int = 2000,
8
+ Av: float = 0.00002095,
9
+ omega_c: float = 0.8242,
10
+ seed: int = 14) -> float:
11
+ """
12
+ Creates rail unevenness following :cite: `zhang_2001`.
13
+
14
+ A summary of default values can be found in : cite: `Podworna_2015`.
15
+
16
+ Args:
17
+ - x (float) : position of the node [m]
18
+ - f_min (float): minimum spatial frequency for the PSD of the unevenness [1/m] (default 2 1/m)
19
+ - f_max: (float) maximum spatial frequency for the PSD of the unevenness [1/m] (default 500 1/m)
20
+ - N (int): number of frequency increments [-] (default 2000)
21
+ - Av (float): vertical track irregularity parameters [m2 rad/m] (default 0.00002095 m2 rad/m)
22
+ - omega_c (float): critical wave number [rad/m] (default 0.8242 rad/m)
23
+ - seed: (int) seed for random generator [-] (default 14)
24
+
25
+ Returns:
26
+ - irregularity (float): irregularity at the node [m]
27
+ """
28
+
29
+ # random generator
30
+ random_generator = np.random.default_rng(seed)
31
+
32
+ # define omega range
33
+ omega_max = 2 * np.pi * f_max
34
+ omega_min = 2 * np.pi * f_min
35
+ delta_omega = (omega_max - omega_min) / N
36
+
37
+ # for each frequency increment
38
+ omega_n = omega_min + delta_omega * np.arange(N)
39
+ phi = random_generator.uniform(0, 2 * np.pi, N)
40
+ irregularity = np.sum(np.sqrt(4 * spectral(omega_n, Av, omega_c) * delta_omega) * np.cos(omega_n * x - phi))
41
+
42
+ return irregularity
43
+
44
+
45
+ def spectral(omega: np.ndarray, Av: float, omega_c: float) -> np.ndarray:
46
+ """
47
+ Computes spectral unevenness
48
+
49
+ Args:
50
+ - omega (np.ndarray): wave number [rad/m]
51
+ - Av (float): vertical track irregularity parameters [m2 rad/m]
52
+ - omega_c (float): critical wave number [rad/m]
53
+
54
+ Returns:
55
+ - spectral_unevenness (np.ndarray): spectral unevenness [m3 / rad]
56
+ """
57
+
58
+ spectral_unevenness = 2 * np.pi * Av * omega_c**2 / ((omega**2 + omega_c**2) * omega**2)
59
+ return spectral_unevenness