### ===================================================================================================================
###   CLASS: GeoOutput
### ===================================================================================================================
# Copyright ©VIIA 2025
### ===================================================================================================================
###   1. Import modules
### ===================================================================================================================
# General imports
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List
from collections import namedtuple
from pathlib import Path
import json
# References for functions and classes in the haskoning_structural package
from haskoning_structural.fem_math import fem_compare_values, fem_min_max_point_list, fem_angle_between_2_vectors
from haskoning_structural.analyses import SteppedAnalysisReference, Analysis
# References for functions and classes in the viiaPackage
if TYPE_CHECKING:
    from viiapackage.viiaStatus import ViiaProject
from viiapackage.geometry import viia_get_fstrip_width, get_fstrip_average_length, get_fstrip_direction, \
    get_fstrip_placement_point_for_geo
### ===================================================================================================================
###   2. Base-class for geo-output
### ===================================================================================================================
[docs]class GeoOutput:
    """ This is the super-class for the output for the geotechnical assessment. It contains the general methods and sets
    the general attributes."""
    def __init__(self, foundation_type: str, support_type: str = 'FlexBaseFinal'):
        """
        Input:
            - foundation_type (str): The foundation type modelled in the model. The allowed values are 'mixed', 'strips'
              or 'piles'.
            - support_type (str): The support type modelled in the model. The default value is 'FlexBaseFinal'. The
              allowed values are 'FixedBase', 'FlexBaseGlobal', 'FlexBaseFinal'.
        """
        # When the instance of GeoOutput class is initialised, the project needs is assigned
        self.project = None
        # Element data from the .dat file
        self.element_data = {}
        # Foundation element data
        self.foundation_elements = {
            'fstrips': [],
            'piles': []}
        # Forces in the foundation elements
        self.forces_foundation_elements = {
            'fstrips': [],
            'piles': []}
        # Displacement in the pile foundation elements
        self.displacements_foundation_elements = {
            'piles': []}
        # Set up the foundation type of the geo output
        self.foundation_type = foundation_type
        # Set up the support type of the geo output
        self.support_type = support_type
    @property
    def foundation_type(self):
        return self.__foundation_type
    @foundation_type.setter
    def foundation_type(self, foundation_type: str):
        if foundation_type not in ['mixed', 'piles', 'strips']:
            raise NotImplementedError(
                f"ERROR: The provided {foundation_type} is not supported yet by the geo output class.")
        self.__foundation_type = foundation_type
    @property
    def support_type(self):
        return self.__support_type
    @support_type.setter
    def support_type(self, support_type: str):
        if support_type not in ['FixedBase', 'FlexBaseGlobal', 'FlexBaseFinal']:
            raise NotImplementedError(f"ERROR: The support type {support_type} is not implemented yet for geo-output.")
        self.__support_type = support_type
[docs]    @classmethod
    def check_foundation_type(cls, project: ViiaProject):
        """Get the foundation type"""
        foundation_type = None
        support_types = set([support_set.name for support_set in project.collections.supportsets])
        if any([support in support_types for support in ['FixedBase', 'FlexBaseGlobal', 'FlexBaseFinal']]):
            foundation_type = 'strips'
        if 'PileSupport' in support_types:
            foundation_type = 'piles'
            if any([support in support_types for support in
                    ['FixedBase', 'FlexBaseGlobal', 'FlexBaseFinal']]):
                foundation_type = 'mixed'
        if not foundation_type:
            raise NotImplementedError(
                f"The foundation type in the dat file of project {project.name} could not be identified,"
                f"please check the name convention of the foundations and the inputs.")
        return foundation_type 
[docs]    def get_fstrip_elements(self):
        """ Get the foundation strip elements."""
        if self.foundation_elements['fstrips']:
            return self.foundation_elements['fstrips']
        for shape in [item.fstrip for item in self.forces_foundation_elements['fstrips']]:
            info = dict()
            info['object'] = shape
            info['material'] = shape.material.name
            info['thickness'] = shape.geometry.geometry_model.thickness
            info['forces'] = []
            # Set the minimum and maximum coordinates
            min_max = fem_min_max_point_list(shape.get_points())
            for key, value in min_max.items():
                # min_max returns keys like 'min-x', while they should be 'min_x' here.
                info[key.replace('-', '_')] = value
            # Set the center point of the strip
            info['x_coord'], info['y_coord'] = get_fstrip_placement_point_for_geo(shape)
            # Get the width of the strip
            info['width'] = viia_get_fstrip_width(fstrip=shape)
            # Determine angle between fstrip and x axis
            fstrip_direction = get_fstrip_direction(shape)
            angle = fem_angle_between_2_vectors(fstrip_direction, [1, 0, 0], degrees=True)
            info['angle'] = angle if not fem_compare_values(angle, 180) else 0  # Set angle to 0 if horizontal
            # Get average length, in case it's not symmetrical
            average_length = get_fstrip_average_length(fstrip=shape)
            info['length'] = average_length
            # add to foundation elements
            self.foundation_elements['fstrips'].append(info)
        return self.foundation_elements['fstrips'] 
[docs]    def get_pile_elements(self):
        """ Get the foundation pile elements."""
        if self.foundation_elements['piles']:
            return self.foundation_elements['piles']
        for pile in self.project.collections.piles:
            info = dict()
            info["object"] = pile
            info['material'] = pile.material.name
            info["forces"] = []
            part = pile.parent
            if part is not None:
                for connection in part.connections:
                    if "FCRITZ" in connection.name:
                        info["vertical spring"] = connection
                        break
            # Set the minimum and maximum coordinates
            min_max = fem_min_max_point_list(pile.get_points())
            for key, value in min_max.items():
                # min_max returns keys like 'min-x', while they should be 'min_x' here.
                info[key.replace('-', '_')] = value
            # Set the center point of the strip
            info["x_coord"] = (min_max['x-max'] + min_max['x-min']) / 2.0
            info["y_coord"] = (min_max['y-max'] + min_max['y-min']) / 2.0
            # The pile naming convention is different for FixedBase and for FlexBase
            # For FixedBase, the pile naming convention is 'PAAL-LIN-BETON-290x290-TYPE-A-1'
            if self.project.viia_settings.support_type == 'FixedBase':
                group_code = info['object'].name.split('-')[-2]
            else:
                # For FlexBase, the pile naming convention is 'PAAL-290x290-TYPE-Prefab-Paal-A-HUAN-1'
                group_code = info['object'].name.split('-')[-3]
            if group_code not in [chr(x) for x in range(ord('A'), ord('Z')+1)]:
                raise ValueError(
                    f"ERROR: The naming convention is incorrect [{info['name']}]. The group code {group_code} could "
                    f"not be retrieved. Please select from 'A' to 'Z'.")
            else:
                info['group_nr'] = ord(group_code) - 64
            self.foundation_elements['piles'].append(info)
        return self.foundation_elements['piles'] 
[docs]    def get_fstrip_pairs(self) -> namedtuple:
        """ Method of 'GeoOutput' class to obtain the fstrips and the corresponding boundary interfaces from the
        dat-file."""
        # Container for name mapping
        Fstrip_pair = namedtuple('Fstrip_pair', ['fstrip_obj', 'fstrip_itf'])
        fstrip_pairs = []
        
        # All boundary interface names
        fstrips_boundary_itfs = [
            fstrip for fstrip in self.project.collections.boundary_interfaces if 'VLAK-IF' in fstrip.name]
        
        # All fstrip names
        fstrips_lst = [
            fstrip for fstrip in self.project.collections.fstrips if 'STROKEN' in fstrip.name]
        # Since the coordinate should be the same, the node should be exactly the same
        for fstrip_itf in fstrips_boundary_itfs:
            fstrip_itf_nodes = fstrip_itf.mesh.get_meshnodes()
            
            # Loop through all fstrips and find the corresponding fstrip
            for fstrip in fstrips_lst:
                fstrip_nodes = fstrip.mesh.get_meshnodes()
                
                # The fstrip node is a subset of interface node
                if all(i in fstrip_itf_nodes for i in fstrip_nodes):
                    
                    # Create the mapping : 'F-STROKEN-LIN-MW-KLEI<1945-100-20' <-> 'F-VLAK-IF-FOS-20'
                    fstrip_pairs.append(Fstrip_pair(fstrip_obj=fstrip, fstrip_itf=fstrip_itf))
                    break
            
            # Check if the boundary interface connects to floor shape
            else:
                # Loop through all floors and find the corresponding floor
                for floor in self.project.collections.floors:
                    floor_nodes = floor.mesh.get_meshnodes()
                    # The floor node is a subset of interface node
                    if all(i in fstrip_itf_nodes for i in floor_nodes):
                        # Create the mapping : 'F-STROKEN-LIN-MW-KLEI<1945-100-20' <-> 'F-VLAK-IF-FOS-20'
                        fstrip_pairs.append(Fstrip_pair(fstrip_obj=floor, fstrip_itf=fstrip_itf))
                        break
                # Raise an exception if there is no corresponding fstrip
                else:
                    raise Exception(
                        f"ERROR: There is no corresponding shape for {fstrip_itf}, please check your model.")
        return fstrip_pairs 
[docs]    def get_pile_pairs(self) -> namedtuple:
        """ Method of 'GeoOutput' class to match the pile name and its nodes."""
        Pile_pair = namedtuple(
            'Pile_pair', ['pile_obj', 'pile_mesh_node', 'vertical_spring', 'spring_top_node'])
        pile_pairs = []
        pile_list = [
            element for element in self.get_pile_elements()
            if 'FCRIT' not in element['object'].name and 'ROT' not in element['object'].name]
        for pile in pile_list:
            pile_mesh_node = None
            vertical_spring = None
            spring_top_node = None
            for node in pile['object'].mesh.get_meshnodes():
                if fem_compare_values(node.coordinates[2], pile['z_max']):
                    pile_mesh_node = node
                    break
            if pile['vertical spring'] is not None:
                vertical_spring = pile['vertical spring']
                spring_top_node = sorted(
                    pile['vertical spring'].mesh.get_meshnodes(), key=lambda x: x.coordinates[2])[-1]
            pile_pairs.append(Pile_pair(
                pile_obj=pile['object'], pile_mesh_node=pile_mesh_node,
                vertical_spring=vertical_spring, spring_top_node=spring_top_node))
            
            # Raise an exception if looped through and cannot find nodes
            if pile_mesh_node is None:
                raise Exception(
                    f"There is no corresponding node for pile {pile['object'].name}, please check your model")
        return pile_pairs 
[docs]    def get_forces_from_abaqus(self, results_file: Path):
        """
        Method to load forces per strip or per pile from a ABAQUS results json-file into the GeoOutput class.
        Input:
            results_file (Path): Path to json-file containing force output.
        """
        # Type should be immutable, avoid nested dictionary
        Fstrip_data = namedtuple('Fstrip_data', ['fstrip', 'forces'])
        Pile_data = namedtuple('Pile_data', ['pile', 'forces'])
        with open(results_file, 'r') as f:
            data = json.load(f)
        force_output = data.get('section_force_requests', None)
        if not force_output:
            self.project.write_log(f"ERROR: Section force requests were not found in file '{results_file.as_posix()}'.")
            return self.forces_foundation_elements['piles'], self.forces_foundation_elements['fstrips']
        for item in force_output['items']:
            forces_per_phase = []
            result_keys = item['force']['3']
            for phase in result_keys:
                forces_per_load_step = {'x': {}, 'y': {}, 'z': {}, 'time': {}}
                for time, n in zip(result_keys[phase], range(len(list(result_keys[phase])))):
                    forces_per_load_step['x'][n] = list(item['force']['1'][phase][time].values())
                    forces_per_load_step['y'][n] = list(item['force']['2'][phase][time].values())
                    forces_per_load_step['z'][n] = list(item['force']['3'][phase][time].values())
                    forces_per_load_step['time'][n] = time
                forces_per_phase.append(forces_per_load_step)
            # Name change check
            if 'rhdhv_fem' in item:
                raise ValueError(
                    "ERROR: The results are loaded from an old version of the package. Please update the package "
                    "before proceeding. The item in the dictionary should be renamed from 'rhdhv_fem' to 'fem'. "
                    "Contact the automation team for assistance.")
            # Write to the namedtuple
            if 'PAAL' in item['fem']['shape_name']:
                shape_type = 'piles'
                self.forces_foundation_elements[shape_type].append(Pile_data(
                    pile=self.project.viia_get(name=item['fem']['shape_name']),
                    forces=forces_per_phase))
            else:
                shape_type = 'fstrips'
                self.forces_foundation_elements[shape_type].append(Fstrip_data(
                    fstrip=self.project.viia_get(name=item['fem']['shape_name']),
                    forces=forces_per_phase))
        return self.forces_foundation_elements['piles'], self.forces_foundation_elements['fstrips'] 
[docs]    def get_pile_displacements_from_abaqus(self, results_file: Path):
        """
        Method to load displacements per pile from a ABAQUS results json-file into the GeoOutput class.
        Input:
            - results_file (Path): Path to json-file containing pile displacements.
        """
        # Type should be immutable, avoid nested dictionary
        Pile_data = namedtuple('Pile_data', ['pile', 'displacements'])
        with open(results_file, 'r') as f:
            data = json.load(f)
        pile_displacement_output = data.get('displacement_requests', None)
        if not pile_displacement_output:
            self.project.write_log(f"ERROR: Displacement requests were not found in file '{results_file.as_posix()}'.")
            return self.displacements_foundation_elements['piles']
        for item in pile_displacement_output['items']:
            displacement_per_phase = []
            result_keys = item['displacement']['3']
            for phase in result_keys:
                displacement_per_load_step = {'x': {}, 'y': {}, 'z': {}, 'time': {}}
                for time, n in zip(result_keys[phase], range(len(list(result_keys[phase])))):
                    displacement_per_load_step['x'][n] = list(item['displacement']['1'][phase][time].values())
                    displacement_per_load_step['y'][n] = list(item['displacement']['2'][phase][time].values())
                    displacement_per_load_step['z'][n] = list(item['displacement']['3'][phase][time].values())
                    displacement_per_load_step['time'][n] = time
                displacement_per_phase.append(displacement_per_load_step)
            
            # Write to the namedtuple
            if 'PAAL' in item['fem']['shape_name']:
                shape_type = 'piles'
            else:
                raise ValueError("ERROR: The foundation type is not piles.")
            self.displacements_foundation_elements[shape_type].append(Pile_data(
                pile=self.project.viia_get(name=item['fem']['shape_name']),
                displacements=displacement_per_phase))
        return self.displacements_foundation_elements['piles'] 
[docs]    def get_forces_from_diana(self, tb_file: Path, data_type: str, analysis: Analysis):
        """
        Method to load forces per strip or per pile from a DIANA tb-file into the GeoOutput class.
        Input:
            - tb_file (Path): Path to tb-file containing force output results.
            - analysis (obj): Object reference of analysis of the results in the tb-file.
        """
        def check_analysis_references(analysis_references_1, analysis_references_2, analysis_references_3):
            """ Check the analysis_references """
            def sort_analysis_references(analysis_references_list: List[SteppedAnalysisReference]):
                analysis_references = []
                for calculation_block in analysis_references_list[0].analysis.calculation_blocks:
                    analysis_reference_per_block = []
                    for analysis_reference in analysis_references_list:
                        if analysis_reference.calculation_block == calculation_block:
                            analysis_reference_per_block.append(analysis_reference)
                    analysis_references.extend(sorted(analysis_reference_per_block, key=lambda x: x.step_nr))
                return analysis_references
            if not (len(analysis_references_1) == len(analysis_references_2) == len(analysis_references_3)) or \
                
not all([analysis_references in analysis_references_1
                        for analysis_references in analysis_references_2]) or \
                
not all(analysis_references in analysis_references_1
                        for analysis_references in analysis_references_3) or \
                
not all(analysis_references in analysis_references_2
                        for analysis_references in analysis_references_1) or \
                
not all(analysis_references in analysis_references_2
                        for analysis_references in analysis_references_3) or \
                
not all(analysis_references in analysis_references_3
                        for analysis_references in analysis_references_1) or \
                
not all(analysis_references in analysis_references_3
                        for analysis_references in analysis_references_2):
                raise ValueError("ERROR: Ambigous analysis references")
            if all(isinstance(analysis_reference, SteppedAnalysisReference)
                   for analysis_reference in analysis_references_1):
                analysis_references_1 = sort_analysis_references(analysis_references_list=analysis_references_1)
            if all(isinstance(analysis_reference, SteppedAnalysisReference)
                   for analysis_reference in analysis_references_2):
                analysis_references_2 = sort_analysis_references(analysis_references_list=analysis_references_2)
            if all(isinstance(analysis_reference, SteppedAnalysisReference)
                   for analysis_reference in analysis_references_3):
                analysis_references_3 = sort_analysis_references(analysis_references_list=analysis_references_3)
            return analysis_references_1, analysis_references_2, analysis_references_3
        def get_forces_from_diana_piles(analysis_obj):
            pile_pairs = self.get_pile_pairs()
            Pile_data = namedtuple('Pile_data', ['pile', 'forces'])
            software = pile_pairs[0].pile_obj.results.get_softwares()[0]
            output_items = pile_pairs[0].pile_obj.results.get_output_items()
            FNTX, FNTY, FNTZ = None, None, None
            for output_item in output_items:
                diana_abbreviation = output_item.get_diana_abbreviation(
                    analysis_block=analysis_obj.get_all_analysis_blocks()[-1], item=pile_pairs[0].pile_obj)
                if 'FNTX' == diana_abbreviation:
                    FNTX = output_item
                elif 'FNTY' == diana_abbreviation:
                    FNTY = output_item
                elif 'FNTZ' == diana_abbreviation:
                    FNTZ = output_item
            if FNTX is None or FNTY is None or FNTZ is None:
                raise ValueError("ERROR: Output items are not found.")
            analysis_references_1 = pile_pairs[0].pile_obj.results.get_analysis_references(output_item=FNTX)
            analysis_references_2 = pile_pairs[0].pile_obj.results.get_analysis_references(output_item=FNTY)
            analysis_references_3 = pile_pairs[0].pile_obj.results.get_analysis_references(output_item=FNTZ)
            analysis_references_1, analysis_references_2, analysis_references_3 = \
                
check_analysis_references(analysis_references_1, analysis_references_2, analysis_references_3)
            # Get sorted phases
            for pile_pair in pile_pairs:
                # Get results per phase per time step
                forces_per_phase = []
                forces_per_load_step = {'x': {}, 'y': {}, 'z': {}, 'time': {}}
                for index in range(len(analysis_references_1)):
                    # Only consider last phase
                    if analysis_references_1[index].analysis.calculation_blocks[-1] != \
                            
analysis_references_1[index].calculation_block:
                        continue
                    result_location = pile_pair.pile_obj
                    forces_per_load_step['x'][analysis_references_1[index].step_nr] = \
                        
result_location.results.get_result_value(
                            output_item=FNTX, analysis_reference=analysis_references_1[index], software=software,
                            mesh_element=result_location.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node)
                    forces_per_load_step['y'][analysis_references_1[index].step_nr] = \
                        
result_location.results.get_result_value(
                            output_item=FNTY, analysis_reference=analysis_references_1[index], software=software,
                            mesh_element=result_location.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node)
                    forces_per_load_step['z'][analysis_references_1[index].step_nr] = \
                        
result_location.results.get_result_value(
                            output_item=FNTZ, analysis_reference=analysis_references_1[index], software=software,
                            mesh_element=result_location.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node)
                    if analysis_references_1[index].meta_data is not None:
                        forces_per_load_step['time'][analysis_references_1[index].step_nr] = \
                            
analysis_references_1[index].meta_data.get('time', None)
                    else:
                        forces_per_load_step['time'][analysis_references_1[index].step_nr] = None
                    forces_per_phase.append(forces_per_load_step)
                # Write to the namedtuple
                self.forces_foundation_elements["piles"].append(Pile_data(pile=pile_pair.pile_obj,
                                                                          forces=forces_per_phase))
        def get_forces_from_diana_fstrips():
            # Loop through all the fstrips
            fstrip_pairs = self.get_fstrip_pairs()
            # Type should be immutable, avoid nested dictionary
            Fstrip_data = namedtuple('Fstrip_data', ['fstrip', 'forces'])
            # Get the output by type
            software = fstrip_pairs[0].fstrip_itf.results.get_softwares()[0]
            output_items = fstrip_pairs[0].fstrip_itf.results.get_output_items()
            STSx, STSy, STNz = None, None, None
            for output_item in output_items:
                if output_item.engineering_notation == 't_x_tot_intp':
                    STSx = output_item
                elif output_item.engineering_notation == 't_y_tot_intp':
                    STSy = output_item
                elif output_item.engineering_notation == 't_z_tot_intp':
                    STNz = output_item
            if STSx is None or STSy is None or STNz is None:
                raise ValueError("ERROR: Output-items for the geo-output could not be found.")
            analysis_references_1 = fstrip_pairs[0].fstrip_itf.results.get_analysis_references(output_item=STSx)
            analysis_references_2 = fstrip_pairs[0].fstrip_itf.results.get_analysis_references(output_item=STSy)
            analysis_references_3 = fstrip_pairs[0].fstrip_itf.results.get_analysis_references(output_item=STNz)
            analysis_references_1, analysis_references_2, analysis_references_3 = \
                
check_analysis_references(analysis_references_1, analysis_references_2, analysis_references_3)
            # Loop through fstrips
            for fstrip_pair in fstrip_pairs:
                result_location = fstrip_pair.fstrip_itf
                forces_per_phase = []
                forces_per_load_step = {'x': {}, 'y': {}, 'z': {}, 'time': {}}
                for index in range(len(analysis_references_1)):
                    # Only consider last phase
                    if analysis_references_1[index].analysis.calculation_blocks[-1] != \
                            
analysis_references_1[index].calculation_block:
                        continue
                    forces_per_load_step['x'][analysis_references_1[index].step_nr] = 0
                    forces_per_load_step['y'][analysis_references_1[index].step_nr] = 0
                    forces_per_load_step['z'][analysis_references_1[index].step_nr] = 0
                    # Loop through all mesh elements of the interfaces
                    for mesh_element in result_location.mesh.get_meshelements():
                        element_area = mesh_element.get_area()
                        x = result_location.results.get_result_value(
                                output_item=STSx, analysis_reference=analysis_references_1[index], software=software,
                                mesh_element=mesh_element)
                        y = result_location.results.get_result_value(
                                output_item=STSy, analysis_reference=analysis_references_1[index], software=software,
                                mesh_element=mesh_element)
                        z = result_location.results.get_result_value(
                                output_item=STNz, analysis_reference=analysis_references_1[index], software=software,
                                mesh_element=mesh_element)
                        if len(x) not in [3, 4] or len(y) not in [3, 4] or len(z) not in [3, 4]:
                            raise ValueError(
                                f"ERROR: Only linear triangular or quadrilateral elements can be processed.")
                        # Add force of mesh element to step
                        forces_per_load_step['x'][analysis_references_1[index].step_nr] += \
                            
element_area * sum(x.values()) / len(x)
                        forces_per_load_step['y'][analysis_references_1[index].step_nr] += \
                            
element_area * sum(y.values()) / len(y)
                        forces_per_load_step['z'][analysis_references_1[index].step_nr] += \
                            
element_area * sum(z.values()) / len(z)
                    if analysis_references_1[index].meta_data is not None:
                        forces_per_load_step['time'][analysis_references_1[index].step_nr] = \
                            
analysis_references_1[index].meta_data.get('time', None)
                    else:
                        forces_per_load_step['time'][analysis_references_1[index].step_nr] = None
                    forces_per_phase.append(forces_per_load_step)
                # Write to the namedtuple
                self.forces_foundation_elements['fstrips'].append(Fstrip_data(
                    fstrip=fstrip_pair.fstrip_obj, forces=forces_per_phase))
        # Read result
        self.project.read_diana_tbfile(file=tb_file, analysis=analysis)
        # Retrieve fstrip data
        if data_type == 'fstrips':
            get_forces_from_diana_fstrips()
        # Retrieve pile data
        elif data_type == 'piles':
            get_forces_from_diana_piles(analysis_obj=analysis)
        return self.forces_foundation_elements['piles'], self.forces_foundation_elements['fstrips'] 
[docs]    def get_pile_displacements_from_diana(self, analysis: Analysis, tb_file: Optional[Path] = None):
        """
        Method to load displacement per pile from a DIANA tb-file into the GeoOutput class.
        Input:
            - tb_file (Path): path to tb file containing force output. If tb-file is already read somewhere else None
              should be given so the reading is skipped.
        Output:
            - Returns the displacement for each pile for all the time steps.
        """
        pile_pairs = self.get_pile_pairs()
        Pile_displacement = namedtuple('Pile_displacement', ['pile', 'x', 'y', 'z', 'time_step'])
        # Read result
        if tb_file is not None:
            tb_file_diana = self.project.read_diana_tbfile(file=tb_file)
        software = pile_pairs[0].pile_obj.results.get_softwares()[0]
        analysis_block = analysis.get_all_analysis_blocks()[-1]
        if pile_pairs[0].vertical_spring:
            output_items = pile_pairs[0].vertical_spring.results.get_output_items()
        else:
            output_items = pile_pairs[0].pile_obj.results.get_output_items()
        TDtX, TDtY, TDtZ = None, None, None
        for output_item in output_items:
            diana_abbreviation = output_item.get_diana_abbreviation(
                analysis_block=analysis_block, execute_block=analysis_block.execute_blocks[-1],
                item=pile_pairs[0].pile_obj)
            if 'TDtX' == diana_abbreviation:
                TDtX = output_item
            elif 'TDtY' == diana_abbreviation:
                TDtY = output_item
            elif 'TDtZ' == diana_abbreviation:
                TDtZ = output_item
        if TDtX is None or TDtY is None or TDtZ is None:
            raise ValueError("ERROR: Output items are not found.")
        analysis_references = []
        vertical_spring = False
        if pile_pairs[0].vertical_spring:
            vertical_spring = True
            analysis_references = pile_pairs[0].vertical_spring.results.get_analysis_references(
                output_item=TDtX, software=software)
        else:
            analysis_references = pile_pairs[0].pile_obj.results.get_analysis_references(
                output_item=TDtX, software=software)
        if not analysis_references:
            raise ValueError("ERROR: Analysis references are not found.")
        if not all(isinstance(analysis_reference, SteppedAnalysisReference)
                   for analysis_reference in analysis_references):
            raise TypeError("ERROR: Not all analysis_references are of type SteppedAnalysisReference.")
        analysis_references = sorted(analysis_references, key=lambda x: x.step_nr)
        for pile_pair in pile_pairs:
            disp_x = []
            disp_y = []
            disp_z = []
            time_step = []
            for analysis_reference in analysis_references:
                if vertical_spring:
                    # take difference
                    disp_x.append(pile_pair.vertical_spring.results.get_result_value(
                        output_item=TDtX, analysis_reference=analysis_reference, software=software,
                        mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node) -
                                  pile_pair.vertical_spring.results.get_result_value(
                        output_item=TDtX, analysis_reference=analysis_reference, software=software,
                        mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0], mesh_node=pile_pair.spring_top_node))
                    disp_y.append(pile_pair.vertical_spring.results.get_result_value(
                        output_item=TDtY, analysis_reference=analysis_reference, software=software,
                        mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node) -
                                  pile_pair.vertical_spring.results.get_result_value(
                                 output_item=TDtY, analysis_reference=analysis_reference, software=software,
                                 mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0],
                                 mesh_node=pile_pair.spring_top_node))
                    disp_z.append(pile_pair.vertical_spring.results.get_result_value(
                        output_item=TDtZ, analysis_reference=analysis_reference, software=software,
                        mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0], mesh_node=pile_pair.pile_mesh_node) -
                                  pile_pair.vertical_spring.results.get_result_value(
                                 output_item=TDtZ, analysis_reference=analysis_reference, software=software,
                                 mesh_element=pile_pair.vertical_spring.mesh.mesh_elements[0],
                                 mesh_node=pile_pair.spring_top_node))
                    if analysis_reference.meta_data is not None:
                        time_step.append(analysis_reference.meta_data['time'])
                    else:
                        time_step.append(None)
                else:
                    raise NotImplementedError("ERROR: 5B output with only displacements ar top node of pile are not "
                                              "implemented anymore. Displacements at top and bottom of pile springs "
                                              "should be present.")
            self.displacements_foundation_elements['piles'].append(Pile_displacement(
                pile=pile_pair.pile_obj, x=disp_x, y=disp_y, z=disp_z, time_step=time_step))
        return self.displacements_foundation_elements['piles'] 
[docs]    def get_viia_object_address(self) -> str:
        """ Retrieve the address for the object."""
        if 'object_adressen' in list(self.project.project_information.keys()):
            return \
                
f"{self.project.project_information['object_adressen'][0]['straatnaam']} " \
                
f"{self.project.project_information['object_adressen'][0]['huisnummer']} in " \
                
f"{self.project.project_information['object_adressen'][0]['plaatsnaam']}"
        return 'No address obtained from MYVIIA'  
### ===================================================================================================================
###    3. End of script
### ===================================================================================================================