mcft 0.1.0__py3-none-any.whl
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.
- mcft/__init__.py +0 -0
- mcft/io.py +583 -0
- mcft/longitudinal_stiffness.py +287 -0
- mcft/materials.py +490 -0
- mcft/mcft.py +437 -0
- mcft/r2k_validation.py +222 -0
- mcft/section.py +542 -0
- mcft/solver.py +636 -0
- mcft-0.1.0.dist-info/METADATA +33 -0
- mcft-0.1.0.dist-info/RECORD +12 -0
- mcft-0.1.0.dist-info/WHEEL +4 -0
- mcft-0.1.0.dist-info/licenses/LICENSE +674 -0
mcft/__init__.py
ADDED
|
File without changes
|
mcft/io.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""XML (.rsp) parser for Response-2000 files.
|
|
2
|
+
|
|
3
|
+
Reads the suite_3g XML format produced by Response-2000 v1.9.6+.
|
|
4
|
+
Values are stored exactly as read — no unit conversion is performed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import shlex
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Data model
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Concrete:
|
|
25
|
+
name: str
|
|
26
|
+
fcp: float
|
|
27
|
+
maxagg: float = 20.0
|
|
28
|
+
c_mod: int = 2
|
|
29
|
+
c_soft: int = 1 # 0=None, 1=Collins-Bentz (default), 2=VC1982
|
|
30
|
+
tsfactor: float = 1.0
|
|
31
|
+
ft: float | None = None
|
|
32
|
+
e0: float | None = None
|
|
33
|
+
Ec: float | None = None
|
|
34
|
+
curve: list[tuple[float, float]] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Rebar:
|
|
39
|
+
name: str
|
|
40
|
+
fy: float
|
|
41
|
+
E: float = 200_000.0
|
|
42
|
+
fu: float | None = None
|
|
43
|
+
esh: float = 10.0
|
|
44
|
+
eu: float = 100.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Prestress:
|
|
49
|
+
name: str
|
|
50
|
+
fu: float
|
|
51
|
+
E: float = 190_000.0
|
|
52
|
+
A: float = 0.030
|
|
53
|
+
B: float = 121.0
|
|
54
|
+
C: float = 6.0
|
|
55
|
+
eu: float = 40.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class SectionPoint:
|
|
60
|
+
depth: float
|
|
61
|
+
width: float
|
|
62
|
+
concrete_name: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class LongReinf:
|
|
67
|
+
z: float
|
|
68
|
+
type: str
|
|
69
|
+
A: float
|
|
70
|
+
name: str = ""
|
|
71
|
+
num: int = 1
|
|
72
|
+
Ai: float | None = None
|
|
73
|
+
db: float | None = None
|
|
74
|
+
space: float = 0.0
|
|
75
|
+
dep: float = 0.0
|
|
76
|
+
drape: float = 0.0
|
|
77
|
+
bartitle: str = ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class TransReinf:
|
|
82
|
+
A: float
|
|
83
|
+
type: str
|
|
84
|
+
pattern: int
|
|
85
|
+
space: float
|
|
86
|
+
disttop: float = 0.0
|
|
87
|
+
distbot: float = 0.0
|
|
88
|
+
name: str = ""
|
|
89
|
+
Ai: float | None = None
|
|
90
|
+
db: float | None = None
|
|
91
|
+
dep: float = 0.0
|
|
92
|
+
bartitle: str = ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ElemInfo:
|
|
97
|
+
L: float
|
|
98
|
+
mido2: float = 0.0
|
|
99
|
+
lplate: float = 0.0
|
|
100
|
+
rplate: float = 0.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class SectLoading:
|
|
105
|
+
MM: float = 0.0
|
|
106
|
+
MV: float = 0.0
|
|
107
|
+
BN: float = 0.0
|
|
108
|
+
BM: float = 0.0
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def av(self) -> float:
|
|
112
|
+
"""Shear span M/V in analysis length units (mm).
|
|
113
|
+
|
|
114
|
+
Derived from the load-path ratios MM and MV.
|
|
115
|
+
In .rsp files MM is in kN·m and MV in kN, so MM/MV
|
|
116
|
+
is in m; the factor of 1000 converts to mm.
|
|
117
|
+
Returns inf for pure bending (MV == 0).
|
|
118
|
+
"""
|
|
119
|
+
if abs(self.MV) < 1e-15:
|
|
120
|
+
return float("inf")
|
|
121
|
+
return self.MM / self.MV * 1000.0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ShrinkTherm:
|
|
126
|
+
profile: list[tuple[float, float]] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class BeamSection:
|
|
131
|
+
name: str
|
|
132
|
+
doneby: str = ""
|
|
133
|
+
date: str = ""
|
|
134
|
+
concretes: dict[str, Concrete] = field(default_factory=dict)
|
|
135
|
+
rebars: dict[str, Rebar] = field(default_factory=dict)
|
|
136
|
+
prestress: dict[str, Prestress] = field(default_factory=dict)
|
|
137
|
+
section_points: list[SectionPoint] = field(default_factory=list)
|
|
138
|
+
long_reinf: list[LongReinf] = field(default_factory=list)
|
|
139
|
+
trans_reinf: list[TransReinf] = field(default_factory=list)
|
|
140
|
+
elem_info: ElemInfo | None = None
|
|
141
|
+
sect_loading: SectLoading | None = None
|
|
142
|
+
shrink_therm: ShrinkTherm | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class BMDPoint:
|
|
147
|
+
position: float
|
|
148
|
+
moment: float
|
|
149
|
+
flag: int
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class BMD:
|
|
154
|
+
points: list[BMDPoint] = field(default_factory=list)
|
|
155
|
+
isconstant: bool = False
|
|
156
|
+
type: str = ""
|
|
157
|
+
axial_load: float = 0.0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class MatZone:
|
|
162
|
+
position: float
|
|
163
|
+
section_name: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class Support:
|
|
168
|
+
which: str
|
|
169
|
+
platelocate: float
|
|
170
|
+
platesize: float = 0.0
|
|
171
|
+
support: str = ""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class MemberAnalysis:
|
|
176
|
+
length: float
|
|
177
|
+
bmds: list[BMD] = field(default_factory=list)
|
|
178
|
+
mat_zones: list[MatZone] = field(default_factory=list)
|
|
179
|
+
supports: list[Support] = field(default_factory=list)
|
|
180
|
+
topplatesize: float = 0.0
|
|
181
|
+
displocate: float = 0.0
|
|
182
|
+
leftanchor: str = ""
|
|
183
|
+
rightanchor: str = ""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class R2KModel:
|
|
188
|
+
sections: list[BeamSection] = field(default_factory=list)
|
|
189
|
+
member_analysis: MemberAnalysis | None = None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# XML preprocessing
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
_DOCTYPE_RE = re.compile(r"<!DOCTYPE[^>]*>")
|
|
197
|
+
_TAG_RE = re.compile(r"<[^!?/][^>]*>", re.DOTALL)
|
|
198
|
+
_ATTR_RE = re.compile(r"""(\w+)\s*=\s*"([^"]*)"\s*""")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _strip_doctype(xml_text: str) -> str:
|
|
202
|
+
return _DOCTYPE_RE.sub("", xml_text)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _dedup_attrs(xml_text: str) -> str:
|
|
206
|
+
"""Remove duplicate attributes within each XML tag, keeping last occurrence."""
|
|
207
|
+
|
|
208
|
+
def _dedup_tag(match: re.Match) -> str:
|
|
209
|
+
tag = match.group(0)
|
|
210
|
+
attrs = _ATTR_RE.findall(tag)
|
|
211
|
+
if not attrs:
|
|
212
|
+
return tag
|
|
213
|
+
seen: dict[str, str] = {}
|
|
214
|
+
for name, value in attrs:
|
|
215
|
+
if name in seen:
|
|
216
|
+
logger.debug("Duplicate attribute %r in tag, keeping last value", name)
|
|
217
|
+
seen[name] = value
|
|
218
|
+
if len(seen) == len(attrs):
|
|
219
|
+
return tag
|
|
220
|
+
# Rebuild: take tag name portion, then unique attrs
|
|
221
|
+
# Find where the tag name ends
|
|
222
|
+
stripped = tag.lstrip("<")
|
|
223
|
+
tag_name = stripped.split()[0].rstrip(">").rstrip("/")
|
|
224
|
+
is_self_closing = tag.rstrip().endswith("/>")
|
|
225
|
+
rebuilt = f"<{tag_name}"
|
|
226
|
+
for aname, aval in seen.items():
|
|
227
|
+
rebuilt += f' {aname}="{aval}"'
|
|
228
|
+
rebuilt += " />" if is_self_closing else ">"
|
|
229
|
+
return rebuilt
|
|
230
|
+
|
|
231
|
+
return _TAG_RE.sub(_dedup_tag, xml_text)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _preprocess(xml_text: str) -> str:
|
|
235
|
+
return _dedup_attrs(_strip_doctype(xml_text))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Text content parsers
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_text_lines(text: str | None) -> list[list[str]]:
|
|
244
|
+
"""Split element text into tokenised lines using shlex for quoted strings."""
|
|
245
|
+
if not text:
|
|
246
|
+
return []
|
|
247
|
+
lines: list[list[str]] = []
|
|
248
|
+
for raw in text.strip().splitlines():
|
|
249
|
+
raw = raw.strip()
|
|
250
|
+
if not raw:
|
|
251
|
+
continue
|
|
252
|
+
try:
|
|
253
|
+
tokens = shlex.split(raw)
|
|
254
|
+
except ValueError:
|
|
255
|
+
tokens = raw.split()
|
|
256
|
+
lines.append(tokens)
|
|
257
|
+
return lines
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_section_points(text: str | None) -> list[SectionPoint]:
|
|
261
|
+
points: list[SectionPoint] = []
|
|
262
|
+
for tokens in _parse_text_lines(text):
|
|
263
|
+
if len(tokens) < 2:
|
|
264
|
+
continue
|
|
265
|
+
depth = float(tokens[0])
|
|
266
|
+
width = float(tokens[1])
|
|
267
|
+
concrete_name = tokens[2] if len(tokens) >= 3 else None
|
|
268
|
+
points.append(SectionPoint(depth=depth, width=width, concrete_name=concrete_name))
|
|
269
|
+
return points
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _parse_curve(text: str | None) -> list[tuple[float, float]] | None:
|
|
273
|
+
lines = _parse_text_lines(text)
|
|
274
|
+
if not lines:
|
|
275
|
+
return None
|
|
276
|
+
curve: list[tuple[float, float]] = []
|
|
277
|
+
for tokens in lines:
|
|
278
|
+
if len(tokens) >= 2:
|
|
279
|
+
curve.append((float(tokens[0]), float(tokens[1])))
|
|
280
|
+
return curve or None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _parse_bmd_points(text: str | None) -> list[BMDPoint]:
|
|
284
|
+
points: list[BMDPoint] = []
|
|
285
|
+
for tokens in _parse_text_lines(text):
|
|
286
|
+
if len(tokens) >= 3:
|
|
287
|
+
points.append(
|
|
288
|
+
BMDPoint(
|
|
289
|
+
position=float(tokens[0]),
|
|
290
|
+
moment=float(tokens[1]),
|
|
291
|
+
flag=int(tokens[2]),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
return points
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _parse_mat_zones(text: str | None) -> list[MatZone]:
|
|
298
|
+
zones: list[MatZone] = []
|
|
299
|
+
for tokens in _parse_text_lines(text):
|
|
300
|
+
if len(tokens) >= 2:
|
|
301
|
+
zones.append(MatZone(position=float(tokens[0]), section_name=tokens[1]))
|
|
302
|
+
return zones
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _parse_shrink_therm(text: str | None) -> list[tuple[float, float]]:
|
|
306
|
+
profile: list[tuple[float, float]] = []
|
|
307
|
+
for tokens in _parse_text_lines(text):
|
|
308
|
+
if len(tokens) >= 2:
|
|
309
|
+
profile.append((float(tokens[0]), float(tokens[1])))
|
|
310
|
+
return profile
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Attribute helpers
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _float(el: ET.Element, attr: str, default: float | None = None) -> float | None:
|
|
319
|
+
val = el.get(attr)
|
|
320
|
+
if val is None:
|
|
321
|
+
return default
|
|
322
|
+
return float(val)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _float_req(el: ET.Element, attr: str) -> float:
|
|
326
|
+
val = el.get(attr)
|
|
327
|
+
if val is None:
|
|
328
|
+
raise ValueError(f"Required attribute {attr!r} missing on <{el.tag}>")
|
|
329
|
+
return float(val)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _int(el: ET.Element, attr: str, default: int = 0) -> int:
|
|
333
|
+
val = el.get(attr)
|
|
334
|
+
if val is None:
|
|
335
|
+
return default
|
|
336
|
+
return int(val)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _str(el: ET.Element, attr: str, default: str = "") -> str:
|
|
340
|
+
val = el.get(attr)
|
|
341
|
+
return val if val is not None else default
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Element parsers
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
_concrete_counter: int = 0
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _map_c_soft(r2k_code: int) -> int:
|
|
352
|
+
"""Map R2K c_soft code (1-based) to internal code.
|
|
353
|
+
|
|
354
|
+
Internal: 0=None (β=1), 1=Collins-Bentz (default), 2=Vecchio-Collins 1982.
|
|
355
|
+
R2K: 1=None, 2=VC1982, 18=Collins-Bentz 2011 (default). Others → Collins-Bentz.
|
|
356
|
+
"""
|
|
357
|
+
if r2k_code == 1:
|
|
358
|
+
return 0 # None
|
|
359
|
+
if r2k_code == 2:
|
|
360
|
+
return 2 # Vecchio-Collins 1982
|
|
361
|
+
return 1 # Collins-Bentz for everything else (including default 18)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _parse_concrete(el: ET.Element) -> Concrete:
|
|
365
|
+
global _concrete_counter
|
|
366
|
+
_concrete_counter += 1
|
|
367
|
+
name = _str(el, "name", f"Concrete {_concrete_counter}")
|
|
368
|
+
c_mod_r2k = _int(el, "c_mod", 2)
|
|
369
|
+
# R2K codes: 1=Linear, 2=Parabolic(Popovics), 3=Popovics/Thorenfeldt, 4=Elasto-Plastic, 5=User-Defined
|
|
370
|
+
# Internal codes (materials.py): 0=Linear, 1=Parabolic, 2=Popovics
|
|
371
|
+
# Empirically: R2K default c_mod=2 ("Parabolic") matches our Popovics (2) — no offset for code 2+.
|
|
372
|
+
# Only special case: R2K c_mod=1 (Linear) must map to our 0 (Linear).
|
|
373
|
+
c_mod = 0 if c_mod_r2k == 1 else c_mod_r2k
|
|
374
|
+
curve = _parse_curve(el.text) if c_mod_r2k == 4 else None # c_mod=4 = User-Defined in R2K
|
|
375
|
+
return Concrete(
|
|
376
|
+
name=name,
|
|
377
|
+
fcp=_float_req(el, "fcp"),
|
|
378
|
+
maxagg=_float(el, "maxagg", 20.0), # type: ignore[arg-type]
|
|
379
|
+
c_mod=c_mod,
|
|
380
|
+
c_soft=_map_c_soft(_int(el, "c_soft", 18)),
|
|
381
|
+
tsfactor=_float(el, "tsfactor", 1.0), # type: ignore[arg-type]
|
|
382
|
+
ft=_float(el, "ft"),
|
|
383
|
+
e0=_float(el, "e0"),
|
|
384
|
+
Ec=_float(el, "E"),
|
|
385
|
+
curve=curve,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _parse_rebar(el: ET.Element) -> Rebar:
|
|
390
|
+
return Rebar(
|
|
391
|
+
name=_str(el, "name", "Steel"),
|
|
392
|
+
fy=_float_req(el, "fy"),
|
|
393
|
+
E=_float(el, "E", 200_000.0), # type: ignore[arg-type]
|
|
394
|
+
fu=_float(el, "fu"),
|
|
395
|
+
esh=_float(el, "esh", 10.0), # type: ignore[arg-type]
|
|
396
|
+
eu=_float(el, "eu", 100.0), # type: ignore[arg-type]
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _parse_prestress(el: ET.Element) -> Prestress:
|
|
401
|
+
return Prestress(
|
|
402
|
+
name=_str(el, "name", "P-Steel"),
|
|
403
|
+
fu=_float_req(el, "fu"),
|
|
404
|
+
E=_float(el, "E", 190_000.0), # type: ignore[arg-type]
|
|
405
|
+
A=_float(el, "A", 0.030), # type: ignore[arg-type]
|
|
406
|
+
B=_float(el, "B", 121.0), # type: ignore[arg-type]
|
|
407
|
+
C=_float(el, "C", 6.0), # type: ignore[arg-type]
|
|
408
|
+
eu=_float(el, "eu", 40.0), # type: ignore[arg-type]
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _parse_long_reinf(el: ET.Element) -> LongReinf:
|
|
413
|
+
return LongReinf(
|
|
414
|
+
z=_float_req(el, "z"),
|
|
415
|
+
type=_str(el, "type", "Steel"),
|
|
416
|
+
A=_float_req(el, "A"),
|
|
417
|
+
name=_str(el, "name"),
|
|
418
|
+
num=_int(el, "num", 1),
|
|
419
|
+
Ai=_float(el, "Ai"),
|
|
420
|
+
db=_float(el, "db"),
|
|
421
|
+
space=_float(el, "space", 0.0), # type: ignore[arg-type]
|
|
422
|
+
dep=_float(el, "dep", 0.0), # type: ignore[arg-type]
|
|
423
|
+
drape=_float(el, "drape", 0.0), # type: ignore[arg-type]
|
|
424
|
+
bartitle=_str(el, "bartitle"),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _parse_trans_reinf(el: ET.Element) -> TransReinf:
|
|
429
|
+
pattern = _int(el, "pattern", 0)
|
|
430
|
+
if pattern == 7:
|
|
431
|
+
logger.warning(
|
|
432
|
+
"Unknown transverse reinforcement pattern=7 on %r, treating as pattern=1 (open stirrup)",
|
|
433
|
+
_str(el, "name", "<unnamed>"),
|
|
434
|
+
)
|
|
435
|
+
return TransReinf(
|
|
436
|
+
A=_float_req(el, "A"),
|
|
437
|
+
type=_str(el, "type", "Steel"),
|
|
438
|
+
pattern=pattern,
|
|
439
|
+
space=_float_req(el, "space"),
|
|
440
|
+
disttop=_float(el, "disttop", 0.0), # type: ignore[arg-type]
|
|
441
|
+
distbot=_float(el, "distbot", 0.0), # type: ignore[arg-type]
|
|
442
|
+
name=_str(el, "name"),
|
|
443
|
+
Ai=_float(el, "Ai"),
|
|
444
|
+
db=_float(el, "db"),
|
|
445
|
+
dep=_float(el, "dep", 0.0), # type: ignore[arg-type]
|
|
446
|
+
bartitle=_str(el, "bartitle"),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _parse_elem_info(el: ET.Element) -> ElemInfo:
|
|
451
|
+
return ElemInfo(
|
|
452
|
+
L=_float_req(el, "L"),
|
|
453
|
+
mido2=_float(el, "mido2", 0.0), # type: ignore[arg-type]
|
|
454
|
+
lplate=_float(el, "lplate", 0.0), # type: ignore[arg-type]
|
|
455
|
+
rplate=_float(el, "rplate", 0.0), # type: ignore[arg-type]
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _parse_sect_loading(el: ET.Element) -> SectLoading:
|
|
460
|
+
return SectLoading(
|
|
461
|
+
MM=_float(el, "MM", 0.0), # type: ignore[arg-type]
|
|
462
|
+
MV=_float(el, "MV", 0.0), # type: ignore[arg-type]
|
|
463
|
+
BN=_float(el, "BN", 0.0), # type: ignore[arg-type]
|
|
464
|
+
BM=_float(el, "BM", 0.0), # type: ignore[arg-type]
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# Top-level parsers
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _parse_beam_section(el: ET.Element) -> BeamSection:
|
|
474
|
+
global _concrete_counter
|
|
475
|
+
_concrete_counter = 0
|
|
476
|
+
|
|
477
|
+
section = BeamSection(
|
|
478
|
+
name=_str(el, "name", "Unnamed"),
|
|
479
|
+
doneby=_str(el, "doneby"),
|
|
480
|
+
date=_str(el, "date"),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
for child in el:
|
|
484
|
+
tag = child.tag
|
|
485
|
+
if tag == "concrete":
|
|
486
|
+
c = _parse_concrete(child)
|
|
487
|
+
section.concretes[c.name] = c
|
|
488
|
+
elif tag == "rebar":
|
|
489
|
+
r = _parse_rebar(child)
|
|
490
|
+
section.rebars[r.name] = r
|
|
491
|
+
elif tag == "prestress":
|
|
492
|
+
p = _parse_prestress(child)
|
|
493
|
+
section.prestress[p.name] = p
|
|
494
|
+
elif tag == "sectionSOLID":
|
|
495
|
+
section.section_points = _parse_section_points(child.text)
|
|
496
|
+
elif tag == "shapesection":
|
|
497
|
+
pass # display metadata only
|
|
498
|
+
elif tag == "longreinfx":
|
|
499
|
+
section.long_reinf.append(_parse_long_reinf(child))
|
|
500
|
+
elif tag == "transreinfz":
|
|
501
|
+
section.trans_reinf.append(_parse_trans_reinf(child))
|
|
502
|
+
elif tag == "eleminfo":
|
|
503
|
+
section.elem_info = _parse_elem_info(child)
|
|
504
|
+
elif tag == "sectloading":
|
|
505
|
+
section.sect_loading = _parse_sect_loading(child)
|
|
506
|
+
elif tag == "shrinktherm":
|
|
507
|
+
section.shrink_therm = ShrinkTherm(profile=_parse_shrink_therm(child.text))
|
|
508
|
+
else:
|
|
509
|
+
logger.debug("Ignoring unknown element <%s> in r3g_beam %r", tag, section.name)
|
|
510
|
+
|
|
511
|
+
return section
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _parse_support(el: ET.Element) -> Support:
|
|
515
|
+
return Support(
|
|
516
|
+
which=_str(el, "which", ""),
|
|
517
|
+
platelocate=_float(el, "platelocate", 0.0), # type: ignore[arg-type]
|
|
518
|
+
platesize=_float(el, "platesize", 0.0), # type: ignore[arg-type]
|
|
519
|
+
support=_str(el, "support"),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _parse_bmd(el: ET.Element) -> BMD:
|
|
524
|
+
return BMD(
|
|
525
|
+
points=_parse_bmd_points(el.text),
|
|
526
|
+
isconstant=_str(el, "isconstant") == "1",
|
|
527
|
+
type=_str(el, "type"),
|
|
528
|
+
axial_load=_float(el, "AxialLoad", 0.0), # type: ignore[arg-type]
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _parse_member_analysis(el: ET.Element) -> MemberAnalysis:
|
|
533
|
+
ma = MemberAnalysis(
|
|
534
|
+
length=_float_req(el, "length"),
|
|
535
|
+
topplatesize=_float(el, "topplatesize", 0.0), # type: ignore[arg-type]
|
|
536
|
+
displocate=_float(el, "displocate", 0.0), # type: ignore[arg-type]
|
|
537
|
+
leftanchor=_str(el, "leftanchor"),
|
|
538
|
+
rightanchor=_str(el, "rightanchor"),
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
for child in el:
|
|
542
|
+
tag = child.tag
|
|
543
|
+
if tag == "BMD":
|
|
544
|
+
ma.bmds.append(_parse_bmd(child))
|
|
545
|
+
elif tag == "mattypes":
|
|
546
|
+
ma.mat_zones = _parse_mat_zones(child.text)
|
|
547
|
+
elif tag == "supports":
|
|
548
|
+
ma.supports.append(_parse_support(child))
|
|
549
|
+
else:
|
|
550
|
+
logger.debug("Ignoring unknown element <%s> in R2010_Analysis", tag)
|
|
551
|
+
|
|
552
|
+
return ma
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _parse_document(root: ET.Element) -> R2KModel:
|
|
556
|
+
model = R2KModel()
|
|
557
|
+
for child in root:
|
|
558
|
+
if child.tag == "r3g_beam":
|
|
559
|
+
model.sections.append(_parse_beam_section(child))
|
|
560
|
+
elif child.tag == "R2010_Analysis":
|
|
561
|
+
model.member_analysis = _parse_member_analysis(child)
|
|
562
|
+
else:
|
|
563
|
+
logger.debug("Ignoring unknown top-level element <%s>", child.tag)
|
|
564
|
+
return model
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
# Public API
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def read_rsp(path: str | Path) -> R2KModel:
|
|
573
|
+
"""Parse an .rsp XML file and return the complete model."""
|
|
574
|
+
path = Path(path)
|
|
575
|
+
xml_text = path.read_text(encoding="utf-8")
|
|
576
|
+
return read_rsp_string(xml_text)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def read_rsp_string(xml_text: str) -> R2KModel:
|
|
580
|
+
"""Parse an .rsp XML string and return the complete model."""
|
|
581
|
+
cleaned = _preprocess(xml_text)
|
|
582
|
+
root = ET.fromstring(cleaned)
|
|
583
|
+
return _parse_document(root)
|