PyCBA 0.6.0__tar.gz → 0.7.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.
Files changed (27) hide show
  1. {pycba-0.6.0/src/PyCBA.egg-info → pycba-0.7.0}/PKG-INFO +1 -1
  2. {pycba-0.6.0 → pycba-0.7.0/src/PyCBA.egg-info}/PKG-INFO +1 -1
  3. {pycba-0.6.0 → pycba-0.7.0}/src/PyCBA.egg-info/SOURCES.txt +2 -1
  4. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/__init__.py +1 -1
  5. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/analysis.py +46 -6
  6. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/bridge.py +31 -5
  7. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/inf_lines.py +11 -7
  8. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/load.py +281 -34
  9. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/results.py +153 -39
  10. {pycba-0.6.0 → pycba-0.7.0}/tests/test_basic.py +305 -6
  11. pycba-0.7.0/tests/test_bridge.py +274 -0
  12. {pycba-0.6.0 → pycba-0.7.0}/tests/test_inf_lines.py +27 -0
  13. pycba-0.7.0/tests/test_results.py +83 -0
  14. pycba-0.6.0/tests/test_bridge.py +0 -102
  15. {pycba-0.6.0 → pycba-0.7.0}/LICENSE +0 -0
  16. {pycba-0.6.0 → pycba-0.7.0}/README.md +0 -0
  17. {pycba-0.6.0 → pycba-0.7.0}/pyproject.toml +0 -0
  18. {pycba-0.6.0 → pycba-0.7.0}/setup.cfg +0 -0
  19. {pycba-0.6.0 → pycba-0.7.0}/setup.py +0 -0
  20. {pycba-0.6.0 → pycba-0.7.0}/src/PyCBA.egg-info/dependency_links.txt +0 -0
  21. {pycba-0.6.0 → pycba-0.7.0}/src/PyCBA.egg-info/requires.txt +0 -0
  22. {pycba-0.6.0 → pycba-0.7.0}/src/PyCBA.egg-info/top_level.txt +0 -0
  23. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/beam.py +0 -0
  24. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/pattern.py +0 -0
  25. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/types.py +0 -0
  26. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/utils.py +0 -0
  27. {pycba-0.6.0 → pycba-0.7.0}/src/pycba/vehicle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyCBA
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Python Continuous Beam Analysis
5
5
  Author-email: Colin Caprani <colin.caprani@monash.edu>
6
6
  License: Apache 2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyCBA
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Python Continuous Beam Analysis
5
5
  Author-email: Colin Caprani <colin.caprani@monash.edu>
6
6
  License: Apache 2.0
@@ -20,4 +20,5 @@ src/pycba/utils.py
20
20
  src/pycba/vehicle.py
21
21
  tests/test_basic.py
22
22
  tests/test_bridge.py
23
- tests/test_inf_lines.py
23
+ tests/test_inf_lines.py
24
+ tests/test_results.py
@@ -2,7 +2,7 @@
2
2
  PyCBA - Continuous Beam Analysis in Python
3
3
  """
4
4
 
5
- __version__ = "0.6.0"
5
+ __version__ = "0.7.0"
6
6
 
7
7
  from .analysis import BeamAnalysis
8
8
  from .beam import Beam
@@ -91,13 +91,15 @@ class BeamAnalysis:
91
91
  * ``+k`` — elastic spring stiffness in consistent units.
92
92
 
93
93
  LM : list of list, optional
94
- Load matrix: a list of load descriptors, each of the form
95
- ``[span, load_type, value, a, c]``. Load types:
94
+ Load matrix: a list of load descriptors. The number of columns
95
+ per entry depends on the load type:
96
96
 
97
- 1. UDL — ``value`` is load intensity; set ``a = c = 0``.
98
- 2. Point load — ``value`` at distance ``a`` from the span start.
99
- 3. Partial UDL — ``value`` intensity from ``a`` for length ``c``.
100
- 4. Moment load — ``value`` at distance ``a`` from the span start.
97
+ 1. UDL — ``[span, 1, w]``.
98
+ 2. Point load — ``[span, 2, P, a]``.
99
+ 3. Partial UDL — ``[span, 3, w, a, c]``.
100
+ 4. Moment load — ``[span, 4, M, a]``.
101
+ 5. Trapezoidal — ``[span, 5, w1, w2]`` (full span) or
102
+ ``[span, 5, w1, w2, a, c]`` (partial).
101
103
 
102
104
  eletype : array_like of int, optional
103
105
  Element type for each span, controlling which end(s) carry moment:
@@ -241,6 +243,44 @@ class BeamAnalysis:
241
243
  load = [i_span, 4, m, a]
242
244
  self._beam.add_load(load)
243
245
 
246
+ def add_trap(
247
+ self,
248
+ i_span: int,
249
+ w1: float,
250
+ w2: float,
251
+ a: Optional[float] = None,
252
+ c: Optional[float] = None,
253
+ ):
254
+ """
255
+ Append a trapezoidal (linearly varying) distributed load.
256
+
257
+ When *a* and *c* are omitted the load covers the full span, varying
258
+ from *w1* at the left end to *w2* at the right end. When *a* and *c*
259
+ are given the load covers the region from *a* to *a + c*, varying from
260
+ *w1* to *w2* over that length.
261
+
262
+ Parameters
263
+ ----------
264
+ i_span : int
265
+ 1-based span index.
266
+ w1 : float
267
+ Load intensity at the start of the load. Positive values act downward.
268
+ w2 : float
269
+ Load intensity at the end of the load. Positive values act downward.
270
+ a : float, optional
271
+ Distance from the left end of the span to the start of the load.
272
+ If given, *c* must also be provided.
273
+ c : float, optional
274
+ Length (cover) of the load. Required when *a* is provided.
275
+ """
276
+ if a is not None and c is None:
277
+ raise ValueError("If 'a' is specified, 'c' must also be provided")
278
+ if a is not None:
279
+ load = [i_span, 5, w1, w2, a, c]
280
+ else:
281
+ load = [i_span, 5, w1, w2]
282
+ self._beam.add_load(load)
283
+
244
284
  def analyze(self, npts: Optional[int] = None) -> int:
245
285
  """
246
286
  Execute the direct-stiffness analysis.
@@ -209,7 +209,12 @@ class BridgeAnalysis:
209
209
  return self.ba.analyze()
210
210
 
211
211
  def run_vehicle(
212
- self, step: float, plot_env: bool = False, plot_all: bool = False
212
+ self,
213
+ step: float,
214
+ plot_env: bool = False,
215
+ plot_all: bool = False,
216
+ pos_start: Optional[float] = None,
217
+ pos_end: Optional[float] = None,
213
218
  ) -> Envelopes:
214
219
  """
215
220
  Runs the vehicle over the bridge performing a static analysis at each point
@@ -223,6 +228,12 @@ class BridgeAnalysis:
223
228
  plot_all : bool, optional
224
229
  Whether or not to plot the results for each position as an animation.
225
230
  The default is False.
231
+ pos_start : Optional[float], optional
232
+ The starting position of the front axle. Defaults to 0 (front axle at
233
+ the left end of the beam).
234
+ pos_end : Optional[float], optional
235
+ The ending position of the front axle. Defaults to beam length plus
236
+ vehicle length (front axle past the right end of the beam).
226
237
 
227
238
  Raises
228
239
  ------
@@ -239,14 +250,20 @@ class BridgeAnalysis:
239
250
  self._check_objects()
240
251
  self.pos = []
241
252
  self.vResults = []
242
- npts = round((self.ba.beam.length + self.veh.L) / step) + 1
253
+
254
+ if pos_start is None:
255
+ pos_start = 0.0
256
+ if pos_end is None:
257
+ pos_end = self.ba.beam.length + self.veh.L
258
+
259
+ npts = round((pos_end - pos_start) / step) + 1
243
260
 
244
261
  if plot_all:
245
262
  fig, axs = plt.subplots(2, 1, sharex=True)
246
263
 
247
264
  for i in range(npts):
248
265
  # load position
249
- pos = i * step
266
+ pos = pos_start + i * step
250
267
  self.pos.append(pos)
251
268
  out = self._single_analysis(pos)
252
269
  if out != 0:
@@ -269,7 +286,11 @@ class BridgeAnalysis:
269
286
  ) -> Dict[str, Dict[str, Union[float, np.ndarray]]]:
270
287
  """
271
288
  From the envelopes output, returns the extreme values, their locations,
272
- and the position of the vehicle for each in a dictionary of dictionaries
289
+ and the position of the vehicle for each in a dictionary of dictionaries.
290
+
291
+ Each moment entry (``Mmax``, ``Mmin``) also contains a ``"Vco"`` key with
292
+ the coincident shear at the critical location. Each shear entry (``Vmax``,
293
+ ``Vmin``) contains a ``"Mco"`` key with the coincident moment.
273
294
 
274
295
  Parameters
275
296
  ----------
@@ -287,7 +308,8 @@ class BridgeAnalysis:
287
308
  -------
288
309
  crit_values : Dict[str, Dict[str, Union[float, np.ndarray]]]
289
310
  A dictionary of dictionaries containing the critical values (i.e. extremes)
290
- of each of the load effects, both maximum and minimum.
311
+ of each of the load effects, both maximum and minimum, along with
312
+ coincident values of the other effect.
291
313
  """
292
314
 
293
315
  crit_values = {}
@@ -329,21 +351,25 @@ class BridgeAnalysis:
329
351
  "val": Mmax,
330
352
  "at": env.x[env.Mmax.argmax()],
331
353
  "pos": [self.pos[i] for i in indx["Mmax"]],
354
+ "Vco": env.Vco_Mmax[env.Mmax.argmax()],
332
355
  }
333
356
  crit_values["Mmin"] = {
334
357
  "val": Mmin,
335
358
  "at": env.x[env.Mmin.argmin()],
336
359
  "pos": [self.pos[i] for i in indx["Mmin"]],
360
+ "Vco": env.Vco_Mmin[env.Mmin.argmin()],
337
361
  }
338
362
  crit_values["Vmax"] = {
339
363
  "val": Vmax,
340
364
  "at": env.x[env.Vmax.argmax()],
341
365
  "pos": [self.pos[i] for i in indx["Vmax"]],
366
+ "Mco": env.Mco_Vmax[env.Vmax.argmax()],
342
367
  }
343
368
  crit_values["Vmin"] = {
344
369
  "val": Vmin,
345
370
  "at": env.x[env.Vmin.argmin()],
346
371
  "pos": [self.pos[i] for i in indx["Vmin"]],
372
+ "Mco": env.Mco_Vmin[env.Vmin.argmin()],
347
373
  }
348
374
  crit_values["nsup"] = env.nsup
349
375
  for i in range(env.nsup):
@@ -132,13 +132,19 @@ class InfluenceLines:
132
132
  self.ba.beam.no_fixed_restraints
133
133
  )
134
134
 
135
- # idx = np.abs(x - poi).argmin()
135
+ # Find the span containing the poi and the closest non-padding index
136
+ # within that span. Padding indices (first/last per span) have M/V
137
+ # forced to zero, so we must avoid them.
138
+ ispan, _ = self.ba.beam.get_local_span_coords(poi)
139
+ if ispan == -1:
140
+ ispan = self.ba.beam.no_spans - 1
141
+ span_x = self.vResults[0].vRes[ispan].x
142
+ # Skip padding at indices 0 and -1; real data is at 1:-1
143
+ idx_in_span = np.abs(span_x[1:-1] - poi).argmin() + 1
136
144
 
137
145
  if load_effect.upper() == "V":
138
- dx = x[2] - x[1]
139
- idx = np.where(np.abs(x - poi) <= dx * 1e-6)[0][0]
140
146
  for i, res in enumerate(self.vResults):
141
- eta[i] = res.results.V[idx]
147
+ eta[i] = res.vRes[ispan].V[idx_in_span]
142
148
 
143
149
  elif load_effect.upper() == "R":
144
150
  #
@@ -182,10 +188,8 @@ class InfluenceLines:
182
188
  eta[i] = res.R[mt_sup_idx]
183
189
 
184
190
  else:
185
- dx = x[2] - x[1]
186
- idx = np.where(np.abs(x - poi) <= dx * 1e-6)[0][0]
187
191
  for i, res in enumerate(self.vResults):
188
- eta[i] = res.results.M[idx]
192
+ eta[i] = res.vRes[ispan].M[idx_in_span]
189
193
 
190
194
  return (np.array(self.pos), eta)
191
195
 
@@ -1,37 +1,34 @@
1
- """
2
- PyCBA - Load module
3
-
4
- The load matrix represents the loads as a `List` of `Lists`.
5
- Each list entry represents a single load and must be in the following format:
6
-
7
- Span No. | Load Type | Load Value | Distance a | Load Cover c
8
-
9
- Load Types are:
10
-
11
- 1 - **Uniformly Distributed Loads**, which only have a load value; distances `a` and `c` are set to "0".
12
-
13
- 2 - **Point Loads**, located at `a` from the left end of the span; distances `c` is set to "0".
14
-
15
- 3 - **Partial UDLs**, starting at `a` for a distance of `c` (i.e. the cover) where $L >= a+c$.
16
-
17
- 4 - **Moment Load**, located at `a`; distances `c` is set to "0".
18
-
19
- It has dimension `M` x 5, where `M` is the number of loads applied to the beam.
20
-
21
- The type alias `LoadMatrix` is defined as
22
-
23
- .. autodata:: LoadMatrix
24
-
25
- """
26
-
27
- from __future__ import annotations
28
- from typing import Union, List, NamedTuple, Tuple, Optional
29
- import numpy as np
30
-
31
- from .types import LoadType, LoadMatrix, LoadCNL, MemberResults
32
-
33
-
34
- class Load:
1
+ """
2
+ PyCBA - Load module
3
+
4
+ The load matrix is a ``List[List]`` of load descriptors. Each entry
5
+ describes one load; the number of columns varies by load type:
6
+
7
+ ===== ==================== ================================ ====
8
+ Type Name Format Cols
9
+ ===== ==================== ================================ ====
10
+ 1 UDL ``[span, 1, w]`` 3
11
+ 2 Point Load ``[span, 2, P, a]`` 4
12
+ 3 Partial UDL ``[span, 3, w, a, c]`` 5
13
+ 4 Moment Load ``[span, 4, M, a]`` 4
14
+ 5 Trapezoidal (full) ``[span, 5, w1, w2]`` 4
15
+ 5 Trapezoidal (partial) ``[span, 5, w1, w2, a, c]`` 6
16
+ ===== ==================== ================================ ====
17
+
18
+ The type alias `LoadMatrix` is defined as
19
+
20
+ .. autodata:: LoadMatrix
21
+
22
+ """
23
+
24
+ from __future__ import annotations
25
+ from typing import Union, List, NamedTuple, Tuple, Optional
26
+ import numpy as np
27
+
28
+ from .types import LoadType, LoadMatrix, LoadCNL, MemberResults
29
+
30
+
31
+ class Load:
35
32
  """
36
33
  Beam load container and processor
37
34
  """
@@ -430,6 +427,244 @@ class LoadPUDL(Load):
430
427
  return res
431
428
 
432
429
 
430
+ class LoadTrapez(Load):
431
+ """
432
+ Trapezoidal (linearly varying) distributed load, optionally partial.
433
+
434
+ The load varies linearly from intensity *w1* at position *a* to *w2* at
435
+ position *a + c*. When *a* = 0 and *c* = span length the load covers
436
+ the full span (the default).
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ i_span: int,
442
+ w1: float,
443
+ w2: float,
444
+ a: float = 0.0,
445
+ c: Optional[float] = None,
446
+ ):
447
+ """
448
+ Creates a trapezoidal load for the member.
449
+
450
+ Parameters
451
+ ----------
452
+ i_span : int
453
+ The member index to which the load is applied.
454
+ w1 : float
455
+ The load intensity at position *a* (left edge of the load).
456
+ w2 : float
457
+ The load intensity at position *a + c* (right edge of the load).
458
+ a : float, optional
459
+ Distance from the left end of the span to the start of the load.
460
+ Default is 0 (load starts at the left end).
461
+ c : float or None, optional
462
+ Length (cover) of the load. ``None`` (default) means full span
463
+ from *a* to the right end.
464
+ """
465
+ super().__init__(i_span)
466
+ self.w1 = w1
467
+ self.w2 = w2
468
+ self.a = a
469
+ self._c = c # None ⇒ full span from a, resolved when L is known
470
+
471
+ def _resolve(self, L: float):
472
+ """Resolve c and clip to span boundaries.
473
+
474
+ Returns (w1, w2, dw, a, c) with c > 0, or c = 0 when the load
475
+ falls outside the span.
476
+ """
477
+ a = self.a
478
+ c = self._c if self._c is not None else L - a
479
+ w1 = self.w1
480
+ w2 = self.w2
481
+
482
+ if a >= L or c <= 0:
483
+ return w1, w2, 0.0, a, 0.0
484
+
485
+ # Clip overhang
486
+ if a + c > L:
487
+ c_orig = c
488
+ c = L - a
489
+ w2 = w1 + (w2 - w1) * c / c_orig # interpolated at clipped end
490
+
491
+ return w1, w2, w2 - w1, a, c
492
+
493
+ def get_cnl(self, L: float, eType: int) -> LoadCNL:
494
+ """
495
+ Consistent Nodal Loads for the trapezoidal load on a fixed-fixed span.
496
+
497
+ Derived from the fixed-end force influence integrals:
498
+
499
+ .. math::
500
+ M_A = \\frac{1}{L^2}\\int_a^b w(x)\\,x\\,(L-x)^2\\,dx
501
+
502
+ Parameters
503
+ ----------
504
+ L : float
505
+ The length of the member
506
+ eType : int
507
+ The member element type
508
+
509
+ Returns
510
+ -------
511
+ LoadCNL
512
+ Consistent Nodal Loads for this load type
513
+ """
514
+ w1, w2, dw, a, c = self._resolve(L)
515
+
516
+ if c <= 0:
517
+ return LoadCNL(Va=0.0, Vb=0.0, Ma=0.0, Mb=0.0)
518
+
519
+ alpha = L - a # distance: load start → right beam end
520
+ # delta = L - a - c # distance: load end → right beam end (not used directly)
521
+
522
+ # --- Ma: ∫ w(x) · x · (L−x)² dx / L² ---
523
+ # Split into UDL(w1) integral I1 and triangular(dw) integral I2.
524
+ I1 = (
525
+ c**4 / 4
526
+ + (a - 2 * alpha) * c**3 / 3
527
+ + (alpha**2 - 2 * a * alpha) * c**2 / 2
528
+ + a * alpha**2 * c
529
+ )
530
+ I2 = (
531
+ c**5 / 5
532
+ + (a - 2 * alpha) * c**4 / 4
533
+ + (alpha**2 - 2 * a * alpha) * c**3 / 3
534
+ + a * alpha**2 * c**2 / 2
535
+ )
536
+ Ma = (w1 / L**2) * I1 + (dw / (c * L**2)) * I2
537
+
538
+ # --- Mb: −∫ w(x) · x² · (L−x) dx / L² ---
539
+ J1 = (
540
+ -(c**4) / 4
541
+ + (alpha - 2 * a) * c**3 / 3
542
+ + (2 * a * alpha - a**2) * c**2 / 2
543
+ + a**2 * alpha * c
544
+ )
545
+ J2 = (
546
+ -(c**5) / 5
547
+ + (alpha - 2 * a) * c**4 / 4
548
+ + (2 * a * alpha - a**2) * c**3 / 3
549
+ + a**2 * alpha * c**2 / 2
550
+ )
551
+ Mb = -((w1 / L**2) * J1 + (dw / (c * L**2)) * J2)
552
+
553
+ # --- Va: ∫ w(x) · (L−x)² · (2x+L) dx / L³ ---
554
+ K1 = (
555
+ c**4 / 2
556
+ + (a - alpha) * c**3
557
+ - 3 * a * alpha * c**2
558
+ + (3 * a + alpha) * alpha**2 * c
559
+ )
560
+ K2 = (
561
+ 2 * c**5 / 5
562
+ + 3 * (a - alpha) * c**4 / 4
563
+ - 2 * a * alpha * c**3
564
+ + (3 * a + alpha) * alpha**2 * c**2 / 2
565
+ )
566
+ Va = (w1 / L**3) * K1 + (dw / (c * L**3)) * K2
567
+
568
+ Vb = (w1 + w2) * c / 2 - Va
569
+
570
+ return LoadCNL(Va=Va, Vb=Vb, Ma=Ma, Mb=Mb)
571
+
572
+ def get_mbr_results(self, x: np.ndarray, L: float) -> MemberResults:
573
+ """
574
+ Simply-supported member results using Macaulay bracket integration.
575
+
576
+ The load ``w(x) = w1 + (w2−w1)·(x−a)/c`` for ``a ≤ x ≤ a+c`` is
577
+ integrated using Macaulay brackets at positions *a* and *b = a+c*.
578
+
579
+ Parameters
580
+ ----------
581
+ x : np.ndarray
582
+ Vector of points along the length of the member
583
+ L : float
584
+ The length of the member
585
+
586
+ Returns
587
+ -------
588
+ res : MemberResults
589
+ A populated :class:`pycba.load.MemberResults` object
590
+ """
591
+ npts = len(x)
592
+ res = MemberResults(vals=None, n=npts)
593
+ res.x = x
594
+
595
+ w1, w2, dw, a, c = self._resolve(L)
596
+
597
+ if c <= 0:
598
+ res.V = np.zeros(npts)
599
+ res.M = np.zeros(npts)
600
+ res.R = np.zeros(npts)
601
+ res.D = np.zeros(npts)
602
+ return res
603
+
604
+ b = a + c
605
+ alpha = L - a
606
+ delta = L - b
607
+
608
+ # Simply-supported reaction at left end (moment equilibrium about B)
609
+ Va = (
610
+ (w1 / 2) * alpha**2
611
+ + (dw / (6 * c)) * alpha**3
612
+ - (w2 / 2) * delta**2
613
+ - (dw / (6 * c)) * delta**3
614
+ ) / L
615
+
616
+ # Rotation integration constant (from D(0) = D(L) = 0)
617
+ Ra = (
618
+ -(Va / 6) * L**3
619
+ + (w1 / 24) * alpha**4
620
+ + (dw / (120 * c)) * alpha**5
621
+ - (w2 / 24) * delta**4
622
+ - (dw / (120 * c)) * delta**5
623
+ ) / L
624
+
625
+ # Macaulay brackets at load start and end
626
+ MBa = self.MB(x - a)
627
+ MBb = self.MB(x - b)
628
+
629
+ res.V = (
630
+ Va
631
+ - w1 * MBa
632
+ - (dw / (2 * c)) * MBa**2
633
+ + w2 * MBb
634
+ + (dw / (2 * c)) * MBb**2
635
+ )
636
+ res.M = (
637
+ Va * x
638
+ - (w1 / 2) * MBa**2
639
+ - (dw / (6 * c)) * MBa**3
640
+ + (w2 / 2) * MBb**2
641
+ + (dw / (6 * c)) * MBb**3
642
+ )
643
+ res.R = (
644
+ (Va / 2) * x**2
645
+ - (w1 / 6) * MBa**3
646
+ - (dw / (24 * c)) * MBa**4
647
+ + (w2 / 6) * MBb**3
648
+ + (dw / (24 * c)) * MBb**4
649
+ + Ra
650
+ )
651
+ res.D = (
652
+ (Va / 6) * x**3
653
+ - (w1 / 24) * MBa**4
654
+ - (dw / (120 * c)) * MBa**5
655
+ + (w2 / 24) * MBb**4
656
+ + (dw / (120 * c)) * MBb**5
657
+ + Ra * x
658
+ )
659
+
660
+ res.V[0] = 0.0
661
+ res.V[npts - 1] = 0.0
662
+ res.M[0] = 0.0
663
+ res.M[npts - 1] = 0.0
664
+
665
+ return res
666
+
667
+
433
668
  class LoadMaMb(Load):
434
669
  """
435
670
  Member end moment loads
@@ -654,6 +889,13 @@ def parse_LM(LM: LoadMatrix) -> List[Load]:
654
889
  m = load[2]
655
890
  a = load[3]
656
891
  loads.append(LoadML(span, m, a))
892
+ # Trapezoidal Load
893
+ elif ltype == 5:
894
+ w1 = load[2]
895
+ w2 = load[3]
896
+ a = load[4] if len(load) > 4 else 0.0
897
+ c = load[5] if len(load) > 5 else None
898
+ loads.append(LoadTrapez(span, w1, w2, a, c))
657
899
  return loads
658
900
 
659
901
 
@@ -710,6 +952,11 @@ def factor_LM(LM: LoadMatrix, gamma: float) -> LoadMatrix:
710
952
  LMnew.append([i_span, l_type, mag])
711
953
  elif l_type == 2 or l_type == 4: # PL or ML
712
954
  LMnew.append([i_span, l_type, mag, load[3]])
955
+ elif l_type == 5: # Trapezoidal
956
+ new_load = [i_span, l_type, mag, gamma * load[3]]
957
+ if len(load) > 4:
958
+ new_load.extend(load[4:]) # a, c are not factored
959
+ LMnew.append(new_load)
713
960
  else: # PUDL
714
961
  LMnew.append([i_span, l_type, mag, load[3], load[4]])
715
962