Moving Load Wizard#
|
Driving a wheel load across a single-span slab bridge deck with the Moving Load Wizard:
Keywords:
moving load wizard load model tree table bridge deck influence line |
"""
Moving Load Wizard via the RFEM gRPC API.
Builds a single-span reinforced-concrete slab bridge deck, drives one wheel load
along the traffic lane with the Moving Load Wizard, calculates every generated
position and prints the resulting influence line of the deck deflection.
The model
---------
12.0 m span, 8.0 m wide, 0.60 m C30/37 slab, simply supported at both
abutments. The traffic lane is the deck centreline (y = 4.0 m), modelled as
a LineSet - that line set is the wizard's trajectory.
A LoadModel (the "vehicle") carries one concentrated wheel force. The lane
keeps the wheel 1.0 m clear of each abutment and steps it along in 0.5 m
increments, so the wizard generates one load case per wheel position
(21 of them, x = 1.0 ... 11.0 m).
The wheel force is 150 kN - the EN 1991-2 Load Model 1 tandem axle Q1k =
300 kN shared by two wheels - applied here as a single wheel print for
simplicity.
Wizard tree tables
------------------
The load components of a LoadModel and the lane definitions of a MovingLoad
are tree tables. Their rows are addressed by `key` (independent of the RFEM
language). A row is written through its typed field (e.g. `moving_step`)
and read back through `value`, a string holding the number in SI units.
"""
from dlubal.api import rfem, common
from math import inf
SPAN = 12.0 # m, abutment to abutment
WIDTH = 8.0 # m, deck width
THICKNESS = 0.60 # m
WHEEL_LOAD = 150.0 # kN, EN 1991-2 LM1 tandem axle Q1k = 300 kN over two wheels
MOVING_STEP = 0.5 # m, distance between two generated wheel positions
END_CLEARANCE = 1.0 # m, wheel kept this far from each abutment
def dump_tree(rows, depth=0):
"""Print a wizard tree table: row keys and their values (SI units)."""
for row in rows:
print(f"{' ' * depth + row.key:36s} {row.value}".rstrip())
dump_tree(row.rows, depth + 1)
def tree_value(rows, path) -> float:
"""Value of the tree row addressed by its chain of keys, in SI units."""
for key in path:
row = next(row for row in rows if row.key == key)
rows = row.rows
return float(row.value)
def define_structure() -> list:
"""Simply supported slab deck plus the centreline lane used as trajectory."""
return [
rfem.structure_core.Material(no=1, name="C30/37"),
rfem.structure_core.Thickness(no=1, material=1, uniform_thickness=THICKNESS),
# Deck corners, then the two centreline nodes carrying the lane.
rfem.structure_core.Node(no=1, coordinate_1=0.0, coordinate_2=0.0),
rfem.structure_core.Node(no=2, coordinate_1=SPAN, coordinate_2=0.0),
rfem.structure_core.Node(no=3, coordinate_1=SPAN, coordinate_2=WIDTH),
rfem.structure_core.Node(no=4, coordinate_1=0.0, coordinate_2=WIDTH),
rfem.structure_core.Node(no=5, coordinate_1=0.0, coordinate_2=WIDTH / 2),
rfem.structure_core.Node(no=6, coordinate_1=SPAN, coordinate_2=WIDTH / 2),
rfem.structure_core.Line(no=1, definition_nodes=[1, 2]), # edge beam, y = 0
rfem.structure_core.Line(no=2, definition_nodes=[2, 3]), # abutment, x = SPAN
rfem.structure_core.Line(no=3, definition_nodes=[3, 4]), # edge beam, y = WIDTH
rfem.structure_core.Line(no=4, definition_nodes=[4, 1]), # abutment, x = 0
rfem.structure_core.Line(no=5, definition_nodes=[5, 6]), # traffic lane
rfem.structure_core.Surface(no=1, boundary_lines=[1, 2, 3, 4], thickness=1),
# The trajectory must be a LINE SET, not a plain line.
rfem.structure_core.LineSet(no=1, lines=[5]),
# Simply supported: one abutment held in all three directions, the other
# free to slide along the span so the deck is not restrained axially.
rfem.types_for_lines.LineSupport(
no=1, user_defined_name_enabled=True, name="Fixed bearing",
lines=[4],
spring=common.Vector3d(x=inf, y=inf, z=inf),
rotational_restraint=common.Vector3d(x=0.0, y=0.0, z=0.0),
),
rfem.types_for_lines.LineSupport(
no=2, user_defined_name_enabled=True, name="Sliding bearing",
lines=[2],
spring=common.Vector3d(x=0.0, y=inf, z=inf),
rotational_restraint=common.Vector3d(x=0.0, y=0.0, z=0.0),
),
rfem.loading.StaticAnalysisSettings(no=1),
]
def max_deck_deflection(rfem_app, load_case_no: int) -> tuple[float, float]:
"""Largest downward deck deflection for one load case: (u_z [mm], x [m]).
Global Z points down, so the maximum u_z is the deepest sag. On a simply
supported span it stays near midspan wherever the wheel stands; only its
size changes.
"""
results = rfem_app.get_results(
results_type=rfem.results.STATIC_ANALYSIS_SURFACES_GLOBAL_DEFORMATIONS_MESH_NODES,
filters=[rfem.results.ResultsFilter(column_id='loading',
filter_expression=f'LC{load_case_no}')],
).data
worst = results.loc[results['u_z'].idxmax()]
return float(worst['u_z']) * 1000.0, float(worst['location_x'])
with rfem.Application() as rfem_app:
rfem_app.close_all_models(save_changes=False)
rfem_app.create_model(name='moving_load_wizard')
rfem_app.delete_all_objects()
# The wizard lives in the Load Wizards add-on. The combination wizard stays
# off, so the generated load cases keep the wizard's default action
# category - assigning another one requires the combination wizard.
base_data: rfem.BaseData = rfem_app.get_base_data()
base_data.addons.load_wizards_active = True
base_data.addons.combination_wizard_and_classification_active = False
rfem_app.set_base_data(base_data=base_data)
rfem_app.create_object_list(define_structure())
# --- The vehicle -----------------------------------------------------------
# TYPE_SURFACES because the load travels over the deck slab. RFEM gives the
# load model one component, a concentrated force acting in global Z; only
# its magnitude (SI, in N) is set here, every other row keeps its default.
load_model = rfem.load_wizards.LoadModel
component = load_model.LoadComponentsTreeTableRow
rfem_app.create_object(
load_model(
no=1,
type=load_model.TYPE_SURFACES,
user_defined_name_enabled=True,
name="LM1 wheel",
load_components=load_model.LoadComponentsTreeTable(rows=[
component(key="load_component", rows=[
component(key="parameters", rows=[
component(key="load_magnitude_P", load_magnitude_P=WHEEL_LOAD * 1000.0),
]),
]),
]),
comment="Created via gRPC API",
)
)
components = rfem_app.get_object(load_model(no=1)).load_components
print("=" * 74)
print("Load model components (key, value [SI]):")
print("=" * 74)
dump_tree(components.rows)
wheel_load = tree_value(components.rows,
["load_component", "parameters", "load_magnitude_P"]) / 1000.0
# --- The moving load -------------------------------------------------------
# The lane follows line set 1. Its moving step and its start / end offsets
# are set in the lane-definitions tree; the lane length is taken from the
# line set. Every movement row needs `lane_definition` - without it the
# wizard generates nothing.
moving_load = rfem.load_wizards.MovingLoad
lane = moving_load.LaneDefinitionsTreeTableRow
rfem_app.create_object(
moving_load(
no=1,
user_defined_name_enabled=True,
name="LM1 wheel crossing the deck",
all_surfaces_used=True,
trajectory_set_line=1,
lane_definitions=moving_load.LaneDefinitionsTreeTable(rows=[
lane(key="lane", rows=[
lane(key="parameters", rows=[
lane(key="moving_step", moving_step=MOVING_STEP),
]),
lane(key="offset_and_bumper", rows=[
lane(key="start_offset", start_offset=END_CLEARANCE),
lane(key="end_offset", end_offset=END_CLEARANCE),
]),
]),
]),
movements=moving_load.MovementsTable(rows=[
moving_load.MovementsRow(
no=1,
lane_definition=1,
load_model=1,
load_factor=1.0,
),
]),
generate_load_cases=True,
comment="Created via gRPC API",
)
)
wizard = rfem_app.get_object(moving_load(no=1))
print("=" * 74)
print("Lane definitions (key, value [SI]):")
print("=" * 74)
dump_tree(wizard.lane_definitions.rows)
lane_length = tree_value(wizard.lane_definitions.rows, ["lane", "parameters", "length"])
moving_step = tree_value(wizard.lane_definitions.rows, ["lane", "parameters", "moving_step"])
start_offset = tree_value(wizard.lane_definitions.rows,
["lane", "offset_and_bumper", "start_offset"])
generated = [row.load_case for row in wizard.load_cases.rows]
print("-" * 74)
print(f"lane length : {lane_length:.1f} m (= length of line set 1)")
print(f"moving step : {moving_step:.1f} m")
print(f"start offset : {start_offset:.1f} m")
print(f"generated LCs : {len(generated)} (LC{generated[0]} ... LC{generated[-1]})")
print(f"wheel load : {wheel_load:.0f} kN (load model, load factor 1.0)")
# --- Calculate every position and read the influence line -------------------
rfem_app.calculate_all(skip_warnings=True)
print("=" * 74)
print("Influence line - maximum deck deflection per wheel position")
print("=" * 74)
print(f"{'LC':>4} {'wheel x [m]':>12} {'max u_z [mm]':>13} {'at x [m]':>9}")
influence = []
for index, load_case_no in enumerate(generated):
position = start_offset + index * moving_step
deflection, at_x = max_deck_deflection(rfem_app, load_case_no)
influence.append((position, deflection))
print(f"{load_case_no:>4} {position:>12.1f} {deflection:>13.2f} {at_x:>9.2f}")
worst_position, worst_deflection = max(influence, key=lambda item: item[1])
print("-" * 74)
print(f"governing wheel position : x = {worst_position:.1f} m "
f"(span/2 = {SPAN / 2:.1f} m)")
print(f"maximum deflection : {worst_deflection:.2f} mm")
// Moving Load Wizard via the RFEM gRPC API.
//
// Builds a single-span reinforced-concrete slab bridge deck, drives one wheel load
// along the traffic lane with the Moving Load Wizard, calculates every generated
// position and prints the resulting influence line of the deck deflection.
//
// The model
// ---------
// 12.0 m span, 8.0 m wide, 0.60 m C30/37 slab, simply supported at both
// abutments. The traffic lane is the deck centreline (y = 4.0 m), modelled as
// a LineSet - that line set is the wizard's trajectory.
//
// A LoadModel (the "vehicle") carries one concentrated wheel force. The lane
// keeps the wheel 1.0 m clear of each abutment and steps it along in 0.5 m
// increments, so the wizard generates one load case per wheel position
// (21 of them, x = 1.0 ... 11.0 m).
//
// The wheel force is 150 kN - the EN 1991-2 Load Model 1 tandem axle Q1k =
// 300 kN shared by two wheels - applied here as a single wheel print for
// simplicity.
//
// Wizard tree tables
// ------------------
// The load components of a LoadModel and the lane definitions of a MovingLoad
// are tree tables. Their rows are addressed by Key (independent of the RFEM
// language). A row is written through its typed field (e.g. MovingStep) and
// read back through Value, a string holding the number in SI units.
using System.Globalization;
using Google.Protobuf;
using Common = Dlubal.Api.Common;
using Rfem = Dlubal.Api.Rfem;
using LoadModel = Dlubal.Api.Rfem.LoadWizards.LoadModel;
using MovingLoad = Dlubal.Api.Rfem.LoadWizards.MovingLoad;
// -------------------------------------------------------------------------
// Editable parameters (SI units)
// -------------------------------------------------------------------------
const double SPAN = 12.0; // m, abutment to abutment
const double WIDTH = 8.0; // m, deck width
const double THICKNESS = 0.60; // m
const double WHEEL_LOAD = 150.0; // kN, EN 1991-2 LM1 tandem axle Q1k = 300 kN over two wheels
const double MOVING_STEP = 0.5; // m, distance between two generated wheel positions
const double END_CLEARANCE = 1.0; // m, wheel kept this far from each abutment
static List<IMessage> DefineStructure()
{
// Simply supported slab deck plus the centreline lane used as trajectory.
const double halfWidth = WIDTH / 2.0;
return new List<IMessage>
{
new Rfem.StructureCore.Material { No = 1, Name = "C30/37" },
new Rfem.StructureCore.Thickness { No = 1, Material = 1, UniformThickness = THICKNESS },
// Deck corners, then the two centreline nodes carrying the lane.
new Rfem.StructureCore.Node { No = 1, Coordinate1 = 0.0, Coordinate2 = 0.0 },
new Rfem.StructureCore.Node { No = 2, Coordinate1 = SPAN, Coordinate2 = 0.0 },
new Rfem.StructureCore.Node { No = 3, Coordinate1 = SPAN, Coordinate2 = WIDTH },
new Rfem.StructureCore.Node { No = 4, Coordinate1 = 0.0, Coordinate2 = WIDTH },
new Rfem.StructureCore.Node { No = 5, Coordinate1 = 0.0, Coordinate2 = halfWidth },
new Rfem.StructureCore.Node { No = 6, Coordinate1 = SPAN, Coordinate2 = halfWidth },
new Rfem.StructureCore.Line { No = 1, DefinitionNodes = { 1, 2 } }, // edge beam, y = 0
new Rfem.StructureCore.Line { No = 2, DefinitionNodes = { 2, 3 } }, // abutment, x = SPAN
new Rfem.StructureCore.Line { No = 3, DefinitionNodes = { 3, 4 } }, // edge beam, y = WIDTH
new Rfem.StructureCore.Line { No = 4, DefinitionNodes = { 4, 1 } }, // abutment, x = 0
new Rfem.StructureCore.Line { No = 5, DefinitionNodes = { 5, 6 } }, // traffic lane
new Rfem.StructureCore.Surface { No = 1, BoundaryLines = { 1, 2, 3, 4 }, Thickness = 1 },
// The trajectory must be a LINE SET, not a plain line.
new Rfem.StructureCore.LineSet { No = 1, Lines = { 5 } },
// Simply supported: one abutment held in all three directions, the other
// free to slide along the span so the deck is not restrained axially.
new Rfem.TypesForLines.LineSupport
{
No = 1,
UserDefinedNameEnabled = true,
Name = "Fixed bearing",
Lines = { 4 },
Spring = new Common.Vector3d { X = double.PositiveInfinity, Y = double.PositiveInfinity, Z = double.PositiveInfinity },
RotationalRestraint = new Common.Vector3d { X = 0.0, Y = 0.0, Z = 0.0 },
},
new Rfem.TypesForLines.LineSupport
{
No = 2,
UserDefinedNameEnabled = true,
Name = "Sliding bearing",
Lines = { 2 },
Spring = new Common.Vector3d { X = 0.0, Y = double.PositiveInfinity, Z = double.PositiveInfinity },
RotationalRestraint = new Common.Vector3d { X = 0.0, Y = 0.0, Z = 0.0 },
},
new Rfem.Loading.StaticAnalysisSettings { No = 1 },
};
}
static List<TreeNode> AdaptLaneRows(IEnumerable<MovingLoad.Types.LaneDefinitionsTreeTableRow> rows)
{
// The two wizard trees have the same row shape but are distinct generated
// types, so each is projected onto a common TreeNode before walking it.
return rows.Select(row => new TreeNode(row.Key, row.Value, AdaptLaneRows(row.Rows))).ToList();
}
static List<TreeNode> AdaptComponentRows(IEnumerable<LoadModel.Types.LoadComponentsTreeTableRow> rows)
{
return rows.Select(row => new TreeNode(row.Key, row.Value, AdaptComponentRows(row.Rows))).ToList();
}
static void DumpTree(IReadOnlyList<TreeNode> rows, int depth = 0)
{
// Print a wizard tree table: row keys and their values (SI units).
foreach (var row in rows)
{
var key = new string(' ', depth * 2) + row.Key;
Console.WriteLine($"{key,-36} {row.Value}".TrimEnd());
DumpTree(row.Rows, depth + 1);
}
}
static double TreeValue(IReadOnlyList<TreeNode> rows, params string[] path)
{
// Value of the tree row addressed by its chain of keys, in SI units.
TreeNode? node = null;
foreach (var key in path)
{
node = rows.FirstOrDefault(row => row.Key == key)
?? throw new InvalidOperationException($"Tree row '{key}' not found.");
rows = node.Rows;
}
return double.Parse(node!.Value, CultureInfo.InvariantCulture);
}
static (double Uz, double X) MaxDeckDeflection(ApplicationRfem rfemApp, int loadCaseNo)
{
// Largest downward deck deflection for one load case: (u_z [mm], x [m]).
//
// Global Z points down, so the maximum u_z is the deepest sag. On a simply
// supported span it stays near midspan wherever the wheel stands; only its
// size changes.
Common.Table table = rfemApp.get_results(
resultsType: Rfem.Results.ResultsType.StaticAnalysisSurfacesGlobalDeformationsMeshNodes,
filters: new List<Rfem.Results.ResultsFilter>
{
new Rfem.Results.ResultsFilter { ColumnId = "loading", FilterExpression = $"LC{loadCaseNo}" },
}
);
var uzColumn = table.Data.Columns["u_z"];
var xColumn = table.Data.Columns["location_x"];
double worstUz = 0.0;
double worstX = 0.0;
for (long i = 0; i < uzColumn.Length; i++)
{
if (!double.TryParse(uzColumn[i]?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var uz))
{
continue;
}
if (uz <= worstUz)
{
continue;
}
worstUz = uz;
double.TryParse(xColumn[i]?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out worstX);
}
return (worstUz * 1000.0, worstX);
}
// -------------------------------------------------------
// MAIN SCRIPT
// -------------------------------------------------------
ApplicationRfem? rfemApp = null;
try
{
rfemApp = new ApplicationRfem();
// Create a clean model
rfemApp.close_all_models(saveChanges: false);
rfemApp.create_model(name: "moving_load_wizard");
rfemApp.delete_all_objects();
// The wizard lives in the Load Wizards add-on. The combination wizard stays
// off, so the generated load cases keep the wizard's default action
// category - assigning another one requires the combination wizard.
var baseData = rfemApp.get_base_data();
baseData.Addons.LoadWizardsActive = true;
baseData.Addons.CombinationWizardAndClassificationActive = false;
rfemApp.set_base_data(baseData: baseData);
rfemApp.create_object_list(DefineStructure());
// --- The vehicle -----------------------------------------------------------
// Surfaces because the load travels over the deck slab. RFEM gives the load
// model one component, a concentrated force acting in global Z; only its
// magnitude (SI, in N) is set here, every other row keeps its default.
rfemApp.create_object(new LoadModel
{
No = 1,
Type = LoadModel.Types.Type.Surfaces,
UserDefinedNameEnabled = true,
Name = "LM1 wheel",
LoadComponents = new LoadModel.Types.LoadComponentsTreeTable
{
Rows =
{
new LoadModel.Types.LoadComponentsTreeTableRow
{
Key = "load_component",
Rows =
{
new LoadModel.Types.LoadComponentsTreeTableRow
{
Key = "parameters",
Rows =
{
new LoadModel.Types.LoadComponentsTreeTableRow
{
Key = "load_magnitude_P",
LoadMagnitudeP = WHEEL_LOAD * 1000.0,
},
},
},
},
},
},
},
Comment = "Created via gRPC API",
});
var componentRows = AdaptComponentRows(
rfemApp.get_object<LoadModel>(new LoadModel { No = 1 }).LoadComponents.Rows);
Console.WriteLine(new string('=', 74));
Console.WriteLine("Load model components (key, value [SI]):");
Console.WriteLine(new string('=', 74));
DumpTree(componentRows);
var wheelLoad = TreeValue(componentRows, "load_component", "parameters", "load_magnitude_P") / 1000.0;
// --- The moving load -------------------------------------------------------
// The lane follows line set 1. Its moving step and its start / end offsets
// are set in the lane-definitions tree; the lane length is taken from the
// line set. Every movement row needs LaneDefinition - without it the
// wizard generates nothing.
rfemApp.create_object(new MovingLoad
{
No = 1,
UserDefinedNameEnabled = true,
Name = "LM1 wheel crossing the deck",
AllSurfacesUsed = true,
TrajectorySetLine = 1,
LaneDefinitions = new MovingLoad.Types.LaneDefinitionsTreeTable
{
Rows =
{
new MovingLoad.Types.LaneDefinitionsTreeTableRow
{
Key = "lane",
Rows =
{
new MovingLoad.Types.LaneDefinitionsTreeTableRow
{
Key = "parameters",
Rows =
{
new MovingLoad.Types.LaneDefinitionsTreeTableRow { Key = "moving_step", MovingStep = MOVING_STEP },
},
},
new MovingLoad.Types.LaneDefinitionsTreeTableRow
{
Key = "offset_and_bumper",
Rows =
{
new MovingLoad.Types.LaneDefinitionsTreeTableRow { Key = "start_offset", StartOffset = END_CLEARANCE },
new MovingLoad.Types.LaneDefinitionsTreeTableRow { Key = "end_offset", EndOffset = END_CLEARANCE },
},
},
},
},
},
},
Movements = new MovingLoad.Types.MovementsTable
{
Rows =
{
new MovingLoad.Types.MovementsRow
{
No = 1,
LaneDefinition = 1,
LoadModel = 1,
LoadFactor = 1.0,
},
},
},
GenerateLoadCases = true,
Comment = "Created via gRPC API",
});
var wizard = rfemApp.get_object<MovingLoad>(new MovingLoad { No = 1 });
var laneRows = AdaptLaneRows(wizard.LaneDefinitions.Rows);
Console.WriteLine(new string('=', 74));
Console.WriteLine("Lane definitions (key, value [SI]):");
Console.WriteLine(new string('=', 74));
DumpTree(laneRows);
var laneLength = TreeValue(laneRows, "lane", "parameters", "length");
var movingStep = TreeValue(laneRows, "lane", "parameters", "moving_step");
var startOffset = TreeValue(laneRows, "lane", "offset_and_bumper", "start_offset");
var generated = wizard.LoadCases.Rows.Select(row => row.LoadCase).ToList();
Console.WriteLine(new string('-', 74));
Console.WriteLine($"lane length : {laneLength:F1} m (= length of line set 1)");
Console.WriteLine($"moving step : {movingStep:F1} m");
Console.WriteLine($"start offset : {startOffset:F1} m");
Console.WriteLine($"generated LCs : {generated.Count} (LC{generated.First()} ... LC{generated.Last()})");
Console.WriteLine($"wheel load : {wheelLoad:F0} kN (load model, load factor 1.0)");
// --- Calculate every position and read the influence line -------------------
rfemApp.calculate_all(skipWarnings: true);
Console.WriteLine(new string('=', 74));
Console.WriteLine("Influence line - maximum deck deflection per wheel position");
Console.WriteLine(new string('=', 74));
Console.WriteLine($"{"LC",4} {"wheel x [m]",12} {"max u_z [mm]",13} {"at x [m]",9}");
var worstPosition = 0.0;
var worstDeflection = double.NegativeInfinity;
for (int index = 0; index < generated.Count; index++)
{
var position = startOffset + index * movingStep;
var (deflection, atX) = MaxDeckDeflection(rfemApp, generated[index]);
if (deflection > worstDeflection)
{
worstDeflection = deflection;
worstPosition = position;
}
Console.WriteLine($"{generated[index],4} {position,12:F1} {deflection,13:F2} {atX,9:F2}");
}
Console.WriteLine(new string('-', 74));
Console.WriteLine($"governing wheel position : x = {worstPosition:F1} m (span/2 = {SPAN / 2.0:F1} m)");
Console.WriteLine($"maximum deflection : {worstDeflection:F2} mm");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
if (rfemApp != null) rfemApp.close_connection();
}
// Shared shape of both wizard tree tables (key / value / rows).
record TreeNode(string Key, string Value, IReadOnlyList<TreeNode> Rows);