Wind Load Wizard#
|
Generating duopitch building envelope wind loads with the Wind Load Wizard, letting RFEM create the target load cases:
Keywords:
wind load wizard duopitch roof building envelope GeoZone Tool generate_missing_load_cases EN 1991-1-4 free polygon load |
"""Duopitch building envelope with a Wind Load Wizard that creates its own load cases.
The target load cases are NOT prepared by the script. The wizard is created with
`generate_missing_load_cases = True` (the GUI "Generate Load Cases" button), and
RFEM works out how many arrangements the standard produces, creates one load case per
arrangement - named after its wind direction and case, e.g.
"Wind Perpendicular to Wall 2 (B-C-D) | 270 deg | Case w+" - and generates the
loads into them.
The opposite strategy - preparing the load cases first and handing their numbers
to the wizard through `generate_into_load_cases` - works here too, and is what
load_wizard_snow.py does. Use it when the load-case numbering has to match an
existing model. Both wizards accept both strategies; the two examples show one
each rather than repeating the same script twice.
The geometry follows the published worked example Access Steel SX016a-EN-EU,
"Example: Determination of loads on a building envelope", M. Oppe / C. Mueller,
June 2005 (EN 1991-1-3, EN 1991-1-4):
Total length, b = 72.000 m (ridge direction, global Y)
Bay width, d = 30.000 m (span across the slopes, global X)
Eaves height, h' = 5.988 m
Ridge height, h = 7.300 m
Roof slope, alpha = 5.0 deg
Auto-generated versus prepared load cases
-----------------------------------------
Letting the wizard create them is the shorter route: nothing has to be known in
advance about how many arrangements EN 1991 produces for a duopitch building
(twelve here - four per ridge-normal direction per Table 7.4a Note 1, two per
ridge-parallel direction), and the load cases come out labelled by direction
instead of by a hand-written comment. Preparing twelve load cases by hand, in the
order the wizard expects, is the cost the other strategy carries.
Two properties of the auto-generation drive the order of this script:
1. It numbers the new load cases from 1 upwards and does NOT skip load cases
that already exist - it generates INTO them. With a self-weight LC1 in the
model beforehand, the first wind arrangement lands on top of the
self-weight case. So the wizard is created FIRST, while the model has no
load cases at all, and the self-weight case is added afterwards with the
next free number (13 here, read back from the wizard).
2. Static analysis settings are needed before the wizard is created - the
generated load cases are given the existing settings object, and a load
case without analysis settings is not a valid generation target.
`generate_missing_load_cases` is a one-shot trigger, not stored state: it reads
back as False once the load cases have been made. Re-sending it in update_object
generates a further set of load cases, it does not refresh the existing ones -
changing a standard parameter is enough for that (see the v_b,0 write below).
Base Data assigns EN 1991 with the base CEN annex, which works from the EN
recommended values. The basic wind velocity v_b,0 for GEOZONE_ADDRESS is fetched
from the GeoZone Tool and written into the wizard's standard-parameters tree (the
GUI "Parameters" tab), mirroring load_wizard_snow.py for the snow load s_k. Under
the CEN annex v_b,0 is a direct input - there are no wind zones - so this write is
what sets the velocity pressure q_b = 0.5 * rho * v_b,0^2, and RFEM regenerates
the loads in the already-created load cases.
Roof corner nodes must be given in RFEM's duopitch order - a perimeter walk of
the folded roof (the wizard's A-F diagram), as in the Snow Load Wizard - plus
the four ground corners under the eaves in the same order:
A front-left eaves -> B rear-left eaves -> C rear ridge ->
D rear-right eaves -> E front-right eaves -> F front ridge
The combination wizard must be OFF, or plain load cases are not accepted as
generation targets and the wizard is created without any loads appearing. A
National Annex that works from wind zones needs a real load_zone as well, because
a wizard left on LOAD_ZONE_NOT_DEFINED cannot be passed back into update_object;
the CEN annex has no wind zones, so none is set here.
The reference sheet assumes terrain category II, giving q_p = 0.911 kN/m2 at
z = 7.30 m. This wizard defaults to terrain category 0 (k_r = 0.1560,
z_0 = 0.003 m). The tree exposes k_r behind `manual_definition_of_terrain_factor`
but not z_0, so a consistent terrain category cannot be set from the API and the
generated velocity pressure differs from the sheet.
Units are SI: metres for geometry, Pascals for pressures. RFEM uses a Z-down
convention, so structural heights map to negative Z coordinates.
"""
from dlubal.api import rfem, common, geo_zone_tool
from math import radians, tan
# -------------------------------------------------------------------------
# Editable parameters (SI units) - the SX016a building
# -------------------------------------------------------------------------
BUILDING_LENGTH = 72.0 # m, b, ridge direction (global Y)
BUILDING_WIDTH = 30.0 # m, d, span across the slopes (global X)
EAVES_HEIGHT = 5.988 # m, h', ground to eaves
ROOF_PITCH = 5.0 # deg, alpha
MATERIAL = "S235 | EN 1993-1-1:2005-05" # steel, from the RFEM material library
SHELL_THICKNESS = 0.2 # m, envelope surfaces
# GeoZone Tool: the basic wind velocity v_b,0 for this site is fetched from the
# GeoZone service and written into the wizard's standard-parameters tree (same
# pattern as load_wizard_snow.py, which transfers s_k for snow).
GEOZONE_ADDRESS = "Aachen, Germany"
GEOZONE_COUNTRY_CODE = "DE"
WIND_STANDARD_NAME = "EN 1991-1-4" # preferred wind standard from the GeoZone list
def ridge_height() -> float:
"""Ridge height h from the eaves height and the roof pitch (SX016a sheet 1)."""
return EAVES_HEIGHT + (BUILDING_WIDTH / 2.0) * tan(radians(ROOF_PITCH))
def define_structure() -> list:
"""Define and return the structural objects of the duopitch envelope.
Nodes 1-4 are the eaves corners, 5-6 the ridge ends and 7-10 the ground
corners. Six surfaces close the envelope: two roof slopes, two side walls
and two gable walls. SX016a is an envelope example, so no frames are needed -
the wizard loads the surfaces directly.
Static analysis settings are part of this list because they have to exist
before the wizard runs: the load cases it creates are given these settings,
and a load case without them is not a valid generation target. No load case
is defined here - the wizard makes them all (see the module docstring).
"""
length, width, ridge = BUILDING_LENGTH, BUILDING_WIDTH, ridge_height()
objects = [
rfem.structure_core.Material(no=1, name=MATERIAL),
rfem.structure_core.Thickness(
no=1,
material=1,
uniform_thickness=SHELL_THICKNESS,
type=rfem.structure_core.Thickness.TYPE_UNIFORM,
),
# --- Nodes: eaves (1-4), ridge (5-6), ground (7-10). Z is negative for
# structural height. ---
rfem.structure_core.Node(no=1, coordinate_1=0.0, coordinate_2=0.0, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=2, coordinate_1=0.0, coordinate_2=length, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=3, coordinate_1=width, coordinate_2=0.0, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=4, coordinate_1=width, coordinate_2=length, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=5, coordinate_1=width / 2.0, coordinate_2=0.0, coordinate_3=-ridge),
rfem.structure_core.Node(no=6, coordinate_1=width / 2.0, coordinate_2=length, coordinate_3=-ridge),
rfem.structure_core.Node(no=7, coordinate_1=0.0, coordinate_2=0.0, coordinate_3=0.0),
rfem.structure_core.Node(no=8, coordinate_1=0.0, coordinate_2=length, coordinate_3=0.0),
rfem.structure_core.Node(no=9, coordinate_1=width, coordinate_2=length, coordinate_3=0.0),
rfem.structure_core.Node(no=10, coordinate_1=width, coordinate_2=0.0, coordinate_3=0.0),
]
# --- Lines: boundaries of the six envelope surfaces ---
boundaries = {
1: [1, 2], 2: [2, 6], 3: [6, 5], 4: [5, 1], # roof, left slope
5: [3, 4], 6: [4, 6], 7: [6, 5], 8: [5, 3], # roof, right slope
9: [7, 8], 10: [8, 2], 11: [2, 1], 12: [1, 7], # side wall, X = 0
13: [10, 9], 14: [9, 4], 15: [4, 3], 16: [3, 10], # side wall, X = d
17: [7, 10], 18: [10, 3], 19: [3, 5], 20: [5, 1], 21: [1, 7], # gable, Y = 0
22: [8, 9], 23: [9, 4], 24: [4, 6], 25: [6, 2], 26: [2, 8], # gable, Y = b
}
for no, nodes in boundaries.items():
objects.append(rfem.structure_core.Line(no=no, definition_nodes=nodes))
# --- Surfaces: the planes the wind pressures are generated on ---
objects += [
rfem.structure_core.Surface(no=1, geometry=1, boundary_lines=[1, 2, 3, 4], thickness=1),
rfem.structure_core.Surface(no=2, geometry=1, boundary_lines=[5, 6, 7, 8], thickness=1),
rfem.structure_core.Surface(no=3, geometry=1, boundary_lines=[9, 10, 11, 12], thickness=1),
rfem.structure_core.Surface(no=4, geometry=1, boundary_lines=[13, 14, 15, 16], thickness=1),
rfem.structure_core.Surface(no=5, geometry=1, boundary_lines=[17, 18, 19, 20, 21], thickness=1),
rfem.structure_core.Surface(no=6, geometry=1, boundary_lines=[22, 23, 24, 25, 26], thickness=1),
]
# --- Analysis settings for the load cases the wizard is about to create ---
objects.append(
rfem.loading.StaticAnalysisSettings(
no=1,
analysis_type=rfem.loading.StaticAnalysisSettings.ANALYSIS_TYPE_GEOMETRICALLY_LINEAR,
)
)
return objects
def define_wind_load_wizard() -> list:
"""Return the walls + duopitch-roof Wind Load Wizard for the envelope.
`generate_missing_load_cases = True` is what makes RFEM create the target load
cases itself, so no `generate_into_load_cases` table is passed. The flag acts once,
when the wizard is created, and reads back as False afterwards.
Roof corner nodes follow RFEM's duopitch order - a perimeter walk of the
folded roof (A B C D E F) - and the base corner nodes are the four ground
corners in the same walk order. No load zone is passed: the CEN annex has no
wind zones and takes v_b,0 directly, which is written afterwards through the
standard-parameters tree (see main).
"""
return [
rfem.load_wizards.WindLoad(
no=1,
user_defined_name_enabled=True,
name="Wind - Walls + Duopitch Roof",
type=rfem.load_wizards.WindLoad.TYPE_WALLS_ROOF_DUOPITCH,
structure_definition=rfem.load_wizards.WindLoad.STRUCTURE_DEFINITION_BUILDINGS,
roof_corner_nodes=[
1, # A front-left eaves
2, # B rear-left eaves
6, # C rear ridge
4, # D rear-right eaves
3, # E front-right eaves
5, # F front ridge
],
base_corner_nodes=[7, 8, 9, 10],
# USER_DEFINED is required. The default PARAMETERS_FROM_MAP reads the terrain
# category from a map location, so without an address that parameter stays
# "not defined", the wizard is invalid for generation and produces no loads at
# all - without any error.
definition_type=rfem.load_wizards.WindLoad.DEFINITION_TYPE_USER_DEFINED,
generate_missing_load_cases=True,
)
]
def select_wind_zone(standards):
"""Pick the wind load-zone standard/annex/layer from a GeoZone standards list."""
wind_zones = next(
(group.load_zones for group in standards.type_groups if group.name.lower() == "wind"),
None,
)
if not wind_zones:
raise ValueError("No wind load-zone group found for the selected country.")
zone = next(
(z for z in wind_zones if z.annex.actual and z.standard.name == WIND_STANDARD_NAME),
next((z for z in wind_zones if z.annex.actual), wind_zones[0]),
)
return zone.standard.name, zone.annex.name, zone.annex.layers[0]
def fetch_wind_load_from_geozone(rfem_app):
"""Query the GeoZone Tool for the site's wind data.
Returns (geozone_result, v_b_0, zone_value). The standard-parameters tree
stores v_b,0 in SI m/s, which is what GeoZone reports, so no scaling is
needed; zone_value (e.g. '2') is the wind zone the value was read from and is
reported for information.
"""
gzt = geo_zone_tool.GeoZoneTool(token=rfem_app.api_key.value)
standards = gzt.get_load_zone_standards(
country_code=GEOZONE_COUNTRY_CODE, language=geo_zone_tool.Language.EN
)
standard, annex, layer = select_wind_zone(standards)
result = gzt.get_load_zone_characteristics(
address=GEOZONE_ADDRESS,
load_zone_type=geo_zone_tool.LoadZoneType.WIND,
standard=standard,
annex=annex,
layer_id=layer.id,
language=geo_zone_tool.Language.EN,
)
vb0_var = next(
(v for ch in result.characteristics
for v in ch.zone_characteristics.characteristics if v.name == "v_b0"),
None,
)
if vb0_var is None or not vb0_var.calculated_value:
raise ValueError("GeoZone response does not contain 'v_b0'.")
zone_value = result.characteristics[0].zone_characteristics.zone.value
return result, float(vb0_var.calculated_value), zone_value
# Connect to the RFEM application
with rfem.Application() as rfem_app:
# Create a clean model
rfem_app.close_all_models(save_changes=False)
rfem_app.create_model(name="duopitch_wind_loads_generated_lcs")
rfem_app.delete_all_objects()
# GeoZone Tool: fetch v_b,0 and the resolved site location for GEOZONE_ADDRESS
# (token comes from the RFEM API key).
geozone_result, v_b_0, zone_value = fetch_wind_load_from_geozone(rfem_app)
altitude = float(geozone_result.geo_location.altitude)
print(f"\nGeoZone wind query [{GEOZONE_ADDRESS}]:")
print(f" wind zone = {zone_value}")
print(f" zone-derived v_b0 = {v_b_0:.1f} m/s"
f" (-> q_b = {0.5 * 1.25 * v_b_0 ** 2:.1f} Pa)")
print(f" site altitude = {altitude:.1f} m")
# Base Data: activate the load-wizards add-on, assign EN 1991 with the base CEN
# annex, and transfer the resolved site location. Keep the combination wizard
# OFF so the generated load cases are accepted as valid generation targets.
base_data = rfem_app.get_base_data()
base_data.addons.load_wizards_active = True
base_data.addons.combination_wizard_and_classification_active = False
base_data.standards.load_wizard_standard_group = rfem.BaseData.Standards.LOAD_WIZARD_EN_1991_STANDARD_GROUP
base_data.standards.load_wizard_standard = rfem.BaseData.Standards.LOAD_WIZARD_NATIONAL_ANNEX_AND_EDITION_EN_1991_CEN_2015_09_STANDARD
base_data.location.altitude = altitude
base_data.location.latitude = radians(float(geozone_result.geo_location.latitude))
base_data.location.longitude = radians(float(geozone_result.geo_location.longitude))
base_data.location.town_city = geozone_result.geo_location.city
rfem_app.set_base_data(base_data=base_data)
# Envelope and analysis settings, then the wizard - which creates one load case
# per wind arrangement and generates the loads into them.
rfem_app.create_object_list(define_structure())
rfem_app.create_object_list(define_wind_load_wizard())
# Write the GeoZone v_b,0 into the wizard's standard-parameters tree. Under the
# CEN annex v_b,0 is a plain input row, so a single update is enough and RFEM
# recomputes q_b and regenerates the loads in the load cases it already made.
# National Annexes that derive v_b,0 from a wind zone gate the row behind
# `manual_definition_of_wind_velocity`, and there the flag and the value need
# two separate updates.
wind = rfem_app.get_object(rfem.load_wizards.WindLoad(no=1))
params = wind.standard_parameters
common.set_values_by_key(tree=params, key="v_b_0", values=[v_b_0])
rfem_app.update_object(rfem.load_wizards.WindLoad(no=1, standard_parameters=params))
# Add the self-weight case behind the generated ones. Its number is the first
# free one after the arrangements the wizard made - read back from the wizard
# rather than assumed, because how many arrangements the standard produces
# depends on the wizard type and the National Annex.
generated = rfem_app.get_object(
rfem.load_wizards.WindLoad(no=1)
).generate_into_load_cases.rows
self_weight_lc = max(row.load_case for row in generated) + 1
rfem_app.create_object(
rfem.loading.LoadCase(
no=self_weight_lc,
name="Self Weight",
static_analysis_settings=1,
action_category=rfem.loading.LoadCase.ActionCategory.ACTION_CATEGORY_PERMANENT_G,
self_weight_active=True,
)
)
print(f"\nWizard generated {len(generated)} wind load cases, "
f"self weight added as LC{self_weight_lc}:")
for lc in rfem_app.get_object_list([rfem.loading.LoadCase()]):
print(f" LC{lc.no} - {lc.name}")