Skip to content

Commit bbd157c

Browse files
committed
Update hinged component assembly, solver.
- Add back support for zerolength elements in solver. - Add option to ignore elements based on their tag for a given analysis.
1 parent 1773281 commit bbd157c

File tree

7 files changed

+177
-68
lines changed

7 files changed

+177
-68
lines changed

src/osmg/analysis/load_case.py

+55-31
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
from osmg.analysis.supports import ElasticSupport, FixedSupport
1919
from osmg.core.common import EPSILON, THREE_DIMENSIONAL, TWO_DIMENSIONAL
2020
from osmg.core.osmg_collections import BeamColumnAssembly
21-
from osmg.model_objects.element import BeamColumnElement
21+
from osmg.model_objects.element import BeamColumnElement, Bar
2222

2323
from tqdm import tqdm
2424

2525
if TYPE_CHECKING:
2626
from osmg.analysis.common import PointLoad
2727
from osmg.core.model import Model, Model2D, Model3D
2828
from osmg.model_objects.node import Node
29+
from osmg.core.osmg_collections import ComponentAssembly
2930

3031

3132
def ensure_minmax_level_exists_or_add(data: pd.DataFrame) -> pd.DataFrame:
@@ -241,7 +242,7 @@ def define_rigid_diaphragm(
241242
def calculate_basic_forces( # noqa: C901
242243
self,
243244
recorder_name: str,
244-
element_lengths: dict[int, float],
245+
components: dict[int, ComponentAssembly],
245246
*,
246247
ndm: int,
247248
num_stations: int = 12,
@@ -300,9 +301,25 @@ def calculate_basic_forces( # noqa: C901
300301
raise TypeError(msg)
301302

302303
if isinstance(self, HasLoads):
303-
udls = self.load_registry.element_udl
304+
udls_global = self.load_registry.element_udl
304305
else:
305-
udls = {}
306+
udls_global = {}
307+
308+
udls_local = {}
309+
elements = {}
310+
for component_uid, component in components.items():
311+
if not isinstance(component, BeamColumnAssembly):
312+
continue
313+
for element in component.elements.values():
314+
if isinstance(element, (BeamColumnElement, Bar)):
315+
elements[element.uid] = element
316+
elements.update(component.elements)
317+
component_global_udl = udls_global.get(component_uid)
318+
if component_global_udl:
319+
component_local_udls = component.calculate_element_udl(
320+
component_global_udl
321+
)
322+
udls_local.update(component_local_udls)
306323

307324
data = recorder.get_data()
308325

@@ -327,11 +344,17 @@ def calculate_basic_forces( # noqa: C901
327344
# dof: df.xs(1.0, level='station', axis=1) for dof, df in dof_data.items()
328345
# }
329346

330-
missing_elements = [e for e in recorder.elements if e not in element_lengths]
347+
missing_elements = [e for e in recorder.elements if e not in elements]
331348
if missing_elements:
332-
msg = f'Missing lengths for elements: {missing_elements}'
349+
msg = f'Missing elements: {missing_elements}'
333350
raise ValueError(msg)
334351

352+
element_lengths: dict[int, float] = {
353+
element.uid: element.clear_length()
354+
for element in elements.values()
355+
if isinstance(element, BeamColumnElement)
356+
}
357+
335358
locations = np.array(
336359
[
337360
np.linspace(0.00, element_lengths[element], num=num_stations)
@@ -360,7 +383,9 @@ def calculate_basic_forces( # noqa: C901
360383
# data_j_axial = dof_data_j[1]
361384
w_data_axial = np.array(
362385
[
363-
udls[element_uid][0] if udls.get(element_uid) is not None else 0.00
386+
udls_local[element_uid][0]
387+
if udls_local.get(element_uid) is not None
388+
else 0.00
364389
for element_uid in recorder.elements
365390
]
366391
)[np.newaxis, :, np.newaxis]
@@ -385,7 +410,9 @@ def calculate_basic_forces( # noqa: C901
385410
# data_j_shear_y = dof_data_j[2]
386411
w_data_shear_y = np.array(
387412
[
388-
udls[element_uid][1] if udls.get(element_uid) is not None else 0.00
413+
udls_local[element_uid][1]
414+
if udls_local.get(element_uid) is not None
415+
else 0.00
389416
for element_uid in recorder.elements
390417
]
391418
)[np.newaxis, :, np.newaxis]
@@ -418,8 +445,8 @@ def calculate_basic_forces( # noqa: C901
418445
# data_j_shear_z = dof_data_j[3]
419446
w_data_shear_z = np.array(
420447
[
421-
udls[element_uid][2]
422-
if udls.get(element_uid) is not None
448+
udls_local[element_uid][2]
449+
if udls_local.get(element_uid) is not None
423450
else 0.00
424451
for element_uid in recorder.elements
425452
]
@@ -680,18 +707,16 @@ def self_weight(self, case_name: str, scaling_factor: float = 1.0) -> None:
680707
case_name: Name of the load case to be created.
681708
scaling_factor: Self-weight scaling factor to use.
682709
"""
683-
# get all beamcolumn elements
684-
elements = [
685-
element
710+
# get all beamcolumn assemblies
711+
components = [
712+
component
686713
for component in self.model.components.values()
687714
if isinstance(component, BeamColumnAssembly)
688-
for element in component.elements.values()
689-
if isinstance(element, BeamColumnElement)
690715
]
691-
for element in elements:
692-
weight_per_length = element.section.sec_w * scaling_factor
716+
for component in components:
717+
weight_per_length = component.get_section().sec_w * scaling_factor
693718
udl = UDL((0.00, 0.00, -weight_per_length))
694-
self.dead[case_name].load_registry.element_udl[element.uid] = udl
719+
self.dead[case_name].load_registry.element_udl[component.uid] = udl
695720

696721
def self_mass(
697722
self,
@@ -712,30 +737,29 @@ def self_mass(
712737
a scaling coefficient.
713738
g_constant: Factor to convert loads to mass.
714739
"""
715-
# get all beamcolumn elements
716-
elements = {
717-
element.uid: element
740+
# get all BeamColumn assemblies
741+
components = {
742+
component.uid: component
718743
for component in self.model.components.values()
719744
if isinstance(component, BeamColumnAssembly)
720-
for element in component.elements.values()
721-
if isinstance(element, BeamColumnElement)
722745
}
723746
for source_load_case, factor in source_load_cases:
724747
# Convert UDL to mass
725748
for uid, udl in source_load_case.load_registry.element_udl.items():
726-
element = elements[uid]
727-
length = element.clear_length()
749+
component = components[uid]
750+
length = component.clear_length()
728751
weight = np.abs(udl[-1] * length)
729-
mass = weight * factor / g_constant / 2.0
752+
num_nodes = len(component.internal_nodes)
753+
mass = weight * factor / g_constant / num_nodes
730754
# TODO(JVM): separate cases for other ndm/ndf
731755
# configurations.
732756
point_mass = PointMass((mass, mass, mass, 0.00, 0.00, 0.00))
733-
for node in element.nodes:
734-
if node.uid not in target_load_case.mass_registry:
735-
target_load_case.mass_registry[node.uid] = point_mass
757+
for node_uid in component.internal_nodes:
758+
if node_uid not in target_load_case.mass_registry:
759+
target_load_case.mass_registry[node_uid] = point_mass
736760
else:
737-
existing_mass = target_load_case.mass_registry[node.uid]
738-
target_load_case.mass_registry[node.uid] = PointMass(
761+
existing_mass = target_load_case.mass_registry[node_uid]
762+
target_load_case.mass_registry[node_uid] = PointMass(
739763
(*(e + p for e, p in zip(existing_mass, point_mass)),)
740764
)
741765

src/osmg/analysis/solver.py

+27-13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
DispBeamColumn,
2323
ElasticBeamColumn,
2424
TwoNodeLink,
25+
ZeroLength,
2526
)
2627

2728
try:
@@ -49,6 +50,7 @@ class AnalysisSettings:
4950
constraints: tuple[str] = ('Transformation',)
5051
numberer: Literal['RCM'] = 'RCM'
5152
system: Literal['Umfpack'] = 'Umfpack'
53+
ignore_by_tag: set[str] = field(default_factory=set)
5254

5355

5456
@dataclass()
@@ -152,10 +154,9 @@ def opensees_define_material(self, material: UniaxialMaterial) -> None:
152154
ops.uniaxialMaterial(*material.ops_args())
153155
self._defined_materials.append(material.uid)
154156

155-
@staticmethod
156-
def opensees_define_nodes(model: Model) -> None:
157+
def opensees_define_nodes(self, model: Model) -> None:
157158
"""Define the nodes of the model in OpenSees."""
158-
for uid, node in model.get_all_nodes().items():
159+
for uid, node in model.get_all_nodes(self.settings.ignore_by_tag).items():
159160
ops.node(uid, *node.coordinates)
160161

161162
def opensees_define_elements(
@@ -166,6 +167,7 @@ def opensees_define_elements(
166167
elastic_beamcolumn_elements: list[ElasticBeamColumn] = []
167168
bar_elements: list[Bar] = []
168169
two_node_link_elements: list[TwoNodeLink] = []
170+
zerolength_elements: list[ZeroLength] = []
169171
unsupported_element_types: list[str] = []
170172

171173
# Note: Materials are defined on an as-needed basis. We keep
@@ -176,6 +178,8 @@ def opensees_define_elements(
176178

177179
components = model.components.values()
178180
for component in components:
181+
if component.tags & self.settings.ignore_by_tag:
182+
continue
179183
elements = component.elements
180184
for element in elements.values():
181185
if isinstance(element, ElasticBeamColumn):
@@ -184,6 +188,8 @@ def opensees_define_elements(
184188
bar_elements.append(element)
185189
elif isinstance(element, TwoNodeLink):
186190
two_node_link_elements.append(element)
191+
elif isinstance(element, ZeroLength):
192+
zerolength_elements.append(element)
187193
else:
188194
unsupported_element_types.append(element.__class__.__name__)
189195

@@ -197,6 +203,7 @@ def opensees_define_elements(
197203
)
198204
self.opensees_define_bar_elements(bar_elements)
199205
self.opensees_define_two_node_link_elements(two_node_link_elements)
206+
self.opensees_define_zerolength_elements(zerolength_elements)
200207

201208
# clear defined materials
202209
self._defined_materials = []
@@ -239,6 +246,15 @@ def opensees_define_two_node_link_elements(
239246
self.opensees_define_material(material)
240247
ops.element(*element.ops_args())
241248

249+
def opensees_define_zerolength_elements(
250+
self, elements: list[ZeroLength]
251+
) -> None:
252+
"""Define Zerolength elements."""
253+
for element in elements:
254+
for material in element.materials:
255+
self.opensees_define_material(material)
256+
ops.element(*element.ops_args())
257+
242258
def opensees_define_node_restraints(
243259
self, model: Model, load_case: LoadCase
244260
) -> None:
@@ -299,8 +315,8 @@ def opensees_define_model(self, model: Model, load_case: LoadCase) -> None:
299315
self.define_default_recorders(model)
300316
self.opensees_define_recorders()
301317

302-
@staticmethod
303318
def opensees_define_loads(
319+
self,
304320
model: Model,
305321
load_case: LoadCase,
306322
amplification_factor: float = 1.00,
@@ -326,6 +342,8 @@ def opensees_define_loads(
326342
# UDL on components
327343
for component_uid, global_udl in load_case.load_registry.element_udl.items(): # type: ignore
328344
component = model.components[component_uid]
345+
if component.tags & self.settings.ignore_by_tag:
346+
continue
329347
assert isinstance(component, BeamColumnAssembly)
330348
local_udls = component.calculate_element_udl(global_udl)
331349
for beamcolumn_element_uid, local_udl in local_udls.items():
@@ -389,6 +407,8 @@ def define_default_recorders(self, model: Model) -> None:
389407
applicable_elements = []
390408
components = model.components.values()
391409
for component in components:
410+
if component.tags & self.settings.ignore_by_tag:
411+
continue
392412
elements = component.elements
393413
for element in elements.values():
394414
if isinstance(element, (ElasticBeamColumn, DispBeamColumn, Bar)):
@@ -612,15 +632,9 @@ def run(self, model: Model, load_case: LoadCase) -> None: # noqa: C901 # type:
612632
# in the dict.
613633

614634
# Verifying displacements are correct.
615-
for some_node in mode_eigenvectors.index.get_level_values(
616-
'node'
617-
).unique():
618-
for check_dof in range(1, ndf + 1):
619-
if (node, check_dof) in mode_eigenvectors:
620-
assert np.allclose(
621-
np.array(ops.nodeDisp(some_node))[check_dof - 1],
622-
mode_eigenvectors[some_node, check_dof],
623-
)
635+
for key, value in mode_eigenvectors.items():
636+
node_uid, dof = key
637+
assert np.allclose(ops.nodeDisp(node_uid, dof), value)
624638

625639
self.log(' Wiping OpenSees domain.')
626640
# Doing this before reading the basic force recorder data

src/osmg/core/model.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,15 @@ def reference_length(self) -> float:
6969
p_min, p_max = self.bounding_box(padding=0.00)
7070
return float(np.max(p_max - p_min))
7171

72-
def get_all_nodes(self) -> dict[int, Node]:
72+
def get_all_nodes(
73+
self, ignore_by_tag: set[str] | None = None
74+
) -> dict[int, Node]:
7375
"""
7476
Get all nodes in the model.
7577
78+
Params:
79+
ignore_by_tag: Set of tags of components to ignore.
80+
7681
Returns:
7782
A dictionary with the nodes. Keys are their UIDs.
7883
"""
@@ -82,6 +87,8 @@ def get_all_nodes(self) -> dict[int, Node]:
8287
internal_nodes: dict[int, Node] = {}
8388
components = self.components.values()
8489
for component in components:
90+
if ignore_by_tag and component.tags & ignore_by_tag:
91+
continue
8592
internal_nodes.update(component.internal_nodes)
8693

8794
all_nodes: dict[int, Node] = {}

0 commit comments

Comments
 (0)