Snow Load Wizard#
|
Generating duopitch roof snow loads from GeoZone site data with the Snow Load Wizard:
Keywords:
snow load wizard duopitch roof GeoZone Tool |
"""Parametric duopitch (gable) roof building with a Snow Load Wizard.
Builds a multi-frame timber skeleton with a symmetric duopitch roof, then adds a
Snow Load Wizard on the two roof slopes (RFEM "Gable/Duopitch"). Each portal
frame has two columns and two rafters (eaves to ridge); longitudinal beams tie
the frames together. The model is 1D members only - the snow wizard identifies
the two roof planes from the corner nodes and loads the rafters directly.
The default parameters reproduce the reference structure:
Building length, L = 12.0 m (ridge direction)
Building width, B = 12.0 m (span across the slopes)
Eaves height = 5.0 m
Ridge height, H = 7.5 m (ground to top of roof)
Roof pitch, theta = atan((7.5 - 5.0) / (12.0 / 2)) = 22.62 deg
Snow Load Wizard
----------------
Base Data activates the load-wizards add-on and assigns EN 1991 (DIN). The
characteristic snow load s_k and the site altitude are fetched from the GeoZone
Tool for GEOZONE_ADDRESS and written into the wizard's standard-parameters tree
(the GUI "Parameters" tab), mirroring response_spectrum_from_geozone.py for the
seismic a_gR value.
The duopitch roof corner nodes must be given in the order RFEM expects - a walk
around the perimeter of the folded roof (see the wizard's A-F diagram):
A front-left eaves -> B rear-left eaves -> C rear ridge ->
D rear-right eaves -> E front-right eaves -> F front ridge
EN 1991 produces three duopitch snow arrangements (Case i balanced, Cases ii/iii
asymmetric), so the wizard must generate into three DISTINCT load cases - one per
arrangement. Reusing a load case is rejected ("Load cases for generated loads
must differ.").
NOTE (bindings):
Requires dlubal.api bindings that include load_wizards.SnowLoad. With the
correct node order the TYPE_DUOPITCH wizard registers both roof planes
(loaded_planes: 2 planes at 22.62 deg).
Units are SI: metres for geometry, Newtons for forces. 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 inf, radians
# -------------------------------------------------------------------------
# Editable parameters (SI units)
# -------------------------------------------------------------------------
BUILDING_LENGTH = 12.0 # m, ridge direction (global Y)
BUILDING_WIDTH = 12.0 # m, span across the slopes (global X)
EAVES_HEIGHT = 5.0 # m, ground to eaves
RIDGE_HEIGHT = 7.5 # m, ground to ridge (H)
NUMBER_OF_FRAMES = 4 # portal frames along the building length
MATERIAL = "C24" # timber, from the RFEM material library
CROSS_SECTION_COLUMN = "R_M1 120/240" # 120 x 240 mm
CROSS_SECTION_RAFTER = "R_M1 100/200" # 100 x 200 mm
# GeoZone Tool: the characteristic snow load s_k for this site is fetched from the
# GeoZone service and written into the wizard's standard-parameters tree (same
# pattern as response_spectrum_from_geozone.py, which transfers a_gR for seismic).
GEOZONE_ADDRESS = "München, Germany"
GEOZONE_COUNTRY_CODE = "DE"
SNOW_STANDARD_NAME = "EN 1991-1-3" # preferred snow standard from the GeoZone list
def define_structure() -> list:
"""Define and return the structural objects of the duopitch building."""
half_width = BUILDING_WIDTH / 2.0
frame_spacing = BUILDING_LENGTH / (NUMBER_OF_FRAMES - 1)
nodes_per_frame = 5 # left/right base, left/right eaves, ridge
lines_per_frame = 4 # 2 columns + 2 rafters
lines_per_bay = 3 # left eaves beam, right eaves beam, ridge beam
first_bay_line = NUMBER_OF_FRAMES * lines_per_frame
objects = [
rfem.structure_core.Material(no=1, name=MATERIAL),
rfem.structure_core.CrossSection(no=1, name=CROSS_SECTION_COLUMN, material=1),
rfem.structure_core.CrossSection(no=2, name=CROSS_SECTION_RAFTER, material=1),
]
# --- Nodes: 5 per frame (Z is negative for structural height) ---
for f in range(NUMBER_OF_FRAMES):
base = f * nodes_per_frame
y = f * frame_spacing
objects += [
rfem.structure_core.Node(no=base + 1, coordinate_1=-half_width, coordinate_2=y, coordinate_3=0.0),
rfem.structure_core.Node(no=base + 2, coordinate_1=half_width, coordinate_2=y, coordinate_3=0.0),
rfem.structure_core.Node(no=base + 3, coordinate_1=-half_width, coordinate_2=y, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=base + 4, coordinate_1=half_width, coordinate_2=y, coordinate_3=-EAVES_HEIGHT),
rfem.structure_core.Node(no=base + 5, coordinate_1=0.0, coordinate_2=y, coordinate_3=-RIDGE_HEIGHT),
]
# --- Lines: 2 columns + 2 rafters per frame ---
for f in range(NUMBER_OF_FRAMES):
node = f * nodes_per_frame
line = f * lines_per_frame
objects += [
rfem.structure_core.Line(no=line + 1, definition_nodes=[node + 1, node + 3]), # left column
rfem.structure_core.Line(no=line + 2, definition_nodes=[node + 2, node + 4]), # right column
rfem.structure_core.Line(no=line + 3, definition_nodes=[node + 3, node + 5]), # left rafter
rfem.structure_core.Line(no=line + 4, definition_nodes=[node + 4, node + 5]), # right rafter
]
# --- Lines: longitudinal beams tying consecutive frames (per bay) ---
for bay in range(NUMBER_OF_FRAMES - 1):
node = bay * nodes_per_frame
line = first_bay_line + bay * lines_per_bay
objects += [
rfem.structure_core.Line(no=line + 1, definition_nodes=[node + 3, node + nodes_per_frame + 3]), # left eaves
rfem.structure_core.Line(no=line + 2, definition_nodes=[node + 4, node + nodes_per_frame + 4]), # right eaves
rfem.structure_core.Line(no=line + 3, definition_nodes=[node + 5, node + nodes_per_frame + 5]), # ridge
]
# --- Members: columns on CS 1, rafters and longitudinal beams on CS 2 ---
for f in range(NUMBER_OF_FRAMES):
line = f * lines_per_frame
objects += [
rfem.structure_core.Member(no=line + 1, line=line + 1, cross_section_start=1), # left column
rfem.structure_core.Member(no=line + 2, line=line + 2, cross_section_start=1), # right column
rfem.structure_core.Member(no=line + 3, line=line + 3, cross_section_start=2), # left rafter
rfem.structure_core.Member(no=line + 4, line=line + 4, cross_section_start=2), # right rafter
]
for bay in range(NUMBER_OF_FRAMES - 1):
line = first_bay_line + bay * lines_per_bay
for k in range(1, lines_per_bay + 1):
objects.append(
rfem.structure_core.Member(no=line + k, line=line + k, cross_section_start=2)
)
# --- Nodal supports: fixed column bases ---
for f in range(NUMBER_OF_FRAMES):
base = f * nodes_per_frame
objects.append(
rfem.types_for_nodes.NodalSupport(
no=f + 1,
nodes=[base + 1, base + 2],
spring=common.Vector3d(x=inf, y=inf, z=inf),
rotational_restraint=common.Vector3d(x=inf, y=inf, z=inf),
)
)
return objects
def define_loading(snow_load_cases=(2, 3, 4)) -> list:
"""Define and return the loading objects (self-weight + snow arrangement cases).
EN 1991 generates three duopitch snow arrangements (Case i balanced, Cases
ii/iii asymmetric), each into its OWN load case - reusing one is rejected
("Load cases for generated loads must differ."). The wizard must target the
same load cases (see define_snow_load_wizard).
"""
objects = [
rfem.loading.StaticAnalysisSettings(
no=1,
analysis_type=rfem.loading.StaticAnalysisSettings.ANALYSIS_TYPE_GEOMETRICALLY_LINEAR,
),
rfem.loading.LoadCase(
no=1,
name="LC1 - Self Weight",
static_analysis_settings=1,
action_category=rfem.loading.LoadCase.ActionCategory.ACTION_CATEGORY_PERMANENT_G,
self_weight_active=True,
),
]
# Empty target load cases the Snow Load Wizard generates its arrangements into.
for i, lc in enumerate(snow_load_cases, start=1):
objects.append(
rfem.loading.LoadCase(
no=lc,
name=f"LC{lc} - Snow (case {i})",
static_analysis_settings=1,
)
)
return objects
def define_snow_load_wizard(snow_load_cases=(2, 3, 4)) -> list:
"""Return the duopitch Snow Load Wizard for the roof.
Corner nodes are given in RFEM's duopitch order - a perimeter walk of the
folded roof (A B C D E F). The load zone, s_k and altitude are written later
through the standard-parameters tree (see main), so the wizard is created
with a placeholder load zone here.
"""
SnowLoad = rfem.load_wizards.SnowLoad
# Roof outer-corner nodes (5 nodes per frame: +3 eaves-left, +4 eaves-right,
# +5 ridge) on the front frame (f = 0) and rear frame (f = NUMBER_OF_FRAMES - 1).
# Duopitch order is a perimeter walk of the folded roof: A B C D E F.
rear = (NUMBER_OF_FRAMES - 1) * 5
roof_corner_nodes = [
3, # A front-left eaves
rear + 3, # B rear-left eaves
rear + 5, # C rear ridge
rear + 4, # D rear-right eaves
4, # E front-right eaves
5, # F front ridge
]
# One row per snow arrangement, each targeting a distinct load case.
generate_rows = [
SnowLoad.GenerateIntoLoadCasesRow(no=i, checked=True, load_case=lc)
for i, lc in enumerate(snow_load_cases, start=1)
]
return [
SnowLoad(
no=1,
user_defined_name_enabled=True,
name="Snow - Duopitch",
type=SnowLoad.TYPE_DUOPITCH,
roof_corner_nodes=roof_corner_nodes,
definition_type=SnowLoad.DEFINITION_TYPE_USER_DEFINED,
load_zone=SnowLoad.LOAD_ZONE_TYPE_1,
generate_into_load_cases=SnowLoad.GenerateIntoLoadCasesTable(rows=generate_rows),
)
]
def snow_load_zone_enum(zone_value: str) -> int:
"""Map a GeoZone snow-zone value (e.g. '1a*') to a SnowLoad.LoadZone enum.
The enum member names encode the dropdown label, so the label is recovered by
reversing the naming convention: LOAD_ZONE_TYPE_1_A_ASTERISK -> '1a*'.
"""
SnowLoad = rfem.load_wizards.SnowLoad
by_label = {}
for value in SnowLoad.LoadZone.DESCRIPTOR.values:
label = (
value.name.replace("LOAD_ZONE_TYPE_", "")
.replace("_ASTERISK", "*")
.replace("_GREATER", "")
.replace("_A", "a")
.replace("_", "")
)
by_label.setdefault(">" + label if "GREATER" in value.name else label, value.number)
if zone_value not in by_label:
raise ValueError(f"GeoZone snow zone {zone_value!r} has no SnowLoad.LoadZone match.")
return by_label[zone_value]
def select_snow_zone(standards):
"""Pick the snow load-zone standard/annex/layer from a GeoZone standards list."""
snow_zones = next(
(group.load_zones for group in standards.type_groups if group.name.lower() == "snow"),
None,
)
if not snow_zones:
raise ValueError("No snow load-zone group found for the selected country.")
zone = next(
(z for z in snow_zones if z.annex.actual and z.standard.name == SNOW_STANDARD_NAME),
next((z for z in snow_zones if z.annex.actual), snow_zones[0]),
)
return zone.standard.name, zone.annex.name, zone.annex.layers[0]
def fetch_snow_load_from_geozone(rfem_app):
"""Query the GeoZone Tool for the site's snow data.
Returns (geozone_result, s_k, s_k_unit, s_k_pa, load_zone, zone_value). The
standard-parameters tree stores s_k in SI Pa, so a kN/m2 value is scaled
x1000; the zone value (e.g. '1a*') is mapped to a SnowLoad.LoadZone enum.
"""
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_snow_zone(standards)
result = gzt.get_load_zone_characteristics(
address=GEOZONE_ADDRESS,
load_zone_type=geo_zone_tool.LoadZoneType.SNOW,
standard=standard,
annex=annex,
layer_id=layer.id,
language=geo_zone_tool.Language.EN,
)
sk_var = next(
(v for ch in result.characteristics
for v in ch.zone_characteristics.characteristics if v.name == "s_k"),
None,
)
if sk_var is None or sk_var.calculated_value is None:
raise ValueError("GeoZone response does not contain 's_k'.")
sk = float(sk_var.calculated_value)
sk_unit = (sk_var.units_html or "").replace("<sup>", "^").replace("</sup>", "")
sk_pa = sk * 1000.0 if "kN" in sk_unit else sk
zone_value = result.characteristics[0].zone_characteristics.zone.value
load_zone = snow_load_zone_enum(zone_value)
return result, sk, sk_unit, sk_pa, load_zone, 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_roof_building")
rfem_app.delete_all_objects()
# GeoZone Tool: fetch load zone, s_k and the resolved site location for
# GEOZONE_ADDRESS (token comes from the RFEM API key).
geozone_result, sk, sk_unit, sk_pa, load_zone, zone_value = fetch_snow_load_from_geozone(rfem_app)
altitude = float(geozone_result.geo_location.altitude)
zone_name = rfem.load_wizards.SnowLoad.LoadZone.DESCRIPTOR.values_by_number[load_zone].name
print(f"\nGeoZone snow query [{GEOZONE_ADDRESS}]:")
print(f" load zone = {zone_value} ({zone_name})")
print(f" zone-derived s_k = {sk:.2f} {sk_unit} (= {sk_pa:.0f} Pa)")
print(f" site altitude = {altitude:.1f} m")
# Base Data: activate the load-wizards add-on, assign EN 1991 (DIN), and
# transfer the resolved site location. Keep the combination wizard OFF so the
# plain snow 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_DIN_2019_04_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)
# Structure + load cases + Snow Load Wizard.
rfem_app.create_object_list(define_structure() + define_loading())
rfem_app.create_object_list(define_snow_load_wizard())
# Write the GeoZone values into the wizard's standard-parameters tree. s_k
# only becomes an editable row AFTER manual snow-load definition is enabled,
# so this is a two-step write:
# 1. enable manual mode -> update
# 2. re-read (s_k is now editable) -> set s_k -> update
snow = rfem_app.get_object(rfem.load_wizards.SnowLoad(no=1))
params = snow.standard_parameters
common.set_values_by_key(tree=params, key="load_zone", values=[load_zone])
common.set_values_by_key(tree=params, key="a", values=[altitude])
common.set_values_by_key(tree=params, key="manual_snow_load_definition", values=[True])
common.set_values_by_key(tree=params, key="s_k", values=[sk_pa])
rfem_app.update_object(rfem.load_wizards.SnowLoad(no=1, standard_parameters=params))
// Parametric duopitch (gable) roof building with a Snow Load Wizard.
//
// Builds a multi-frame timber skeleton with a symmetric duopitch roof, then adds a
// Snow Load Wizard on the two roof slopes (RFEM "Gable/Duopitch"). Each portal
// frame has two columns and two rafters (eaves to ridge); longitudinal beams tie
// the frames together. The model is 1D members only - the snow wizard identifies
// the two roof planes from the corner nodes and loads the rafters directly.
//
// The default parameters reproduce the reference structure:
//
// Building length, L = 12.0 m (ridge direction)
// Building width, B = 12.0 m (span across the slopes)
// Eaves height = 5.0 m
// Ridge height, H = 7.5 m (ground to top of roof)
// Roof pitch, theta = atan((7.5 - 5.0) / (12.0 / 2)) = 22.62 deg
//
// Snow Load Wizard
// ----------------
// Base Data activates the load-wizards add-on and assigns EN 1991 (DIN). The
// characteristic snow load s_k and the site altitude are fetched from the GeoZone
// Tool for GEOZONE_ADDRESS and written into the wizard's standard-parameters tree
// (the GUI "Parameters" tab), mirroring ResponseSpectrumFromGeozone.cs for the
// seismic a_gR value.
//
// The duopitch roof corner nodes must be given in the order RFEM expects - a walk
// around the perimeter of the folded roof (see the wizard's A-F diagram):
//
// A front-left eaves -> B rear-left eaves -> C rear ridge ->
// D rear-right eaves -> E front-right eaves -> F front ridge
//
// EN 1991 produces three duopitch snow arrangements (Case i balanced, Cases ii/iii
// asymmetric), so the wizard must generate into three DISTINCT load cases - one per
// arrangement. Reusing a load case is rejected ("Load cases for generated loads
// must differ.").
//
// Units are SI: metres for geometry, Newtons for forces. RFEM uses a Z-down
// convention, so structural heights map to negative Z coordinates.
using System.Globalization;
using System.Reflection;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using GeoZoneTool;
using GeoZoneTool.Models;
using Common = Dlubal.Api.Common;
using Rfem = Dlubal.Api.Rfem;
// -------------------------------------------------------------------------
// Editable parameters (SI units)
// -------------------------------------------------------------------------
const double BUILDING_LENGTH = 12.0; // m, ridge direction (global Y)
const double BUILDING_WIDTH = 12.0; // m, span across the slopes (global X)
const double EAVES_HEIGHT = 5.0; // m, ground to eaves
const double RIDGE_HEIGHT = 7.5; // m, ground to ridge (H)
const int NUMBER_OF_FRAMES = 4; // portal frames along the building length
const string MATERIAL = "C24"; // timber, from the RFEM material library
const string CROSS_SECTION_COLUMN = "R_M1 0.120/0.240"; // 120 x 240 mm
const string CROSS_SECTION_RAFTER = "R_M1 0.100/0.200"; // 100 x 200 mm
// GeoZone Tool: the characteristic snow load s_k for this site is fetched from the
// GeoZone service and written into the wizard's standard-parameters tree (same
// pattern as ResponseSpectrumFromGeozone.cs, which transfers a_gR for seismic).
const string GEOZONE_ADDRESS = "München, Germany";
const string GEOZONE_COUNTRY_CODE = "DE";
const string SNOW_STANDARD_NAME = "EN 1991-1-3"; // preferred snow standard from the GeoZone list
static List<IMessage> DefineStructure()
{
// Define and return the structural objects of the duopitch building.
const double halfWidth = BUILDING_WIDTH / 2.0;
const double frameSpacing = BUILDING_LENGTH / (NUMBER_OF_FRAMES - 1);
const int nodesPerFrame = 5; // left/right base, left/right eaves, ridge
const int linesPerFrame = 4; // 2 columns + 2 rafters
const int linesPerBay = 3; // left eaves beam, right eaves beam, ridge beam
const int firstBayLine = NUMBER_OF_FRAMES * linesPerFrame;
var objects = new List<IMessage>
{
new Rfem.StructureCore.Material { No = 1, Name = MATERIAL },
new Rfem.StructureCore.CrossSection { No = 1, Name = CROSS_SECTION_COLUMN, Material = 1 },
new Rfem.StructureCore.CrossSection { No = 2, Name = CROSS_SECTION_RAFTER, Material = 1 },
};
// --- Nodes: 5 per frame (Z is negative for structural height) ---
for (int f = 0; f < NUMBER_OF_FRAMES; f++)
{
int baseNo = f * nodesPerFrame;
double y = f * frameSpacing;
objects.Add(new Rfem.StructureCore.Node { No = baseNo + 1, Coordinate1 = -halfWidth, Coordinate2 = y, Coordinate3 = 0.0 });
objects.Add(new Rfem.StructureCore.Node { No = baseNo + 2, Coordinate1 = halfWidth, Coordinate2 = y, Coordinate3 = 0.0 });
objects.Add(new Rfem.StructureCore.Node { No = baseNo + 3, Coordinate1 = -halfWidth, Coordinate2 = y, Coordinate3 = -EAVES_HEIGHT });
objects.Add(new Rfem.StructureCore.Node { No = baseNo + 4, Coordinate1 = halfWidth, Coordinate2 = y, Coordinate3 = -EAVES_HEIGHT });
objects.Add(new Rfem.StructureCore.Node { No = baseNo + 5, Coordinate1 = 0.0, Coordinate2 = y, Coordinate3 = -RIDGE_HEIGHT });
}
// --- Lines: 2 columns + 2 rafters per frame ---
for (int f = 0; f < NUMBER_OF_FRAMES; f++)
{
int node = f * nodesPerFrame;
int line = f * linesPerFrame;
objects.Add(new Rfem.StructureCore.Line { No = line + 1, DefinitionNodes = { node + 1, node + 3 } }); // left column
objects.Add(new Rfem.StructureCore.Line { No = line + 2, DefinitionNodes = { node + 2, node + 4 } }); // right column
objects.Add(new Rfem.StructureCore.Line { No = line + 3, DefinitionNodes = { node + 3, node + 5 } }); // left rafter
objects.Add(new Rfem.StructureCore.Line { No = line + 4, DefinitionNodes = { node + 4, node + 5 } }); // right rafter
}
// --- Lines: longitudinal beams tying consecutive frames (per bay) ---
for (int bay = 0; bay < NUMBER_OF_FRAMES - 1; bay++)
{
int node = bay * nodesPerFrame;
int line = firstBayLine + bay * linesPerBay;
objects.Add(new Rfem.StructureCore.Line { No = line + 1, DefinitionNodes = { node + 3, node + nodesPerFrame + 3 } }); // left eaves
objects.Add(new Rfem.StructureCore.Line { No = line + 2, DefinitionNodes = { node + 4, node + nodesPerFrame + 4 } }); // right eaves
objects.Add(new Rfem.StructureCore.Line { No = line + 3, DefinitionNodes = { node + 5, node + nodesPerFrame + 5 } }); // ridge
}
// --- Members: columns on CS 1, rafters and longitudinal beams on CS 2 ---
for (int f = 0; f < NUMBER_OF_FRAMES; f++)
{
int line = f * linesPerFrame;
objects.Add(new Rfem.StructureCore.Member { No = line + 1, Line = line + 1, CrossSectionStart = 1 }); // left column
objects.Add(new Rfem.StructureCore.Member { No = line + 2, Line = line + 2, CrossSectionStart = 1 }); // right column
objects.Add(new Rfem.StructureCore.Member { No = line + 3, Line = line + 3, CrossSectionStart = 2 }); // left rafter
objects.Add(new Rfem.StructureCore.Member { No = line + 4, Line = line + 4, CrossSectionStart = 2 }); // right rafter
}
for (int bay = 0; bay < NUMBER_OF_FRAMES - 1; bay++)
{
int line = firstBayLine + bay * linesPerBay;
for (int k = 1; k <= linesPerBay; k++)
{
objects.Add(new Rfem.StructureCore.Member { No = line + k, Line = line + k, CrossSectionStart = 2 });
}
}
// --- Nodal supports: fixed column bases ---
for (int f = 0; f < NUMBER_OF_FRAMES; f++)
{
int baseNo = f * nodesPerFrame;
objects.Add(new Rfem.TypesForNodes.NodalSupport
{
No = f + 1,
Nodes = { baseNo + 1, baseNo + 2 },
Spring = new Common.Vector3d { X = double.PositiveInfinity, Y = double.PositiveInfinity, Z = double.PositiveInfinity },
RotationalRestraint = new Common.Vector3d { X = double.PositiveInfinity, Y = double.PositiveInfinity, Z = double.PositiveInfinity },
});
}
return objects;
}
static List<IMessage> DefineLoading(int[] snowLoadCases)
{
// Define and return the loading objects (self-weight + snow arrangement cases).
//
// EN 1991 generates three duopitch snow arrangements (Case i balanced, Cases
// ii/iii asymmetric), each into its OWN load case - reusing one is rejected
// ("Load cases for generated loads must differ."). The wizard must target the
// same load cases (see DefineSnowLoadWizard).
var objects = new List<IMessage>
{
new Rfem.Loading.StaticAnalysisSettings
{
No = 1,
AnalysisType = Rfem.Loading.StaticAnalysisSettings.Types.AnalysisType.GeometricallyLinear,
},
new Rfem.Loading.LoadCase
{
No = 1,
Name = "LC1 - Self Weight",
StaticAnalysisSettings = 1,
ActionCategory = Rfem.Loading.LoadCase.Types.ActionCategory.PermanentG,
SelfWeightActive = true,
},
};
// Empty target load cases the Snow Load Wizard generates its arrangements into.
for (int i = 0; i < snowLoadCases.Length; i++)
{
objects.Add(new Rfem.Loading.LoadCase
{
No = snowLoadCases[i],
Name = $"LC{snowLoadCases[i]} - Snow (case {i + 1})",
StaticAnalysisSettings = 1,
});
}
return objects;
}
static List<IMessage> DefineSnowLoadWizard(int[] snowLoadCases)
{
// Return the duopitch Snow Load Wizard for the roof.
//
// Corner nodes are given in RFEM's duopitch order - a perimeter walk of the
// folded roof (A B C D E F). The load zone, s_k and altitude are written later
// through the standard-parameters tree (see main script), so the wizard is
// created with a placeholder load zone here.
// Roof outer-corner nodes (5 nodes per frame: +3 eaves-left, +4 eaves-right,
// +5 ridge) on the front frame (f = 0) and rear frame (f = NUMBER_OF_FRAMES - 1).
// Duopitch order is a perimeter walk of the folded roof: A B C D E F.
int rear = (NUMBER_OF_FRAMES - 1) * 5;
var roofCornerNodes = new[]
{
3, // A front-left eaves
rear + 3, // B rear-left eaves
rear + 5, // C rear ridge
rear + 4, // D rear-right eaves
4, // E front-right eaves
5, // F front ridge
};
// One row per snow arrangement, each targeting a distinct load case.
var generateIntoLoadCases = new Rfem.LoadWizards.SnowLoad.Types.GenerateIntoLoadCasesTable();
for (int i = 0; i < snowLoadCases.Length; i++)
{
generateIntoLoadCases.Rows.Add(new Rfem.LoadWizards.SnowLoad.Types.GenerateIntoLoadCasesRow
{
No = i + 1,
Checked = true,
LoadCase = snowLoadCases[i],
});
}
var snowLoad = new Rfem.LoadWizards.SnowLoad
{
No = 1,
UserDefinedNameEnabled = true,
Name = "Snow - Duopitch",
Type = Rfem.LoadWizards.SnowLoad.Types.Type.Duopitch,
DefinitionType = Rfem.LoadWizards.SnowLoad.Types.DefinitionType.UserDefined,
LoadZone = Rfem.LoadWizards.SnowLoad.Types.LoadZone.Type1,
GenerateIntoLoadCases = generateIntoLoadCases,
};
snowLoad.RoofCornerNodes.AddRange(roofCornerNodes);
return new List<IMessage> { snowLoad };
}
static Dictionary<string, Rfem.LoadWizards.SnowLoad.Types.LoadZone> BuildSnowLoadZoneLabelMap()
{
// Map GeoZone snow-zone labels (e.g. '1a*') onto SnowLoad.LoadZone members.
//
// The protobuf member names encode the dropdown label, so the label is
// recovered by reversing the naming convention:
// LOAD_ZONE_TYPE_1_A_ASTERISK -> '1a*'. The original proto names are read from
// the [OriginalName] attributes the C# generator emits, which mirrors the
// DESCRIPTOR lookup used by the Python example.
var byLabel = new Dictionary<string, Rfem.LoadWizards.SnowLoad.Types.LoadZone>();
var enumType = typeof(Rfem.LoadWizards.SnowLoad.Types.LoadZone);
foreach (var field in enumType.GetFields(BindingFlags.Public | BindingFlags.Static))
{
var originalName = field.GetCustomAttribute<OriginalNameAttribute>()?.Name;
if (originalName == null) continue;
var label = originalName
.Replace("LOAD_ZONE_TYPE_", "")
.Replace("_ASTERISK", "*")
.Replace("_GREATER", "")
.Replace("_A", "a")
.Replace("_", "");
var key = originalName.Contains("GREATER") ? ">" + label : label;
if (!byLabel.ContainsKey(key))
{
byLabel[key] = (Rfem.LoadWizards.SnowLoad.Types.LoadZone)field.GetValue(null)!;
}
}
return byLabel;
}
static Rfem.LoadWizards.SnowLoad.Types.LoadZone SnowLoadZoneEnum(string zoneValue)
{
// Map a GeoZone snow-zone value (e.g. '1a*') to a SnowLoad.LoadZone enum.
var byLabel = BuildSnowLoadZoneLabelMap();
if (!byLabel.TryGetValue(zoneValue, out var loadZone))
{
throw new InvalidOperationException($"GeoZone snow zone '{zoneValue}' has no SnowLoad.LoadZone match.");
}
return loadZone;
}
static string SnowLoadZoneOriginalName(Rfem.LoadWizards.SnowLoad.Types.LoadZone loadZone)
{
// Recover the proto name of a LoadZone member (e.g. LOAD_ZONE_TYPE_1_A_ASTERISK).
var field = typeof(Rfem.LoadWizards.SnowLoad.Types.LoadZone).GetField(loadZone.ToString());
return field?.GetCustomAttribute<OriginalNameAttribute>()?.Name ?? loadZone.ToString();
}
static (string Standard, string Annex, LoadzoneLayerShortType Layer) SelectSnowZone(
LoadzoneStandardsResponseType standards,
string snowStandardName)
{
// Pick the snow load-zone standard/annex/layer from a GeoZone standards list.
var snowZones = standards.TypeGroups
?.FirstOrDefault(group => group.Name != null && group.Name.Equals("snow", StringComparison.OrdinalIgnoreCase))
?.Loadzones;
if (snowZones == null || snowZones.Count == 0)
{
throw new InvalidOperationException("No snow load-zone group found for the selected country.");
}
var zone =
snowZones.FirstOrDefault(z => z.Annex.Actual && z.Standard.Name == snowStandardName)
?? snowZones.FirstOrDefault(z => z.Annex.Actual)
?? snowZones[0];
return (zone.Standard.Name, zone.Annex.Name, zone.Annex.Layers[0]);
}
// -------------------------------------------------------
// MAIN SCRIPT
// -------------------------------------------------------
var snowLoadCases = new[] { 2, 3, 4 };
ApplicationRfem? rfemApp = null;
try
{
rfemApp = new ApplicationRfem();
// Create a clean model
rfemApp.close_all_models(saveChanges: false);
rfemApp.create_model(name: "duopitch_roof_building");
rfemApp.delete_all_objects();
// GeoZone Tool: fetch load zone, s_k and the resolved site location for
// GEOZONE_ADDRESS (token comes from the RFEM API key).
var apiKey = new Connection(Connection.GetConfigPath()).ResolveApiKey("", "");
using var gzt = new GeoZone(apiKey);
var standards = await gzt.GetLoadZoneStandardsAsync(new GetLoadZoneStandardsRequest
{
CountryCode = GEOZONE_COUNTRY_CODE,
Language = LanguageEnum.En,
});
var (snowStandard, snowAnnex, snowLayer) = SelectSnowZone(standards, SNOW_STANDARD_NAME);
var geozoneResult = await gzt.GetLoadZoneCharacteristicsAsync(new GetZoneCharacteristicsRequest
{
Address = GEOZONE_ADDRESS,
Type = LoadzoneTypeEnum.Snow,
Standard = snowStandard,
Annex = snowAnnex,
LayerId = snowLayer.Id,
Language = LanguageEnum.En,
});
if (geozoneResult == null)
{
Console.WriteLine("GeoZone returned no response.");
return;
}
var skVariable = geozoneResult.Characteristics
?.SelectMany(ch => ch.ZoneCharacteristics.Characteristics)
.FirstOrDefault(v => v.Name == "s_k");
if (skVariable == null || string.IsNullOrEmpty(skVariable.CalculatedValue))
{
throw new InvalidOperationException("GeoZone response does not contain 's_k'.");
}
// The standard-parameters tree stores s_k in SI Pa, so a kN/m2 value is scaled x1000.
var sk = double.Parse(skVariable.CalculatedValue, CultureInfo.InvariantCulture);
var skUnit = (skVariable.UnitsHtml ?? "").Replace("<sup>", "^").Replace("</sup>", "");
var skPa = skUnit.Contains("kN") ? sk * 1000.0 : sk;
// The zone value (e.g. '1a*') is mapped to a SnowLoad.LoadZone enum.
var zoneValue = geozoneResult.Characteristics![0].ZoneCharacteristics.Zone.Value;
var loadZone = SnowLoadZoneEnum(zoneValue);
var altitude = double.Parse(geozoneResult.GeoLocation.Altitude, CultureInfo.InvariantCulture);
var zoneName = SnowLoadZoneOriginalName(loadZone);
Console.WriteLine($"\nGeoZone snow query [{GEOZONE_ADDRESS}]:");
Console.WriteLine($" load zone = {zoneValue} ({zoneName})");
Console.WriteLine($" zone-derived s_k = {sk:F2} {skUnit} (= {skPa:F0} Pa)");
Console.WriteLine($" site altitude = {altitude:F1} m");
// Base Data: activate the load-wizards add-on, assign EN 1991 (DIN), and
// transfer the resolved site location. Keep the combination wizard OFF so the
// plain snow load cases are accepted as valid generation targets.
var baseData = rfemApp.get_base_data();
baseData.Addons.LoadWizardsActive = true;
baseData.Addons.CombinationWizardAndClassificationActive = false;
baseData.Standards.LoadWizardStandardGroup = Rfem.BaseData.Types.Standards.Types.LoadWizardStandardGroup.LoadWizardEn1991StandardGroup;
baseData.Standards.LoadWizardStandard = Rfem.BaseData.Types.Standards.Types.LoadWizardStandard.LoadWizardNationalAnnexAndEditionEn1991Din201904Standard;
baseData.Location.Altitude = altitude;
baseData.Location.Latitude = double.Parse(geozoneResult.GeoLocation.Latitude, CultureInfo.InvariantCulture) * Math.PI / 180.0;
baseData.Location.Longitude = double.Parse(geozoneResult.GeoLocation.Longitude, CultureInfo.InvariantCulture) * Math.PI / 180.0;
baseData.Location.TownCity = geozoneResult.GeoLocation.City;
rfemApp.set_base_data(baseData: baseData);
// Structure + load cases + Snow Load Wizard.
rfemApp.create_object_list(DefineStructure().Concat(DefineLoading(snowLoadCases)).ToList());
rfemApp.create_object_list(DefineSnowLoadWizard(snowLoadCases));
// Write the GeoZone values into the wizard's standard-parameters tree. s_k
// only becomes an editable row AFTER manual snow-load definition is enabled,
// so this is a two-step write:
// 1. enable manual mode -> update
// 2. re-read (s_k is now editable) -> set s_k -> update
var snow = rfemApp.get_object<Rfem.LoadWizards.SnowLoad>(new Rfem.LoadWizards.SnowLoad { No = 1 });
if (snow != null)
{
var parameters = snow.StandardParameters;
Common.TreeTable.SetValuesByKey(tree: parameters, key: "load_zone", values: new List<object> { (int)loadZone });
Common.TreeTable.SetValuesByKey(tree: parameters, key: "a", values: new List<object> { altitude });
Common.TreeTable.SetValuesByKey(tree: parameters, key: "manual_snow_load_definition", values: new List<object> { true });
Common.TreeTable.SetValuesByKey(tree: parameters, key: "s_k", values: new List<object> { skPa });
rfemApp.update_object(new Rfem.LoadWizards.SnowLoad { No = 1, StandardParameters = parameters });
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
if (rfemApp != null) rfemApp.close_connection();
}