### ===================================================================================================================
### VIIA create appendices
### ===================================================================================================================
# Copyright ©VIIA 2025
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
from __future__ import annotations
from typing import TYPE_CHECKING, Dict, List, Any, Optional
from datetime import datetime
from pathlib import Path
import yaml
# References for functions and classes in the rhdhv_fem package
from rhdhv_fem.fem_tools import fem_create_folder
from rhdhv_fem.fem_math import fem_compare_values
# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
from viiapackage.viiaStatus import ViiaProject
from viiapackage.geometry import viia_get_fstrip_width
from viiapackage.reporting.viia_report_input_check import viia_get_tender_specification_deliverable
from viiapackage.reporting.helper_functions.viia_get_general_info import viia_get_general_info
### ===================================================================================================================
### 2. Functions to create appendices
### ===================================================================================================================
[docs]def viia_create_model_plots_appendix(
project: ViiaProject, template_file: Optional[Path] = None, output_folder: Optional[Path] = None,
pictures_folder: Optional[Path] = None) -> Path:
"""
This function creates appendix of building setup for the engineering report.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- template_location (Path): Optional input for the location of the template file. Default value None. If not
provided the file will be selected from the template library within this function.
- output_folder (Path): Optional input for location where to create the report appendix. Default value is None,
indicating the default location is used. In normal production objects do not change this!
- pictures_folder (Path): Optional input for location where all the pictures should be collected from.
Default value is None. In which case the appendix pictures folder set in project is used.
Output:
- The requested report is generated with the information of the object in py-memory, databases and local
(image-) files. It is saved in the 'ER' folder of the working folder or the location of the folder mentioned
in the input.
"""
# Find template if not provided
if template_file is None:
# Collect the tender specification from the deliverable on MYVIIA
tender_specification = viia_get_tender_specification_deliverable(project=project, report_type='engineering')
# Read yaml with template references
with open(project.viia_settings.project_specific_package_location / 'reporting' / 'reports.yaml') as f:
report_templates = yaml.load(f, Loader=yaml.FullLoader)
# Set template (collect all templates for defined template version)
template_file = \
project.viia_settings.project_specific_package_location / \
report_templates['engineering']['C3'][tender_specification][project.analysis_type]
# Check if the template file exists
if not template_file.exists():
raise NotImplementedError(
"ERROR: Could not find the correct template to be used for the model plots appendix. Please report to the "
"VIIA automation team.")
# Set the location of relevant files and folders, as well as the output document name
if output_folder:
output_document_name = f'VIIA-{project.name}-C3.docx'
else:
time_reference = datetime.now().strftime('%Y%m%d%H%M%S')
output_folder = project.workfolder_location / 'ER'
output_document_name = f'VIIA-{project.name}-C3-{time_reference}.docx'
# Create the report folder
fem_create_folder(output_folder)
# Get the general info for the template
context = viia_get_general_info(project)
# Add necessary data to the context
context['all_pictures'] = get_all_pictures(project=project, pictures_folder=pictures_folder)
context['fstrips'] = get_fstrips_info(project=project)
context['fwalls'] = get_fwalls_info(project=project)
context['floors'] = get_floors_info(project=project)
context['roofs'] = get_roofs_info(project=project)
context['count'] = {
'fstrips': len(project.collections.fstrips),
'fwalls': len([wall for wall in project.collections.walls if 'F-WANDEN' in wall.name]),
'floors': len(project.collections.floors),
'walls': len([wall for wall in project.collections.walls if 'F-WANDEN' not in wall.name]),
'columns': len(project.collections.columns),
'beams': len(project.collections.beams) + len(project.collections.lintels),
'roofs': len(project.collections.roofs),
'linemasses': len(project.collections.line_masses)}
# Getting rid of 'Grids' from context['all_pictures'] dictionary, since a few are not required in
# template version v7_1
if 'Grids' in context['all_pictures']:
del (context['all_pictures']['Grids'])
# Create report
generated_report = project.create_report(
template_file=template_file, data=context, output_file=output_folder / output_document_name, images=True)
project.write_log(
f"A draft of your Appendix building setup {output_document_name} is generated and saved in "
f"'{output_folder.as_posix()}'.")
return generated_report.output_file
### ===================================================================================================================
### 3. Functions to get info for the appendices
### ===================================================================================================================
[docs]def get_all_pictures(project: ViiaProject, pictures_folder: Optional[Path] = None) -> Dict[str, List[str]]:
"""
Function collects all pictures for the building structural setup appendix.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
- pictures_folder (Path): Optional input for location where all the pictures should be collected from. Default
value is None.
Output:
- Returns a dictionary with sections (keys) and list of pictures in the picture folder (values).
"""
# These lists determine ordering: first by section, then by element. Exception: Loads and Roofs (ordered by floor)
sections = [
'Grids', 'Side Views', 'Foundations', 'Basement', 'Ground Floor', 'First Floor', 'Second Floor', 'Third Floor',
'Fourth Floor', 'Fifth Floor', 'Sixth Floor', 'Seventh Floor', 'Eighth Floor', 'Ninth Floor', 'Tenth Floor',
'Eleventh Floor', 'Twelfth Floor', 'Attic Floor', 'Roof Structure', 'Loads']
elements = [
'Floors', 'Walls and Columns', 'Beams and Lintels', 'Surface reinforcements', 'Interface Connections',
'No Connections', 'Hinged Connections']
floor_order = [
'Foundation', 'Basement', 'Ground', 'First', 'Second', 'Third',
'Fourth', 'Fifth', 'Sixth', 'Seventh', 'Eighth', 'Ninth', 'Tenth',
'Eleventh', 'Twelfth']
# Set up a basic structure to be sure all sections are covered
all_pictures = [[section, None] for section in sections]
# If pictures folder is not provided, the appendix_pictures_folder stored in project is selected
if pictures_folder is None and not project.appendix_pictures_folder:
return all_pictures
if pictures_folder is None:
pictures_folder = project.appendix_pictures_folder
if not pictures_folder.exists():
raise FileNotFoundError(f"The folder '{pictures_folder}' does not exist.")
# If we know the pictures folder, continue
else:
# Sorting and filtering is done by picture name
pictures = [x.name for x in pictures_folder.iterdir() if x.is_file()]
# Loop over the basic sections
for i, section in enumerate(sections):
# Get all pictures for the section and store them in a list
# (make sure the loads only pop up in the 'Loads' section, and roofs only pop up in the 'Roofs' section)
section_pictures = []
if section == 'Loads':
for picture in pictures:
if section in picture:
section_pictures.append(picture)
elif section == 'Roof Structure':
for picture in pictures:
if 'Roof' in picture and 'Loads' not in picture:
section_pictures.append(picture)
else:
for picture in pictures:
if section in picture and 'Roof' not in picture and 'Loads' not in picture:
section_pictures.append(picture)
# If there are no pictures in the section, skip the section
if not section_pictures:
continue
# For the standard levels (basement -> roof), filter by element type.
# Wonderfully, ordering of element types inside of 'elements' is preserved in the dict
if section not in ['Grids', 'Side Views', 'Foundations', 'Roof Structure', 'Loads']:
filtered_section_pictures = {
element: [picture for picture in section_pictures if element in picture] for element in elements}
# For the foundation, make sure the foundation details are shown after the other pictures
elif section == 'Foundations':
filtered_section_pictures = {section: []}
max_id_details = []
for j, picture in enumerate(section_pictures):
if 'Cross section' in picture:
max_id_details.append(j)
if max_id_details:
filtered_section_pictures[section] = section_pictures[max(max_id_details) + 1:] + \
section_pictures[:max(max_id_details) + 1]
else:
filtered_section_pictures[section] = section_pictures
# Special case for Roof and Loads: these need to be ordered by floor again,
# with the pictures that don't belong to a specific floor at the end
elif section == 'Roof Structure' or section == 'Loads':
ordered_section_pictures = []
# Add per floor
for floor in floor_order:
for picture in section_pictures:
if floor in picture and picture not in ordered_section_pictures:
ordered_section_pictures.append(picture)
# Add the rest at the end
for picture in section_pictures:
if picture not in ordered_section_pictures:
ordered_section_pictures.append(picture)
filtered_section_pictures = {section: ordered_section_pictures}
# For grids and side views, further ordering or filtering is not needed
else:
filtered_section_pictures = {section: section_pictures}
# Change all the pictures into filepaths
for k,v in filtered_section_pictures.items():
filtered_section_pictures[k] = [pictures_folder.as_posix() + '/' + pic_name for pic_name in v]
all_pictures[i][1] = filtered_section_pictures
return all_pictures
[docs]def get_fstrips_info(project: ViiaProject) -> List[Dict[str, Any]]:
"""
Function collects the information in building structural setup appendix for the foundation strips.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns a list with dictionaries with information per foundation strip.
"""
fstrips = []
for fstrip in project.collections.fstrips:
fstrips.append({
'name': f'Foundation strip {fstrip.id}',
'material': fstrip.material.name.replace('<', ' pre-').replace('>', ' post-'),
'depth': round(fstrip.contour.get_min_z(), 3),
'thickness': round(fstrip.geometry.geometry_model.thickness, 3),
'width': round(viia_get_fstrip_width(fstrip=fstrip), 3),
'density': int(round(fstrip.material.mass_density, 0))})
return fstrips
[docs]def get_fwalls_info(project: ViiaProject) -> List[Dict[str, Any]]:
"""
Function collects the information in building structural setup appendix for the foundation walls.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns a list with dictionaries with information per foundation wall.
"""
fwalls = []
layer_f = project.find('F', 'layers')
if layer_f and layer_f.walls:
for fwall in project.find('F', 'layers').walls:
mat_split = fwall.material.name.split('-')
add_ind = 1 if 'LIN' in mat_split else 0
material = '-'.join(fwall.material.name.split('-')[:2 + add_ind])
material = material.replace('<', ' pre-').replace('>', ' post-')
fwalls.append({
'name': f'Foundation wall {fwall.id}',
'linearity': 'Linear' if fwall.material.is_linear else 'Nonlinear',
'material': material,
'thickness': round(fwall.geometry.geometry_model.thickness, 3),
'height': round(abs(fwall.contour.get_max_z() - fwall.contour.get_min_z()), 3),
'density': int(round(fwall.material.mass_density, 0))})
return fwalls
[docs]def get_floors_info(project: ViiaProject) -> Dict[str, Any]:
"""
Function collects the information in building structural setup appendix for the floors in the model.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns a dictionary with information of the floors in the model.
"""
timber_floors = []
concrete_combi_floors = []
nehobo_floors = []
ribbed_floors = []
beam_block_floors = []
other_floors = []
for floor in project.collections.floors:
mat_split = floor.material.name.split('-')
add_ind = 1 if 'LIN' in mat_split else 0
# Equivalent timber floors
if any(sub_str in floor.material.name for sub_str in ['PLANKEN', 'PLATEN']):
# If Timber floors are made using NPR version before 2020, then there is no info about the span and width,
# and the length of the list is too short
if len(mat_split) < 7 + add_ind + 1:
span, width = '-', '-'
else:
span = mat_split[6 + add_ind]
width = mat_split[7 + add_ind]
timber_floors.append({
'name': f'Floor {floor.id}',
'material': '-'.join(mat_split[:2 + add_ind]),
't_eq': floor.geometry.geometry_model.thickness * 1E3,
't_pl': mat_split[2 + add_ind],
'joist_width': mat_split[3 + add_ind],
'joist_height': mat_split[4 + add_ind],
'joist_ctc': mat_split[5 + add_ind],
'span': span,
'width': width})
# Concrete and/or combination floors
elif any(sub_str in floor.material.name for sub_str in ['BETON', 'LEWIS', 'MPV', 'KPV']):
concrete_combi_floors.append({
'name': f'Floor {floor.id}',
'material': '-'.join(mat_split[:1 + add_ind]),
'thickness': floor.geometry.geometry_model.thickness,
'top_layer': mat_split[-1] if 'KPV' in floor.material.name else '-'})
# NEHOBO floors
elif 'NEHOBO' in floor.material.name:
nehobo_floors.append({
'name': f'Floor {floor.id}',
'material': '-'.join(mat_split[:2 + add_ind]),
'thickness': floor.geometry.geometry_model.thickness})
# Ribbed floors
elif 'RIB' in floor.material.name:
ribbed_floors.append({
'name': f'Floor {floor.id}',
'material': '-'.join(mat_split[:2 + add_ind]),
'thickness': floor.geometry.geometry_model.thickness})
# Beam-and-block floors
elif any(sub_str in floor.material.name for sub_str in ['COMBI', 'DATO', ]):
beam_block_floors.append({
'name': f'Floor {floor.id}',
'material': floor.material.name,
'thickness': floor.geometry.geometry_model.thickness,
'top_layer': mat_split[-1] if 'COMBI' in floor.material.name else '-'})
# Other floors
else:
other_floors.append({
'name': f'Floor {floor.id}',
'material': floor.material.name,
'geometry': floor.geometry.name})
return {
'timber_floors': timber_floors,
'concrete_combi_floors': concrete_combi_floors,
'nehobo_floors': nehobo_floors,
'ribbed_floors': ribbed_floors,
'beam_block_floors': beam_block_floors,
'other_floors': other_floors}
[docs]def get_roofs_info(project: ViiaProject) -> Dict[str, Any]:
"""
Function collects the information in building structural setup appendix for the roofs.
Input:
- project (obj): VIIA project object containing collections of fem objects and project variables.
Output:
- Returns a dictionary with information of the roofs in the model.
"""
rafter_roofs = []
purlin_roofs = []
other_roofs = []
for roof in project.collections.roofs:
mat_split = roof.material.name.split('-')
add_ind = 1 if 'LIN' in mat_split else 0
# Equivalent timber roofs with purlins
if any(sub_str in roof.material.name for sub_str in ['PLANKEN', 'PLATEN']) and \
fem_compare_values(roof.element_x_axis.vector[2], 0):
purlin_roofs.append({
'name': f'Roof {roof.id}',
'material': '-'.join(mat_split[:2 + add_ind]),
't_eq': roof.geometry.geometry_model.thickness * 1E3,
't_pl': mat_split[2 + add_ind],
'beam_width': mat_split[3 + add_ind],
'beam_height': mat_split[4 + add_ind],
'beam_ctc': mat_split[5 + add_ind]})
# Equivalent timber roofs with rafters
elif any(sub_str in roof.material.name for sub_str in ['PLANKEN', 'PLATEN']) and \
not fem_compare_values(roof.element_x_axis.vector[2], 0):
rafter_roofs.append({
'name': f'Roof {roof.id}',
'material': '-'.join(mat_split[:2 + add_ind]),
't_eq': roof.geometry.geometry_model.thickness * 1E3,
't_pl': mat_split[2 + add_ind],
'beam_width': mat_split[3 + add_ind],
'beam_height': mat_split[4 + add_ind],
'beam_ctc': mat_split[5 + add_ind]})
# Other roofs
else:
other_roofs.append({
'name': f'Roof {roof.id}',
'material': roof.material.name,
'geometry': roof.geometry.name})
return {
'rafter_roofs': rafter_roofs,
'purlin_roofs': purlin_roofs,
'other_roofs': other_roofs}
### ===================================================================================================================
### 4. End of script
### ===================================================================================================================