Source code for viiapackage.pictures.model_plots.viia_create_model_plots

### ===================================================================================================================
###  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 ### ===================================================================================================================