Function bodies 335 total
compute_qult function · python · L615-L655 (41 LOC)src/nlb/opensees/materials.py
def compute_qult(diameter: float, depth: float,
soil_layer: SoilLayer) -> float:
"""Compute ultimate tip bearing capacity.
For clay: q_p = Nc * su (Nc = 9 for deep foundations).
For sand: q_p = Nq * sigma_v' (Reese & O'Neill / FHWA method).
Args:
diameter: Pile diameter (inches).
depth: Depth to pile tip (inches).
soil_layer: SoilLayer at tip.
Returns:
Ultimate tip resistance qult (kip) — total force at tip.
Reference:
Reese & O'Neill (1988). FHWA-HI-88-042.
FHWA-NHI-10-016: Drilled Shafts, Chapter 13.
AASHTO LRFD 10.8.3.5.
"""
area = math.pi * (diameter / 2.0) ** 2 # in²
if soil_layer.soil_type == "clay":
# Bearing capacity: qp = Nc * su
# Nc = 6[1 + 0.2(Z/D)] <= 9 for drilled shafts
Nc = min(9.0, 6.0 * (1.0 + 0.2 * depth / diameter)) if diameter > 0 else 9.0
qp = Nc * soil_layer.su # ksi
else:
# Sand: q_p = 0.6 * Nq elastomeric_shear function · python · L662-L682 (21 LOC)src/nlb/opensees/materials.py
def elastomeric_shear(tag: int, G: float, A: float, h: float) -> int:
"""Define elastic material for elastomeric bearing pad shear stiffness.
Stiffness: k = G * A / h (kip/in).
Args:
tag: Material tag.
G: Shear modulus of elastomer (ksi). Typical: 0.080-0.175 ksi
(AASHTO Table 14.7.6.2-1).
A: Plan area of elastomer (in²).
h: Total elastomer thickness (in), sum of all layers.
Returns:
Material tag.
Reference:
AASHTO LRFD 14.7.6.2: Design of Elastomeric Bearings.
"""
k = G * A / h # kip/in
ops.uniaxialMaterial('Elastic', tag, k)
return tagfriction_model function · python · L685-L720 (36 LOC)src/nlb/opensees/materials.py
def friction_model(tag: int, mu_slow: float = 0.06, mu_fast: float = 0.10,
rate: float = 0.001) -> int:
"""Define velocity-dependent friction material for sliding bearings.
Models the transition from static (slow) to kinetic (fast) friction,
typical of PTFE/stainless steel sliding surfaces.
Note: OpenSees uses ``frictionModel`` for friction pendulum elements.
For simpler modeling, this uses a VelDepMultiLinear or equivalent.
Here we use the Elastic material as a simplified placeholder with
the slow friction coefficient, since VelDependent friction models
are element-level in OpenSees (not uniaxial materials).
For full friction pendulum modeling, use the SingleFPBearing element
or FlatSliderBearing element directly.
Args:
tag: Material tag.
mu_slow: Coefficient of friction at low velocity. Default: 0.06.
mu_fast: Coefficient of friction at high velocity. Default: 0.10.
rate: Ratcompression_only function · python · L723-L740 (18 LOC)src/nlb/opensees/materials.py
def compression_only(tag: int, k: float) -> int:
"""Define elastic-no-tension (ENT) material.
For bearings that only transmit compression (e.g., concrete on concrete,
rocker bearings, expansion bearings under uplift).
Args:
tag: Material tag.
k: Compressive stiffness (kip/in).
Returns:
Material tag.
Reference:
AASHTO LRFD 14.8: Anchorage and Uplift.
"""
ops.uniaxialMaterial('ENT', tag, k)
return tagsteel_i_section function · python · L74-L145 (72 LOC)src/nlb/opensees/sections.py
def steel_i_section(tag: int, d: float, bf_top: float, tf_top: float,
bf_bot: float, tf_bot: float, tw: float,
mat_flange: int, mat_web: int,
nf_flange: int = 4, nf_web: int = 8) -> int:
"""Create fiber section for a steel I-shape (plate girder or rolled).
The section is modeled with rectangular fiber patches for each component:
top flange, bottom flange, and web.
Coordinate system:
y=0 at centroid, positive up.
z=0 at center of web.
Args:
tag: Section tag.
d: Total depth (inches).
bf_top: Top flange width (inches).
tf_top: Top flange thickness (inches).
bf_bot: Bottom flange width (inches).
tf_bot: Bottom flange thickness (inches).
tw: Web thickness (inches).
mat_flange: Material tag for flanges.
mat_web: Material tag for web.
nf_flange: Number of fibers across flancomposite_section function · python · L152-L237 (86 LOC)src/nlb/opensees/sections.py
def composite_section(tag: int, steel_section: Dict[str, float],
slab_width: float, slab_thick: float,
haunch: float, mat_steel: int,
mat_concrete: int,
nf_slab: int = 4, nf_flange: int = 4,
nf_web: int = 8) -> int:
"""Create composite steel + concrete slab fiber section.
Models a steel I-girder acting compositely with a concrete deck slab.
The slab is placed above the steel section with an optional haunch.
Args:
tag: Section tag.
steel_section: Dict with keys: 'd', 'bf_top', 'tf_top', 'bf_bot',
'tf_bot', 'tw' (all in inches).
slab_width: Effective slab width (inches). Per AASHTO 4.6.2.6.
slab_thick: Slab thickness (inches).
haunch: Haunch depth between top flange and slab bottom (inches).
mat_steel: Material tag for steel.
mat_concrete: Material circular_rc_section function · python · L244-L298 (55 LOC)src/nlb/opensees/sections.py
def circular_rc_section(tag: int, diameter: float, cover: float,
num_bars: int, bar_area: float,
mat_confined: int, mat_unconfined: int,
mat_steel: int,
n_core_circ: int = 16, n_core_rad: int = 8,
n_cover_circ: int = 16, n_cover_rad: int = 2) -> int:
"""Create fiber section for a circular reinforced concrete column.
Divides the section into:
1. Core (confined concrete): inside the reinforcing cage
2. Cover (unconfined concrete): outside the cage to surface
3. Reinforcing steel: single layer of bars on a circle
Args:
tag: Section tag.
diameter: Outer diameter (inches).
cover: Clear cover to outside of hoop/spiral (inches).
num_bars: Number of longitudinal bars.
bar_area: Area of each bar (in²). e.g., #8 = 0.79 in².
mat_confined: Material tag for confiAll rows scored by the Repobility analyzer (https://repobility.com)
BarLayout class · python · L306-L316 (11 LOC)src/nlb/opensees/sections.py
class BarLayout:
"""Defines reinforcing bar layout for one face of a rectangular section.
Attributes:
num_bars: Number of bars on this face.
bar_area: Area of each bar (in²).
face: "top", "bottom", "left", "right", or "corner".
"""
num_bars: int
bar_area: float
face: str = "bottom"rectangular_rc_section function · python · L319-L403 (85 LOC)src/nlb/opensees/sections.py
def rectangular_rc_section(tag: int, width: float, height: float,
cover: float, bars_layout: List[BarLayout],
mat_confined: int, mat_unconfined: int,
mat_steel: int,
nfy_core: int = 10, nfz_core: int = 10,
nf_cover: int = 2) -> int:
"""Create fiber section for a rectangular reinforced concrete column.
Divides the section into confined core, four cover patches (top, bottom,
left, right), and reinforcing bar layers.
Args:
tag: Section tag.
width: Section width, z-direction (inches).
height: Section height, y-direction (inches).
cover: Clear cover (inches).
bars_layout: List of BarLayout objects defining reinforcement.
mat_confined: Material tag for confined concrete.
mat_unconfined: Material tag for unconfined concrete.
mat_steel: TendonProfile class · python · L411-L421 (11 LOC)src/nlb/opensees/sections.py
class TendonProfile:
"""Tendon location within a section.
Attributes:
y: Vertical position from section centroid (inches).
z: Horizontal position from section center (inches).
area: Tendon area (in²).
"""
y: float
z: float
area: floatbox_girder_section function · python · L424-L509 (86 LOC)src/nlb/opensees/sections.py
def box_girder_section(tag: int, depth: float, top_width: float,
bot_width: float, top_thick: float,
bot_thick: float, web_thick: float,
num_cells: int, mat_concrete: int,
tendons: Optional[List[TendonProfile]] = None,
mat_strand: Optional[int] = None,
nf_slab: int = 4, nf_web: int = 8) -> int:
"""Create fiber section for a post-tensioned concrete box girder.
Models single or multi-cell box with top slab, bottom slab, and webs.
Optional tendons modeled as steel layers.
Args:
tag: Section tag.
depth: Total depth (inches).
top_width: Top slab width (inches).
bot_width: Bottom slab width (inches).
top_thick: Top slab thickness (inches).
bot_thick: Bottom slab thickness (inches).
web_thick: Individual web thickness (inches).
num_cells: NumbStrandPattern class · python · L517-L527 (11 LOC)src/nlb/opensees/sections.py
class StrandPattern:
"""Strand pattern for prestressed girder.
Attributes:
rows: List of (y_from_bottom, num_strands) tuples.
strand_area: Area per strand (in²). Default: 0.217 in² (0.6" dia).
debond: Optional dict of {row_index: debond_length_inches}.
"""
rows: List[Tuple[float, int]]
strand_area: float = 0.217 # 0.6" diameter strand
debond: Optional[Dict[int, float]] = Noneprestressed_i_section function · python · L530-L616 (87 LOC)src/nlb/opensees/sections.py
def prestressed_i_section(tag: int, girder_type: str,
mat_concrete: int,
strand_pattern: StrandPattern,
mat_strand: int,
nf_flange: int = 4, nf_web: int = 8) -> int:
"""Create fiber section for a prestressed concrete I-girder.
Supports AASHTO, BT (Bulb-Tee), and NU (Nebraska University) girder types.
Girder dimensions are looked up from the built-in GIRDER_LIBRARY.
Args:
tag: Section tag.
girder_type: Key into GIRDER_LIBRARY (e.g., "BT_72", "AASHTO_IV").
mat_concrete: Material tag for concrete.
strand_pattern: StrandPattern defining row positions and counts.
mat_strand: Material tag for prestressing strand.
nf_flange: Fibers through flange thickness. Default: 4.
nf_web: Fibers along web depth. Default: 8.
Returns:
Section tag.
Raises:
KeyError: If girdeAssembledModel class · python · L68-L105 (38 LOC)src/nlb/tools/assembler.py
class AssembledModel:
"""Complete bridge model ready for analysis.
Attributes:
script: Complete OpenSees Python script.
node_count: Total number of nodes.
element_count: Total number of elements.
material_count: Total number of materials/sections.
load_cases: Number of load cases.
load_combinations: Number of load combinations.
analysis_sequence: Ordered list of analysis step descriptions.
nodes: All node dicts with global tags.
elements: All element dicts with global tags.
materials: All material dicts with global tags.
sections: All section dicts with global tags.
constraints: All constraint dicts with global tags.
boundary_conditions: All fixity dicts with global tags.
connections: Connection metadata (foundation→sub→bearing→super).
bounding_cases: AnalysisResults class · python · L109-L126 (18 LOC)src/nlb/tools/assembler.py
class AnalysisResults:
"""Results from all analyses.
Attributes:
envelopes: {element_tag: {force_type: {max, min, controlling_combo}}}
dcr: {element_tag: {limit_state: dcr_value}}
reactions: {node_tag: {Fx, Fy, Fz, Mx, My, Mz} per combo}
displacements: {node_tag: {dx, dy, dz, rx, ry, rz} per combo}
modal: {mode: {period, frequency, mass_participation}}
controlling_cases: [{element, check, dcr, combo, description}]
"""
envelopes: dict = field(default_factory=dict)
moving_load_envelopes: dict = field(default_factory=dict)
dcr: dict = field(default_factory=dict)
reactions: dict = field(default_factory=dict)
displacements: dict = field(default_factory=dict)
modal: dict = field(default_factory=dict)
controlling_cases: list[dict] = field(default_factory=list)Repobility · severity-and-effort ranking · https://repobility.com
TagRemapper class · python · L133-L195 (63 LOC)src/nlb/tools/assembler.py
class TagRemapper:
"""Remaps local component tags to global non-colliding ranges.
Each component tool assigns tags starting from 1. This remapper shifts
all tags into the appropriate global range and tracks the mapping so
inter-component connections can be resolved.
"""
def __init__(self, component: str, support_index: int = 0):
"""
Args:
component: One of 'foundation', 'substructure', 'bearing', 'superstructure'.
support_index: Index of the support (0, 1, 2, ...) to space out
foundations/substructures/bearings within their range.
"""
self.component = component
self.support_index = support_index
ranges = TAG_RANGES[component]
# Compute per-support offset within the component range
range_size = ranges["node"][1] - ranges["node"][0] + 1
# Divide range evenly among supports (max 20 supports)
max_supports = 20
per_support =__init__ method · python · L141-L163 (23 LOC)src/nlb/tools/assembler.py
def __init__(self, component: str, support_index: int = 0):
"""
Args:
component: One of 'foundation', 'substructure', 'bearing', 'superstructure'.
support_index: Index of the support (0, 1, 2, ...) to space out
foundations/substructures/bearings within their range.
"""
self.component = component
self.support_index = support_index
ranges = TAG_RANGES[component]
# Compute per-support offset within the component range
range_size = ranges["node"][1] - ranges["node"][0] + 1
# Divide range evenly among supports (max 20 supports)
max_supports = 20
per_support = range_size // max_supports
self.offsets = {}
for kind in ("node", "element", "material"):
base = ranges[kind][0]
self.offsets[kind] = base + support_index * per_support
self._map: dict[str, dict[int, int]] = {"node": {}, "element": {}, "materremap method · python · L165-L179 (15 LOC)src/nlb/tools/assembler.py
def remap(self, local_tag: int, kind: str) -> int:
"""Remap a local tag to global.
Args:
local_tag: Original tag from component tool.
kind: 'node', 'element', or 'material'.
Returns:
Global tag.
"""
if local_tag in self._map[kind]:
return self._map[kind][local_tag]
global_tag = self.offsets[kind] + local_tag
self._map[kind][local_tag] = global_tag
return global_tagget_global method · python · L181-L183 (3 LOC)src/nlb/tools/assembler.py
def get_global(self, local_tag: int, kind: str) -> int:
"""Look up a previously remapped tag."""
return self._map[kind].get(local_tag, self.offsets[kind] + local_tag)_normalize_component function · python · L198-L327 (130 LOC)src/nlb/tools/assembler.py
def _normalize_component(component: dict) -> dict:
"""Normalize all tags in a component dict so they start from 1.
Component tools may use arbitrary internal tag ranges (e.g., foundation
starts nodes at 1004 or 2000). This renumbers everything to start from 1
so the TagRemapper works correctly.
"""
comp = dict(component)
# Build offset maps for each tag type
for tag_type, list_key in [("node", "nodes"), ("element", "elements"), ("material", "materials"), ("section", "sections")]:
items = comp.get(list_key, [])
if not items:
continue
tags = [item.get("tag", 0) for item in items]
if not tags or min(tags) <= 1:
continue
min_tag = min(tags)
offset = min_tag - 1
tag_map = {}
# Renumber items
new_items = []
for item in items:
new = dict(item)
old_tag = new.get("tag", 0)
new_tag = old_tag - offse_remap_component_nodes function · python · L330-L339 (10 LOC)src/nlb/tools/assembler.py
def _remap_component_nodes(nodes: list[dict], remapper: TagRemapper) -> list[dict]:
"""Remap node tags in a list of node dicts."""
result = []
for n in nodes:
new = dict(n)
new["tag"] = remapper.remap(n["tag"], "node")
new["_original_tag"] = n["tag"]
new["_component"] = remapper.component
result.append(new)
return result_remap_component_elements function · python · L342-L364 (23 LOC)src/nlb/tools/assembler.py
def _remap_component_elements(elements: list[dict], remapper: TagRemapper) -> list[dict]:
"""Remap element and node tags in a list of element dicts."""
result = []
for e in elements:
new = dict(e)
new["tag"] = remapper.remap(e["tag"], "element")
new["_original_tag"] = e["tag"]
new["_component"] = remapper.component
if "nodes" in e:
new["nodes"] = [remapper.remap(n, "node") for n in e["nodes"]]
if "section" in e and isinstance(e["section"], int):
new["section"] = remapper.remap(e["section"], "material")
if "transform" in e and isinstance(e["transform"], int):
new["transform"] = remapper.remap(e["transform"], "material")
if "material" in e and isinstance(e["material"], int):
new["material"] = remapper.remap(e["material"], "material")
if "materials" in e and isinstance(e["materials"], list):
new["materials"] = [
remapper.remap(m, "_remap_component_materials function · python · L367-L392 (26 LOC)src/nlb/tools/assembler.py
def _remap_component_materials(materials: list[dict], remapper: TagRemapper) -> list[dict]:
"""Remap material tags (and cross-references) in a list of material dicts."""
result = []
for m in materials:
new = dict(m)
new["tag"] = remapper.remap(m["tag"], "material")
new["_original_tag"] = m["tag"]
new["_component"] = remapper.component
# Remap cross-references in params if they are material tags
if "params" in m and isinstance(m["params"], dict):
params = dict(m["params"])
for key in ("mat_confined", "mat_unconfined", "mat_steel",
"mat_concrete", "mat_flange", "mat_web", "mat_strand",
"material", "matTag"):
if key in params and isinstance(params[key], int):
params[key] = remapper.remap(params[key], "material")
# Handle nested dicts (FiberSection core/cover/steel)
for sub_key in ("core", "cover", "sRepobility — same analyzer, your code, free for public repos · /scan/
_remap_component_sections function · python · L395-L417 (23 LOC)src/nlb/tools/assembler.py
def _remap_component_sections(sections: list[dict], remapper: TagRemapper) -> list[dict]:
"""Remap section tags and material references."""
result = []
for s in sections:
new = dict(s)
new["tag"] = remapper.remap(s["tag"], "material") # sections share material tag space
new["_original_tag"] = s["tag"]
new["_component"] = remapper.component
# Remap top-level material references
for key in ("mat_confined", "mat_unconfined", "mat_steel",
"mat_concrete", "mat_flange", "mat_web", "mat_strand"):
if key in new and isinstance(new[key], int):
new[key] = remapper.remap(new[key], "material")
if "params" in s and isinstance(s["params"], dict):
params = dict(s["params"])
for key in ("mat_confined", "mat_unconfined", "mat_steel",
"mat_concrete", "mat_flange", "mat_web", "mat_strand",
"matTag"):
if _remap_constraints function · python · L420-L432 (13 LOC)src/nlb/tools/assembler.py
def _remap_constraints(constraints: list[dict], remapper: TagRemapper) -> list[dict]:
"""Remap node references in constraints."""
result = []
for c in constraints:
new = dict(c)
if "master" in c:
new["master"] = remapper.remap(c["master"], "node")
if "slave" in c:
new["slave"] = remapper.remap(c["slave"], "node")
if "nodes" in c:
new["nodes"] = [remapper.remap(n, "node") for n in c["nodes"]]
result.append(new)
return resultConnectionError class · python · L439-L441 (3 LOC)src/nlb/tools/assembler.py
class ConnectionError(Exception):
"""Raised when components cannot be connected."""
pass_connect_foundation_to_substructure function · python · L444-L481 (38 LOC)src/nlb/tools/assembler.py
def _connect_foundation_to_substructure(
fnd_remapper: TagRemapper,
sub_remapper: TagRemapper,
fnd_model: dict,
sub_model: dict,
) -> list[dict]:
"""Connect foundation top node(s) to substructure base node(s).
Uses equalDOF constraints to tie the foundation top to the substructure
base at each support.
Returns list of constraint dicts.
"""
constraints = []
fnd_top = fnd_model.get("top_node", 0)
sub_bases = sub_model.get("base_nodes", [])
if not sub_bases:
raise ConnectionError(
"Substructure has no base_nodes — cannot connect to foundation."
)
# Foundation top_node → substructure base_node(s)
# If single foundation top, connect to all sub bases via equalDOF
fnd_top_global = fnd_remapper.get_global(fnd_top, "node")
for base_node in sub_bases:
sub_base_global = sub_remapper.get_global(base_node, "node")
constraints.append({
"type": "equalDOF",
"mas_connect_substructure_to_bearing function · python · L484-L558 (75 LOC)src/nlb/tools/assembler.py
def _connect_substructure_to_bearing(
sub_remapper: TagRemapper,
brg_remapper: TagRemapper,
sub_model: dict,
brg_model: dict,
) -> list[dict]:
"""Connect substructure top/cap nodes to bearing bottom nodes.
Returns list of constraint dicts (equalDOF).
"""
constraints = []
# Prefer cap_nodes, fall back to top_nodes
sub_tops = sub_model.get("cap_nodes", []) or sub_model.get("top_nodes", [])
brg_bots = brg_model.get("bottom_nodes", [])
if not sub_tops:
raise ConnectionError(
"Substructure has no top_nodes or cap_nodes for bearing connection."
)
if not brg_bots:
raise ConnectionError(
"Bearing model has no bottom_nodes for substructure connection."
)
# Match one-to-one if counts match, or connect all bearings to first sub top
if len(sub_tops) == len(brg_bots):
for s_node, b_node in zip(sub_tops, brg_bots):
s_global = sub_remapper.get_global(s_node, "nod_connect_bearing_to_superstructure function · python · L561-L630 (70 LOC)src/nlb/tools/assembler.py
def _connect_bearing_to_superstructure(
brg_remapper: TagRemapper,
sup_remapper: TagRemapper,
brg_model: dict,
sup_model: dict,
support_index: int,
num_supports: int,
num_girders: int,
) -> list[dict]:
"""Connect bearing top nodes to superstructure support nodes.
The superstructure support_nodes are ordered:
[support_0_girder_0, support_0_girder_1, ..., support_1_girder_0, ...]
Returns list of constraint dicts (equalDOF).
"""
constraints = []
brg_tops = brg_model.get("top_nodes", [])
sup_supports = sup_model.get("support_nodes", [])
if not brg_tops:
raise ConnectionError("Bearing has no top_nodes for superstructure connection.")
if not sup_supports:
raise ConnectionError("Superstructure has no support_nodes for bearing connection.")
# Extract superstructure nodes for this support line
# support_nodes has num_girders entries per support, ordered by support then girder
start = support_indenumerate_bounding_cases function · python · L645-L653 (9 LOC)src/nlb/tools/assembler.py
def enumerate_bounding_cases() -> dict[str, dict[str, str]]:
"""Return the 4 standard bounding case combinations.
UU = Upper foundation springs + Upper bearing stiffness (stiffer → higher forces)
UL = Upper foundation springs + Lower bearing stiffness
LU = Lower foundation springs + Upper bearing stiffness
LL = Lower foundation springs + Lower bearing stiffness (softer → larger displacements)
"""
return dict(BOUNDING_LABELS)build_analysis_sequence function · python · L660-L695 (36 LOC)src/nlb/tools/assembler.py
def build_analysis_sequence(
has_seismic: bool = False,
sdc: str = "B",
has_moving_load: bool = True,
) -> list[str]:
"""Build the standard analysis sequence for a bridge model.
Args:
has_seismic: Whether seismic load cases exist.
sdc: Seismic Design Category (A, B, C, D).
has_moving_load: Whether to include HL-93 moving load analysis.
Returns:
Ordered list of analysis step names.
"""
sequence = [
"model_setup", # ops.wipe() + ndm=3, ndf=6
"define_materials", # All materials, sections, transforms
"define_nodes", # All nodes with fixity
"define_elements", # All elements
"gravity_analysis", # DC + DW, load-controlled
"modal_analysis", # Eigenvalue → periods + mode shapes
]
if has_moving_load:
sequence.append("moving_load_analysis") # HL-93 influence lines
if has_seismic:
sequence.append("response_sWant this analysis on your repo? https://repobility.com/scan/
_generate_model_setup function · python · L702-L731 (30 LOC)src/nlb/tools/assembler.py
def _generate_model_setup() -> str:
"""Generate the model setup preamble."""
return textwrap.dedent("""\
#!/usr/bin/env python3
\"\"\"Auto-generated OpenSees bridge model.
Generated by Natural Language Builder assembler tool.
Units: kip-inch-second (KIS).
\"\"\"
import json
import math
import sys
try:
import openseespy.opensees as ops
except (ImportError, RuntimeError):
try:
import opensees.openseespy as ops
except ImportError:
print("ERROR: openseespy not installed. pip install opensees")
sys.exit(1)
# ============================================================
# MODEL SETUP
# ============================================================
ops.wipe()
ops.model('basic', '-ndm', 3, '-ndf', 6)
""")_generate_materials_script function · python · L734-L1023 (290 LOC)src/nlb/tools/assembler.py
def _generate_materials_script(materials: list[dict], sections: list[dict]) -> str:
"""Generate OpenSees material/section definition commands."""
lines = [
"# ============================================================",
"# MATERIALS AND SECTIONS",
"# ============================================================",
"",
"# Default geometric transforms (fallback for any component)",
"ops.geomTransf('PDelta', 1, 0.0, 0.0, 1.0)",
"ops.geomTransf('Linear', 2, 0.0, 0.0, 1.0)",
"ops.geomTransf('Corotational', 3, 0.0, 0.0, 1.0)",
"",
]
# First pass: emit all geomTransf definitions (they share tag space but must not be deduped against materials)
# We'll defer actual emission until after nodes are known, so we can pick correct vecxz
# For now, collect transform metadata
transform_meta = {} # tag -> {type, vecxz}
for m in materials:
mtype = m.get("type", "")
if mtype in ("geomT_generate_nodes_script function · python · L1026-L1081 (56 LOC)src/nlb/tools/assembler.py
def _generate_nodes_script(nodes: list[dict], boundary_conditions: list[dict],
elements: list[dict] | None = None,
constraints: list[dict] | None = None) -> str:
"""Generate OpenSees node definition commands."""
lines = [
"# ============================================================",
"# NODES",
"# ============================================================",
"",
]
# Build element/constraint connectivity set FIRST — needed to validate BCs
connected = set()
if elements is not None:
for e in (elements or []):
for n in e.get("nodes", []):
connected.add(n)
if constraints is not None:
for c in (constraints or []):
connected.add(c.get("master", 0))
connected.add(c.get("slave", 0))
# Collect fixed nodes for boundary conditions — only if actually connected
fixed_nodes: dict[int, list[int]] = {}
for_generate_elements_script function · python · L1084-L1222 (139 LOC)src/nlb/tools/assembler.py
def _generate_elements_script(elements: list[dict], all_nodes: list[dict] | None = None) -> str:
"""Generate OpenSees element definition commands."""
lines = [
"# ============================================================",
"# ELEMENTS",
"# ============================================================",
"",
]
# Build node coordinate lookup for orientation detection
node_coords = {}
if all_nodes:
for n in all_nodes:
node_coords[n["tag"]] = (n.get("x", 0.0), n.get("y", 0.0), n.get("z", 0.0))
# Track orientation-specific transforms (created on the fly)
# Base transforms 1-3 use vecxz=[0,0,1] — good for horizontal elements
# Vertical elements (Y-dir) need vecxz=[1,0,0]
# Z-direction elements need vecxz=[0,1,0]
orient_transforms = {} # (base_tf, orient_key) -> new_tag
next_orient_tf = 90001 # high range for auto-generated transforms
def _get_oriented_transform(base_tf: int, n1: in_generate_constraints_script function · python · L1225-L1253 (29 LOC)src/nlb/tools/assembler.py
def _generate_constraints_script(constraints: list[dict]) -> str:
"""Generate equalDOF and rigidLink constraint commands."""
lines = [
"# ============================================================",
"# CONSTRAINTS (inter-component connections)",
"# ============================================================",
"",
]
for c in constraints:
ctype = c.get("type", "equalDOF")
if ctype == "equalDOF":
master = c.get("master", 0)
slave = c.get("slave", 0)
dofs = c.get("dofs", [1, 2, 3, 4, 5, 6])
conn = c.get("connection", "")
if conn:
lines.append(f"# {conn}")
lines.append(
f"ops.equalDOF({master}, {slave}, "
f"{', '.join(str(d) for d in dofs)})"
)
elif ctype == "rigidLink":
master = c.get("master", 0)
slave = c.get("slave", 0)
lines.append(f"ops.rigidLingenerate_script function · python · L1687-L1789 (103 LOC)src/nlb/tools/assembler.py
def generate_script(model: AssembledModel) -> str:
"""Generate a standalone OpenSees Python script from an assembled model.
The generated script can be run independently:
python bridge_model.py
Args:
model: AssembledModel with all components assembled.
Returns:
Complete Python script as a string.
"""
if model.num_girders <= 1:
# Line-girder mode: use only superstructure + simple pin supports.
# Skip substructure/foundation/bearing chain to avoid constraint
# force amplification from rigidLinks + penalty constraints.
sup_nodes = [n for n in model.nodes if n.get("_component") == "superstructure"]
sup_elems = [e for e in model.elements if e.get("_component") == "superstructure"]
sup_mats = [m for m in model.materials if m.get("_component") == "superstructure"]
sup_secs = [s for s in model.sections if s.get("_component") == "superstructure"]
# Identify support nodes from super_sanitize_script_for_compatibility function · python · L1792-L1901 (110 LOC)src/nlb/tools/assembler.py
def _sanitize_script_for_compatibility(script: str) -> str:
"""Replace materials/sections that crash on opensees 0.1.x arm64 Mac.
Strategy: two-pass approach.
Pass 1: Identify all fiber section blocks and their material dependencies.
Pass 2: Replace Concrete01/Steel02 with Elastic, replace Fiber sections
with Elastic sections, skip patch/layer commands.
"""
import re
lines = script.split('\n')
# Pass 1: find all Fiber section tags and the materials referenced by patches/layers
fiber_tags = set()
fiber_mat_deps = set() # material tags used inside fiber sections
in_fiber = False
for line in lines:
s = line.strip()
m = re.match(r"ops\.section\('Fiber',\s*(\d+)", s)
if m:
fiber_tags.add(int(m.group(1)))
in_fiber = True
continue
if in_fiber:
# Extract material tags from patch/layer calls
pm = re.match(r"ops\.(?:patch|layer)\('\w+',\s*(\d_emit_elastic_section function · python · L1904-L1912 (9 LOC)src/nlb/tools/assembler.py
def _emit_elastic_section(out: list, tag: int, d: float):
"""Emit an elastic section approximating a circular RC section."""
r = d / 2.0
A = math.pi * r * r
I = math.pi * r ** 4 / 4.0
E = 4000.0 # approximate concrete secant modulus (ksi)
out.append(f"# Fiber→Elastic: d={d:.1f} in, A={A:.1f}, I={I:.1f}")
out.append(f"ops.section('Elastic', {tag}, {E}, {A:.2f}, {I:.2f}, {E}, {A:.2f}, {I / 2:.2f})")
out.append("")All rows scored by the Repobility analyzer (https://repobility.com)
_extract_empty_results function · python · L1919-L1928 (10 LOC)src/nlb/tools/assembler.py
def _extract_empty_results() -> AnalysisResults:
"""Return an empty AnalysisResults placeholder."""
return AnalysisResults(
envelopes={},
dcr={},
reactions={},
displacements={},
modal={},
controlling_cases=[],
)assemble_model function · python · L1935-L2204 (270 LOC)src/nlb/tools/assembler.py
def assemble_model(
site: dict,
foundations: list[dict],
substructures: list[dict],
bearings: list[dict],
superstructure: dict,
loads: dict,
) -> AssembledModel:
"""Assemble all components into a complete OpenSees bridge model.
This is the primary entry point. It:
1. Validates component counts
2. Remaps all tags to global ranges
3. Connects components (foundation → substructure → bearing → superstructure)
4. Builds the analysis sequence
5. Generates the complete OpenSees script
6. Enumerates bounding cases
Args:
site: Site profile dict (from site_recon).
foundations: List of FoundationModel dicts (one per support).
substructures: List of SubstructureModel dicts (one per pier/abutment).
bearings: List of BearingModel dicts (one set per support).
superstructure: SuperstructureModel dict.
loads: LoadModel dict.
Returns:
AssembledModel rearun_analysis function · python · L2211-L2428 (218 LOC)src/nlb/tools/assembler.py
def run_analysis(model: AssembledModel) -> AnalysisResults:
"""Execute full analysis sequence and extract results.
This function requires openseespy to be installed. It:
1. Generates the complete OpenSees script via generate_script()
2. Executes the script (which builds model, runs gravity + modal)
3. Extracts forces, displacements, reactions from the live OpenSees state
4. Populates AnalysisResults with real data
Args:
model: AssembledModel from assemble_model().
Returns:
AnalysisResults with envelopes, DCRs, reactions, displacements, modal data.
Note:
If openseespy is not available, returns empty results with a warning.
"""
import json as _json
import subprocess
import sys
import tempfile
import os
results = AnalysisResults()
# Generate the complete OpenSees script
script = generate_script(model)
# Build a wrapper script that:
# 1. Runs the OpenSees analysis (gravity + modalBearingType class · python · L65-L73 (9 LOC)src/nlb/tools/bearings.py
class BearingType(str, Enum):
ELASTOMERIC = "elastomeric"
POT_FIXED = "pot_fixed"
POT_GUIDED = "pot_guided"
PTFE_SLIDING = "ptfe_sliding"
FRICTION_PENDULUM_SINGLE = "fp_single"
FRICTION_PENDULUM_TRIPLE = "fp_triple"
INTEGRAL = "integral"
ROCKER_ROLLER = "rocker_roller"ElastomericConfig class · python · L81-L101 (21 LOC)src/nlb/tools/bearings.py
class ElastomericConfig:
"""Steel-reinforced elastomeric bearing configuration.
Attributes:
length_in: Bearing length along bridge (inches).
width_in: Bearing width transverse (inches).
total_rubber_thickness_in: Sum of all rubber layers (inches).
shear_modulus_ksi: Elastomer shear modulus G (ksi).
AASHTO Table 14.7.6.2-1: 50 dur = 0.080-0.110 ksi,
60 dur = 0.130-0.200 ksi.
num_internal_layers: Number of internal rubber layers.
layer_thickness_in: Individual rubber layer thickness (inches).
steel_shim_thickness_in: Internal steel shim thickness (inches).
"""
length_in: float = 14.0
width_in: float = 9.0
total_rubber_thickness_in: float = 2.5
shear_modulus_ksi: float = 0.100
num_internal_layers: int = 5
layer_thickness_in: float = 0.50
steel_shim_thickness_in: float = 0.105PotBearingConfig class · python · L105-L115 (11 LOC)src/nlb/tools/bearings.py
class PotBearingConfig:
"""Pot bearing configuration.
Attributes:
vertical_capacity_kip: Vertical load capacity (kip).
guide_direction: For guided: 1=longitudinal, 3=transverse, None=fixed.
stiffness_kip_per_in: Horizontal stiffness for "fixed" direction.
"""
vertical_capacity_kip: float = 500.0
guide_direction: int | None = None # None=fixed, 1=long, 3=trans
stiffness_kip_per_in: float = 1.0e6 # essentially rigidPTFEConfig class · python · L119-L129 (11 LOC)src/nlb/tools/bearings.py
class PTFEConfig:
"""PTFE sliding bearing configuration.
Attributes:
vertical_capacity_kip: Vertical capacity (kip).
ptfe_type: One of: "unfilled", "glass_filled", "carbon_filled", "woven".
contact_pressure_ksi: Average contact pressure on PTFE (ksi).
"""
vertical_capacity_kip: float = 500.0
ptfe_type: str = "glass_filled"
contact_pressure_ksi: float = 3.0FPSingleConfig class · python · L133-L145 (13 LOC)src/nlb/tools/bearings.py
class FPSingleConfig:
"""Single friction pendulum bearing configuration.
Attributes:
radius_in: Radius of curvature R (inches).
mu: Coefficient of friction at reference temperature.
displacement_capacity_in: Maximum displacement (inches).
vertical_capacity_kip: Vertical load capacity (kip).
"""
radius_in: float = 40.0 # ~40" → T ≈ 2.0s
mu: float = 0.06
displacement_capacity_in: float = 8.0
vertical_capacity_kip: float = 500.0Repobility · severity-and-effort ranking · https://repobility.com
FPTripleConfig class · python · L149-L170 (22 LOC)src/nlb/tools/bearings.py
class FPTripleConfig:
"""Triple friction pendulum bearing configuration.
Attributes:
R1, R2, R3, R4: Radii of curvature (inches).
mu1, mu2, mu3, mu4: Friction coefficients per surface.
d1, d2, d3, d4: Displacement capacities per surface (inches).
vertical_capacity_kip: Vertical load capacity (kip).
"""
R1: float = 12.0
R2: float = 88.0
R3: float = 88.0
R4: float = 12.0
mu1: float = 0.012
mu2: float = 0.052
mu3: float = 0.052
mu4: float = 0.012
d1: float = 3.0
d2: float = 12.0
d3: float = 12.0
d4: float = 3.0
vertical_capacity_kip: float = 500.0RockerRollerConfig class · python · L174-L186 (13 LOC)src/nlb/tools/bearings.py
class RockerRollerConfig:
"""Rocker/roller (steel) bearing configuration.
Attributes:
vertical_capacity_kip: Vertical capacity (kip).
rocker_radius_in: Rocker contact radius (inches).
roller_diameter_in: Roller diameter (inches) — 0 if rocker only.
steel_fy_ksi: Steel yield strength (ksi).
"""
vertical_capacity_kip: float = 300.0
rocker_radius_in: float = 6.0
roller_diameter_in: float = 0.0
steel_fy_ksi: float = 36.0BearingModel class · python · L194-L218 (25 LOC)src/nlb/tools/bearings.py
class BearingModel:
"""Complete bearing model output.
All tags are integers for direct OpenSees use.
All coordinates in kip-inch-second.
"""
nodes: list[dict] = field(default_factory=list)
elements: list[dict] = field(default_factory=list)
materials: list[dict] = field(default_factory=list)
constraints: list[dict] = field(default_factory=list)
top_nodes: list[int] = field(default_factory=list)
bottom_nodes: list[int] = field(default_factory=list)
properties: dict = field(default_factory=dict)
cases: dict = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
bearing_type: str = ""
compression_only: bool = True
def summary(self) -> str:
return (
f"BearingModel ({self.bearing_type}): "
f"{len(self.nodes)} nodes, {len(self.elements)} elements, "
f"{len(self.materials)} materials | "
f"top={self.top_nodes}, bot={self.bottom_nodes}"
)