### ===================================================================================================================
### FUNCTION: Create model plots for VIIA
### ===================================================================================================================
# Copyright ©VIIA 2025
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
from __future__ import annotations
from copy import copy
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional
# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.materials import DiscreteMass, LineMassMaterialModel
from rhdhv_fem.geometries import Geometry, Rectangle
from rhdhv_fem.shape_geometries import Node, Line
from rhdhv_fem.shapes import Roof, LineMass, Surfaces, Lines, Points
from rhdhv_fem.connections import Interface, NoConnection, Hinge, Connections
from rhdhv_fem.groups import Layer
from rhdhv_fem.plotting import fem_set_colour_lists
from rhdhv_fem.fem_math import fem_ordered_coordinates_list, fem_distance_coordinates, fem_compare_values
# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
from viiapackage.viiaStatus import ViiaProject
from viiapackage.layers import viia_get_highest_layer
from viiapackage.pictures.model_plots import viia_plot_foundation_details
from viiapackage.pictures.viia_appendix_pictures_folder import viia_appendix_pictures_folder
### ===================================================================================================================
### 3. Helper functions for creating model plots
### ===================================================================================================================
def _viia_get_added_mass_on_walls(project: ViiaProject, layer: Layer, dummy_counter: int) -> List[LineMass]:
""" Function to collect the walls that have mass added and show them with dashed lines on the load plots."""
line_masses = []
for wall in layer.walls:
if wall.material.added_mass is not None and wall.material.added_mass > 0:
# The added density on walls is checked to define if a wall has additional mass applied on it
# The mass will be shown as a line on the plot, at the bottom side of the wall
nodes = []
for line in wall.get_bottom_edges():
nodes.extend(line.get_nodes())
nodes = fem_ordered_coordinates_list(coordinates_list=[node.coordinates for node in set(nodes)])
if len(nodes) < 2:
project.write_log(
f"WARNING: The wall {wall.name} does not have two bottom points for the plotting of the added "
f"mass. Please check and add manually if required.")
continue
# Calculate the value of the line-mass by multiplying the volume of the wall with the added density and
# divide by the length of the line
line_length = fem_distance_coordinates(coordinate1=nodes[0], coordinate2=nodes[-1])
if fem_compare_values(value1=line_length, value2=0, precision=project.check_precision):
project.write_log(
f"WARNING: The bottom edge of the wall {wall.name} is too small for plotting of the added mass. "
f"Please check and add manually if required.")
continue
line_mass = wall.get_volume() * wall.material.added_mass / line_length
# Create dummy material, geometry and line-mass for the purpose of plotting the load
material = DiscreteMass(
name=f'LIN-LIJNMASSA-{line_mass:.1f}', material_model=LineMassMaterialModel(
distributed_mass_tangential_direction=line_mass, distributed_mass_first_normal_direction=line_mass,
distributed_mass_second_normal_direction=line_mass))
geometry = Geometry(name='LIJNMASSA', geometry_model=Rectangle(height=0.1, width=0.1, name='100x100'))
shape = LineMass(
name='WALL_LOADS', contour=Line(
node_start=Node(coordinates=nodes[0]), node_end=Node(coordinates=nodes[-1])),
material=material, geometry=geometry, element_z_axis=project.get_direction(name='Z'))
dummy_counter += 1
shape.id = dummy_counter
line_masses.append(shape)
return line_masses
### ===================================================================================================================
### 4. Create model plots for the TVA appendix C3
### ===================================================================================================================
[docs]def viia_create_model_plots(
project: ViiaProject, return_plot_data: bool = False, dpi: Optional[int] = None,
appendix_pictures_folder: Optional[Path] = None):
"""
This function creates most of the pictures for appendix of building setup using the Grid and Layer objects that
should be present in the model.
.. note:: All picture names must correspond to how pictures are inserted into the Appendix of building structural
setup template in the function _viia_create_model_plots_appendix.
The following picture are taken:
- Grids - all
- Side views - from two sides
- Foundations - Overview and cross-section details
- Floors - all layers
- Walls (and columns) - all layers
- Beams and lintels - all layers, except roof beams
- Roof beams
- Roof structure
- Loads - all layers, dead loads and live loads
Input:
- project (obj): Project object containing collections and of fem objects and project variables.
- return_plot_data (bool): If true, will return the plotting information for testing
- dpi (int): Dots per inch argument which is used to increase the resolution of the image. Default value
is None.
- appendix_pictures_folder (Path): Optional input for location where all the appendix pictures should be stored.
Default value is None.
Output:
- Pictures are created and saved in the folder 'Appendix Pictures' in the workfolder.
"""
project.write_log(f"Creating Appendix building setup pictures.")
# Set save folder
save_folder = viia_appendix_pictures_folder(project=project, appendix_pictures_folder=appendix_pictures_folder)
# Set all the colors list
fem_set_colour_lists(project=project)
# Adjust color lists for no-connections
counter_color_connections = len(project.connections_colour_dictionary)
for no_connection in project.collections.no_connections:
if 'D2.01A' in no_connection.name and 'D2.01A' not in project.connections_colour_dictionary:
project.connections_colour_dictionary['D2.01A'] = project.colours[counter_color_connections]
counter_color_connections += 1
elif 'NOCON-L' in no_connection.name and 'NOCON-L' not in project.connections_colour_dictionary:
project.connections_colour_dictionary['NOCON-L'] = project.colours[counter_color_connections]
counter_color_connections += 1
elif 'NOCON-P' in no_connection.name and 'NOCON-P' not in project.connections_colour_dictionary:
project.connections_colour_dictionary['NOCON-P'] = project.colours[counter_color_connections]
counter_color_connections += 1
else:
continue
def _in_roof(point: List[float]) -> bool:
""" Check if point is in one of the roofs."""
for roof in project.collections.roofs:
if roof.contour.is_point_in_surface(point):
return True
return False
def order_line_shapes(lines: List[Lines]):
"""
Order the line shapes in the correct order for plotting. Lines that are totally on top of another line are last
in the list so they will be plotted last / on top of other lines
"""
vertical_shapes = [shape for shape in lines if shape.is_vertical()]
remaining_shapes = [shape for shape in lines if shape not in vertical_shapes]
pairs_to_check = {}
for i, shape_1 in enumerate(remaining_shapes):
pairs_to_check[shape_1] = []
for shape_2 in remaining_shapes:
if shape_1 == shape_2:
continue
if shape_1.get_connecting_lines(shape=shape_2):
pairs_to_check[shape_1].append(shape_2)
solitary_shapes = [shape for shape, pair_list in pairs_to_check.items() if not pair_list]
for shape in solitary_shapes:
del pairs_to_check[shape]
smallest_shapes = []
while True:
new_smallest_shapes = []
for shape, others in pairs_to_check.items():
shape_lines = shape.internal_lines or [shape.contour]
for other in others:
other_lines = other.internal_lines or [other.contour]
if not all(line in other_lines for line in shape_lines):
break
else:
new_smallest_shapes.append(shape)
if new_smallest_shapes:
for shape in new_smallest_shapes:
del pairs_to_check[shape]
for value in pairs_to_check.values():
if shape in value:
value.remove(shape)
smallest_shapes.extend(new_smallest_shapes)
else:
break
smallest_shapes.reverse()
return list(pairs_to_check.keys()) + vertical_shapes + solitary_shapes + smallest_shapes
# Plot roof beams in 3D
roof_beams = []
for beam in project.collections.beams:
if all([_in_roof(point=pt) for pt in beam.contour.get_points()]):
roof_beams.append(beam)
# Check if certain beams are qualified as roof-beams in modelscript
for beam in project.project_specific['lists']['roof_beams']:
if beam not in roof_beams:
roof_beams.append(beam)
# Only create the roof beams plot if there are roof-beams present
if roof_beams:
project.create_plots(
collections=roof_beams, alpha=1, add_collections=project.collections.surfaces, add_alpha=0.1,
viewpoint=[40, -120], save_folder=save_folder, file_name='Roof Beams - Roof Beams 3D - View 1', show=False,
title='', dpi=dpi)
project.create_plots(
collections=roof_beams, alpha=1, add_collections=project.collections.surfaces, add_alpha=0.1,
viewpoint=[40, 60], save_folder=save_folder, file_name='Roof Beams - Roof Beams 3D - View 2', show=False,
title='', dpi=dpi)
# Grid pictures
for grid in project.collections.grids:
grid.plot(
save_folder=save_folder, show=False, show_dimensions=True, file_name=f'Grids - {grid.name}', title='',
dpi=dpi)
# Cross-section pictures
grid = project.collections.grids[0]
project.plot_building_cross_section(
plotting_plane='+xz', grid=grid, show=False, save_folder=save_folder, file_name='Side Views - Side View 1',
title='')
project.plot_building_cross_section(
plotting_plane='+yz', grid=grid, show=False, save_folder=save_folder, file_name='Side Views - Side View 2',
title='')
project.plot_building_cross_section(
plotting_plane='-xz', grid=grid, show=False, save_folder=save_folder, file_name='Side Views - Side View 3',
title='')
project.plot_building_cross_section(
plotting_plane='-yz', grid=grid, show=False, save_folder=save_folder, file_name='Side Views - Side View 4',
title='')
# Foundation detail plots and overview
viia_plot_foundation_details(project=project, save_folder=save_folder, dpi=dpi)
# Layer pictures
layer_pics = {
'Foundations - Overview': {
'shape_types': ['walls', 'fstrips'],
'layers': 'F',
'plot_load': None},
'Floors - %s Floor': {
'shape_types': ['floors'],
'layers': 'not F',
'plot_load': None},
'Walls and Columns - Walls and Columns %s Floor': {
'shape_types': ['walls', 'columns'],
'layers': 'not F',
'plot_load': None},
'Interface Connections - Line and point interfaces %s Floor': {
'shape_types': ['connections'],
'layers': 'all',
'plot_load': None},
'No Connections - Line and point disconnects %s Floor': {
'shape_types': ['connections'],
'layers': 'all',
'plot_load': None},
'Hinged Connections - Hinges %s Floor': {
'shape_types': ['connections'],
'layers': 'all',
'plot_load': None},
'Beams and Lintels - Beams and Lintels %s Floor': {
'shape_types': ['beams', 'lintels'],
'layers': 'all',
'plot_load': None},
'Roof Beams - Roof Beams %s Floor': {
'shape_types': ['beams'],
'layers': 'all',
'plot_load': None},
'Roof Structure - Roof Structure %s Floor': {
'shape_types': ['roofs'],
'layers': 'all',
'plot_load': None},
'Loads - Dead Loads on %s Floor': {
'shape_types': ['line_masses', 'floors'],
'layers': 'all',
'plot_load': 'permanent'},
'Loads - Live Loads on %s Floor': {
'shape_types': ['floors'],
'layers': 'all',
'plot_load': 'variable'},
'Loads - Dead Loads on %s Roof': {
'shape_types': ['roofs'],
'layers': 'all',
'plot_load': 'permanent'},
'Loads - Live Loads on %s Roof': {
'shape_types': ['roofs'],
'layers': 'all',
'plot_load': 'variable'}}
# Layer name conversion dictionary
layer_conv = {
'F': 'Foundation',
'B1': 'Basement',
'B2': 'Second Basement',
'N0': 'Ground',
'N1': 'First',
'N2': 'Second',
'N3': 'Third',
'N4': 'Fourth',
'N5': 'Fifth',
'N6': 'Sixth',
'N7': 'Seventh',
'N8': 'Eighth',
'N9': 'Ninth',
'N10': 'Tenth',
'N11': 'Eleventh',
'N12': 'Twelfth'}
# Record data for testing
plots_lst = []
dummy_counter = 0
for name, settings in layer_pics.items():
if settings['layers'] == 'all':
layers = copy(project.collections.layers)
elif settings['layers'] == 'uppermost':
layers = [project.get_uppermost_layer()]
elif settings['layers'] == 'not uppermost':
layers = copy(project.collections.layers)
layers.remove(project.get_uppermost_layer())
elif settings['layers'] == 'not F':
layers = copy(project.collections.layers)
for layer in layers:
if layer.name == 'F':
layers.remove(layer)
break
elif settings['layers'] == 'F':
for layer in project.collections.layers:
if layer.name == 'F':
layers = [layer]
break
else:
layers = []
else:
raise ValueError(f"ERROR: Layer settings for {name} is not specified.")
# Do thicker lines for the wall plots
if 'Walls and Columns' in name:
thickness_scale = 3.5
else:
thickness_scale = 2.0
# Only show walls in the background for Beams and Lintels
if 'Beams and Lintels' in name:
background_shapes = True
else:
background_shapes = False
for layer in layers:
# Set file name
fname = None
if not fname:
if '%s' in name:
fname = name % layer_conv[layer.name]
else:
fname = name
# Get the shapes and set some settings
plots = {}
building_area = False
shape_types = []
legend_info = {} # dict containing shapes as keys and corresponding legend_labels and colors as values
# For foundations, split up into nonlinear parts (upper foundation wall) and linear (lower foundation wall
# and foundation strip)
if 'Foundations' in name:
plots['Foundations - Foundations Linear Elements'] = \
[[shape for shape in layer.walls + layer.fstrips if shape.material.is_linear], grid, legend_info]
plots['Foundations - Foundations Nonlinear Elements'] = \
[[shape for shape in layer.walls + layer.fstrips if not shape.material.is_linear], grid,
legend_info]
# For floors, walls, live loads, all shape types described above
elif any(sub_str in name for sub_str in ['Floors', 'Walls', 'Live Loads']):
if not ('Walls' in name and layer.name == 'N0'):
building_area = True
shapes = []
for shape_type in settings['shape_types']:
shapes.extend(getattr(layer, shape_type))
plots[fname] = [shapes, grid, legend_info]
# For the roof structure, get the roofs
elif 'Roof Structure' in name:
roofs = [roof for roof in project.collections.roofs if roof in layer.shapes]
if roofs:
building_area = True
plots[fname] = [roofs, grid, legend_info]
# For the roof beams, get the roof beams that were retrieved towards the top of this function
elif 'Roof Beams' in name:
roof_beams_2 = [beam for beam in roof_beams if beam in layer.shapes]
if roof_beams_2:
building_area = True
plots = {f'{fname} - {grid.name}': [[], grid, legend_info]}
for beam in roof_beams_2:
if beam.meta_data and 'grid' in beam.meta_data:
beam_grid = beam.meta_data['grid']
if f'{fname} - {beam_grid.name}' not in plots:
plots[f'{fname} - {beam_grid.name}'] = [[beam], beam_grid]
else:
plots[f'{fname} - {beam_grid.name}'][0].append(beam)
else:
plots[f'{fname} - {grid.name}'][0].append(beam)
# For beams and lintels, leave out the roof beams
elif 'Beams and Lintels' in name:
plots[fname] = [[shape for shape in layer.beams + layer.lintels if shape not in roof_beams], grid,
legend_info]
if layer.name != 'N0':
building_area = True
# For interfaces
elif 'Connections' in name:
# Get line interfaces
if layer.get_connections():
connections_to_plot = []
# For some connections, it may appear on two layers, always plot the connection on the higher layer
for conn in layer.get_connections():
if len(conn.get_layers()) > 1:
higher_layer = viia_get_highest_layer(project, *conn.get_layers())
if higher_layer == layer:
connections_to_plot.append(conn)
if 'Interface' in name:
interface_connections_list = []
for i in connections_to_plot:
if isinstance(i, Interface):
interface_connections_list.append(i)
elif isinstance(i, NoConnection) and 'D2.01A' in i.name:
interface_connections_list.append(i)
legend_info[i] = [f'D2.01A', project.connections_colour_dictionary['D2.01A']]
plots[fname] = [interface_connections_list, grid, legend_info]
elif 'No Connections' in name:
no_connections_list = []
for i in connections_to_plot:
if isinstance(i, NoConnection) and 'NOCON-L' in i.name:
no_connections_list.append(i)
legend_info[i] = ['NOCON-L', project.connections_colour_dictionary['NOCON-L']]
elif isinstance(i, NoConnection) and 'NOCON-P' in i.name:
no_connections_list.append(i)
legend_info[i] = ['NOCON-P', project.connections_colour_dictionary['NOCON-P']]
plots[fname] = [no_connections_list, grid, legend_info]
elif 'Hinge' in name:
plots[fname] = [[i for i in connections_to_plot if isinstance(i, Hinge)], grid, legend_info]
building_area = True
# For dead loads on floors
elif 'Dead Loads on %s Floor' in name:
plot_shapes = []
# First add the floors, as this improves the plots for colour usage
if layer.floors:
for shape in layer.floors:
plot_shapes.append(shape)
if layer.line_masses:
for shape in layer.line_masses:
# Exclude line-masses in the facade openings and the line-masses with a roof as host
if 'ONDER' not in shape.name and 'BOVEN' not in shape.name and not isinstance(shape.host, Roof):
plot_shapes.append(shape)
# Add dummy line-masses in the load plot for added mass on walls, except in the foundation
# These will show as dashed lines
if layer.name != 'F':
dummy_line_masses = _viia_get_added_mass_on_walls(
project=project, layer=layer, dummy_counter=dummy_counter)
plot_shapes.extend(dummy_line_masses)
dummy_counter += len(dummy_line_masses)
# Set the info for the plot of dead load on floors
plots[fname] = [plot_shapes, grid, legend_info]
building_area = True
# For dead loads on roofs
elif 'Dead Loads on %s Roof' in name:
plot_shapes = []
if layer.roofs:
for shape in layer.roofs:
plot_shapes.append(shape)
if layer.line_masses:
for shape in layer.line_masses:
# Exclude line-masses in the facade openings and only apply line-masses with a roof as host
if 'ONDER' not in shape.name and 'BOVEN' not in shape.name and isinstance(shape.host, Roof):
plot_shapes.append(shape)
plots[fname] = [plot_shapes, grid, legend_info]
building_area = True
# Plot the layer with the specified shapes, and save the figure
if plots and not return_plot_data:
for file_name, val in plots.items():
if val[0]:
# Change the order of the shapes to plot, so that horizontal Surfaces are plotted first,
# not horizontal Surfaces are plotted second, Lines third and
# Points forth and Connections last
shapes = \
[shape for shape in val[0] if isinstance(shape, Surfaces) and shape.is_horizontal()] + \
[shape for shape in val[0] if isinstance(shape, Surfaces) and not shape.is_horizontal()] + \
order_line_shapes(lines=[shape for shape in val[0] if isinstance(shape, Lines)]) + \
[shape for shape in val[0] if isinstance(shape, Points)] + \
[shape for shape in val[0] if isinstance(shape, Connections)]
if len(shapes) != len(val[0]):
raise ValueError(
f"ERROR: Not all shapes in {file_name} are of type Surfaces, Lines, Points or "
f"Connections. Unknown items are: {[shape for shape in val[0] if shape not in shapes]}")
layer.plot(
grid=val[1], shape_types=shape_types, shapes=shapes, thickness_scale=thickness_scale,
show=False, background_shapes=background_shapes, show_dimensions=False,
save_folder=save_folder, file_name=file_name, plot_load=settings['plot_load'], title='',
building_area=building_area, dpi=dpi, legend_info=legend_info)
plots_lst.append(plots)
# Create the PSSE NSCE plot in a sub-folder
project.viia_plot_psse_nsce()
if return_plot_data:
return plots_lst
project.write_log(f"Drafts of your appendix building setup pictures have been created and saved in {save_folder}.")
return None
### ===================================================================================================================
### 4. End of script
### ===================================================================================================================