Source code for queens.external_geometry.fourc_dat_geometry

#
# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (c) 2024-2025, QUEENS contributors.
#
# This file is part of QUEENS.
#
# QUEENS is free software: you can redistribute it and/or modify it under the terms of the GNU
# Lesser General Public License as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version. QUEENS is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You
# should have received a copy of the GNU Lesser General Public License along with QUEENS. If not,
# see <https://www.gnu.org/licenses/>.
#
"""4C geometry handling."""

# pylint: disable=too-many-lines
import copy
import fileinput
import os
import re
import shutil

import numpy as np

from queens.external_geometry.external_geometry import ExternalGeometry
from queens.utils.logger_settings import log_init_args


[docs] class FourcDatExternalGeometry(ExternalGeometry): """Class to read in external geometries based on 4C dat files. Attributes: path_to_dat_file (str): Path to dat file from which the *external_geometry_obj* should be extracted. path_to_preprocessed_dat_file (str): Path to preprocessed dat file with added placeholders. coords_dict (str): Dictionary containing coordinates of the discretized random fields and corresponding placeholder names. list_geometric_sets (lst): List of geometric sets that should be extracted. current_dat_section (str): String that encodes the current section in the dat file as file is read line-wise. desired_dat_sections (dict): Dictionary that holds only desired dat-sections and geometric sets within these sections, so that we can skip undesired parts of the dat-file. nodes_of_interest (lst): List that contains all (mesh) nodes that are part of a desired geometric component. new_nodes_lst (lst): List of new nodes that should be written in dnode topology. node_topology (lst): List with topology dicts of edges/nodes (here: mesh nodes not FEM nodes) for each geometric set of this category. line_topology (lst): List with topology dicts of line components for each geometric set of this category. surface_topology (lst): List of topology dicts of surfaces for each geometric set of this category. volume_topology (lst): List of topology of volumes for each geometric set of this category. node_coordinates (dict): Dictionary that holds the desired nodes as well as their corresponding geometric coordinates. element_centers (np.array): Array with center coordinates of elements. element_topology (lst): List of dictionaries containing element topology, including node mapping, materials, and element numbers. original_materials_in_dat (lst): List of original material numbers in dat template file. list_associated_material_numbers (lst): List of associated material numbers w.r.t. the geometric sets of interest. new_material_numbers (lst): List of new material numbers to be used. random_dirich_flag (bool): Flag to check if a random Dirichlet BC exists. random_transport_dirich_flag (bool): Flag to check if a random transport Dirichlet BC exists. random_neumann_flag (bool): Flag to check if a random Neumann BC exists. nodes_written (bool): Flag to check whether nodes have already been written. random_fields (lst): List of random field descriptions. Returns: geometry_obj (obj): Instance of FourcDatExternalGeometry class """ dat_sections = [ "DESIGN DESCRIPTION", "DESIGN POINT DIRICH CONDITIONS", "DESIGN POINT TRANSPORT DIRICH CONDITIONS", "DNODE-NODE TOPOLOGY", "DLINE-NODE TOPOLOGY", "DSURF-NODE TOPOLOGY", "DVOL-NODE TOPOLOGY", "NODE COORDS", "STRUCTURE ELEMENTS", "ALE ELEMENTS", "FLUID ELEMENTS", "LUBRIFICATION ELEMENTS", "TRANSPORT ELEMENTS", "TRANSPORT2 ELEMENTS", "THERMO ELEMENTS", "CELL ELEMENTS", "CELLSCATRA ELEMENTS", "ELECTROMAGNETIC ELEMENTS", "ARTERY ELEMENTS", "MATERIALS", ] section_match_dict = { "DNODE": "DNODE-NODE TOPOLOGY", "DLINE": "DLINE-NODE TOPOLOGY", "DSURFACE": "DSURF-NODE TOPOLOGY", "DVOL": "DVOL-NODE TOPOLOGY", } @log_init_args def __init__( self, input_template, input_template_preprocessed=None, list_geometric_sets=None, associated_material_numbers_geometric_set=None, random_fields=None, ): """Initialize 4C external geometry. Args: input_template (str): Path to dat file from which the external_geometry_obj should be extracted input_template_preprocessed (str): Path to preprocessed dat file with added placeholders list_geometric_sets (list): List of geometric sets that should be extracted associated_material_numbers_geometric_set (lst): List of associated material numbers wrt to the geometric sets of interest random_fields (lst): List of random field descriptions """ super().__init__() # settings / inputs self.path_to_dat_file = input_template self.path_to_preprocessed_dat_file = input_template_preprocessed self.coords_dict = {} self.list_geometric_sets = list_geometric_sets self.current_dat_section = None self.desired_dat_sections = {"DNODE-NODE TOPOLOGY": []} self.nodes_of_interest = None self.new_nodes_lst = None # topology self.node_topology = [{"node_mesh": [], "node_topology": [], "topology_name": ""}] self.line_topology = [{"node_mesh": [], "line_topology": [], "topology_name": ""}] self.surface_topology = [{"node_mesh": [], "surface_topology": [], "topology_name": ""}] self.volume_topology = [{"node_mesh": [], "volume_topology": [], "topology_name": ""}] self.node_coordinates = {"node_mesh": [], "coordinates": []} self.nodeset_names = set() # material specific attributes self.element_centers = [] self.element_topology = [{"element_number": [], "nodes": [], "material": []}] self.original_materials_in_dat = [] self.list_associated_material_numbers = associated_material_numbers_geometric_set self.new_material_numbers = [] # some flags to memorize which sections have been written / manipulated self.random_dirich_flag = False self.random_transport_dirich_flag = False self.random_neumann_flag = False self.nodes_written = False self.random_fields = random_fields # --------------- child methods that must be implemented --------------------------------------
[docs] def read_external_data(self): """Read the external input file with geometric data.""" self.read_geometry_from_dat_file()
[docs] def organize_sections(self): """Organize the sections of the external *external_geometry_obj*.""" self.get_desired_dat_sections()
[docs] def finish_and_clean(self): """Finish the analysis for the *external_geometry_obj* extraction.""" self._sort_node_coordinates() self._get_element_centers()
# -------------- helper methods ---------------------------------------------------------------
[docs] def read_geometry_from_dat_file(self): """Read the dat-file line by line to be memory efficient.""" with open(self.path_to_dat_file, encoding="utf-8") as my_dat: # read dat file line-wise for line in my_dat: line = line.strip() match_bool = self.get_current_dat_section(line) # skip comments outside of section definition if ( line[0:2] == "//" or match_bool or line == "" or line.isspace() or self.current_dat_section is None ): pass else: self.get_only_desired_topology(line) self.get_only_desired_coordinates(line) self.get_materials(line) self.get_elements_belonging_to_desired_material(line)
def _get_element_centers(self): """Calculate the geometric center of each finite element.""" element_centers = [] # TODO atm we only take first element of the list and dont loop over several random fields # pylint: disable=fixme for element_node_lst in self.element_topology[0]["nodes"]: element_center = np.array([0.0, 0.0, 0.0]) for node in element_node_lst: element_center += np.array( self.node_coordinates["coordinates"][ self.node_coordinates["node_mesh"].index(int(node)) ] ) element_centers.append(element_center / float(len(element_node_lst))) self.element_centers = np.array(element_centers)
[docs] def get_current_dat_section(self, line): """Check if the current line starts a new section in the dat-file. Update self.current_dat_section if new section was found. If the current line is the actual section identifier, return a True boolean. Args: line (str): Current line of the dat-file Returns: bool (boolean): True or False depending if current line is the section match """ # regex for the sections section_name_re = re.compile("^-+([^-].+)$") match = section_name_re.match(line) # get the current section of the dat file if match: # remove whitespaces and horizontal line section_string = line.strip("-") section_string = section_string.strip() # check for comments if line[:2] == "//": return True # ignore comment pattern after actual string if section_string.strip("//") in self.dat_sections: self.current_dat_section = section_string return True self.current_dat_section = None return True return False
[docs] def check_if_in_desired_dat_section(self): """Check if the dat-section contains the desired geometric set. Returns: bool: True if the case is found, False otherwise. """ return self.current_dat_section in self.desired_dat_sections
[docs] def get_desired_dat_sections(self): """Get the dat-sections that contain the desired geometric sets.""" # initialize keys with empty lists for geo_set in self.list_geometric_sets: # TODO for now we read in all element_topology; this should be changed such that we only # pylint: disable=fixme # store element_topology that belong to the desired geometric set (difficult though as # we would need to look-up the attached nodes and check their set assignment if geo_set.split()[1] == "ELEMENTS": self.desired_dat_sections[geo_set] = [] else: self.desired_dat_sections[self.section_match_dict[geo_set.split()[0]]] = [] # write desired geometric set to corresponding section key for geo_set in self.list_geometric_sets: if geo_set.split()[1] == "ELEMENTS": self.desired_dat_sections[geo_set].append(geo_set) else: self.desired_dat_sections[self.section_match_dict[geo_set.split()[0]]].append( geo_set )
[docs] def get_elements_belonging_to_desired_material(self, line): """Get finite element_topology that belongs to the material definition. Note that we assume that the geometric set of interest also has its own material name. This speeds-up the element-topology mapping significantly as we do not have to find which element belongs to which geometric set by performing a large node membership search per element and for all nodes of the element. On the other hand this assumption requires that the analyst provides a dat-file in the correct format. Args: line (str): Current line in the dat-file """ # Note that the latter applies to all element sections! if "ELEMENTS" in self.current_dat_section and self.list_associated_material_numbers: material_number = int(line.split("MAT")[1].split()[0]) # TODO atm we only can handle one material--> this should be changed to a list of # pylint: disable=fixme # original_materials_in_dat if material_number == self.list_associated_material_numbers[0][0]: # get the element number self.element_topology[0]["element_number"].append(int(line.split()[0])) # get the nodes per element helper_list = line.split("MAT") nodes_list_str = helper_list[0].split()[3:] nodes = [int(node) for node in nodes_list_str] # below appends a list in a list of lists such that we keep the nodes per element self.element_topology[0]["nodes"].append(nodes) # the first number in the list is the main material the subsequent number the # nested material self.element_topology[0]["material"].append(material_number)
[docs] def get_materials(self, line): """Get the different material definitions from the dat file. Args: line (str): Current line of the dat file """ if self.current_dat_section == "MATERIALS": self.original_materials_in_dat.append(int(line.split()[1]))
# pylint: disable=too-many-branches
[docs] def get_topology(self, line): """Get the geometric topology by extracting and grouping (mesh) nodes. Only nodes that belong to the desired geometric sets and save them to their topology class. Args: line (str): Current line of the dat-file """ topology_name = " ".join(line.split()[2:4]) node_list = line.split() # get edges if self.current_dat_section == "DNODE-NODE TOPOLOGY": self.nodeset_names.add(int(line.split("DNODE ")[-1])) if topology_name in self.desired_dat_sections["DNODE-NODE TOPOLOGY"]: if (self.node_topology[-1]["topology_name"] == "") or ( self.node_topology[-1]["topology_name"] == topology_name ): self.node_topology[-1]["node_mesh"].append(int(node_list[1])) self.node_topology[-1]["node_topology"].append(int(node_list[3])) self.node_topology[-1]["topology_name"] = topology_name else: new_node_topology_dict = { "node_mesh": int(node_list[1]), "node_topology": int(node_list[3]), "topology_name": topology_name, } self.node_topology.extend(new_node_topology_dict) # get lines elif self.current_dat_section == "DLINE-NODE TOPOLOGY": if topology_name in self.desired_dat_sections["DLINE-NODE TOPOLOGY"]: if (self.line_topology[-1]["topology_name"] == "") or ( self.line_topology[-1]["topology_name"] == topology_name ): self.line_topology[-1]["node_mesh"].append(int(node_list[1])) self.line_topology[-1]["line_topology"].append(int(node_list[3])) self.line_topology[-1]["topology_name"] = topology_name else: new_line_topology_dict = { "node_mesh": int(node_list[1]), "line_topology": int(node_list[3]), "topology_name": topology_name, } self.line_topology.extend(new_line_topology_dict) # get surfaces elif self.current_dat_section == "DSURF-NODE TOPOLOGY": if topology_name in self.desired_dat_sections["DSURF-NODE TOPOLOGY"]: # append points to last list entry if geometric set is the name if (self.surface_topology[-1]["topology_name"] == "") or ( self.surface_topology[-1]["topology_name"] == topology_name ): self.surface_topology[-1]["node_mesh"].append(int(node_list[1])) self.surface_topology[-1]["surface_topology"].append(int(node_list[3])) self.surface_topology[-1]["topology_name"] = topology_name # extend list with new geometric set else: new_surf_topology_dict = { "node_mesh": int(node_list[1]), "surface_topology": int(node_list[3]), "topology_name": topology_name, } self.surface_topology.extend(new_surf_topology_dict) # get volumes elif self.current_dat_section == "DVOL-NODE TOPOLOGY": if topology_name in self.desired_dat_sections["DVOL-NODE TOPOLOGY"]: # append points to last list entry if geometric set is the name if (self.volume_topology[-1]["topology_name"] == "") or ( self.volume_topology[-1]["topology_name"] == topology_name ): self.volume_topology[-1]["node_mesh"].append(int(node_list[1])) self.volume_topology[-1]["volume_topology"].append(int(node_list[3])) self.volume_topology[-1]["topology_name"] = topology_name else: new_volume_topology_dict = { "node_mesh": int(node_list[1]), "surface_topology": int(node_list[3]), "topology_name": topology_name, } self.volume_topology.extend(new_volume_topology_dict)
[docs] def get_only_desired_topology(self, line): """Check if the current line contains desired geometric sets. Skip the topology extraction if the current section does not contain a desired geometric set, anyways. Args: line (str): Current line of the dat-file """ # skip lines that are not part of a desired section desired_section_boolean = self.check_if_in_desired_dat_section() if not desired_section_boolean: pass else: # get topology groups self.get_topology(line)
[docs] def get_only_desired_coordinates(self, line): """Get coordinates of nodes that belong to a desired geometric set. Args: line (str): Current line of the dat-file """ if self.current_dat_section == "NODE COORDS": node_list = line.split() if self.nodes_of_interest is None: self.get_nodes_of_interest() if self.nodes_of_interest is not None: if int(node_list[1]) in self.nodes_of_interest: self.get_coordinates_of_desired_geometric_sets(node_list)
[docs] def get_coordinates_of_desired_geometric_sets(self, node_list): """Extract node and coordinate information of the current line. Args: node_list (list): Current line of the dat-file """ self.node_coordinates["node_mesh"].append(int(node_list[1])) nodes_as_float_list = [float(value) for value in node_list[3:6]] self.node_coordinates["coordinates"].append(nodes_as_float_list)
[docs] def get_nodes_of_interest(self): """From the extracted topology, get a unique list of all nodes.""" node_mesh_nodes = [] for node_topo in self.node_topology: node_mesh_nodes.extend(node_topo["node_mesh"]) line_mesh_nodes = [] for line_topo in self.line_topology: line_mesh_nodes.extend(line_topo["node_mesh"]) surf_mesh_nodes = [] for surf_topo in self.surface_topology: surf_mesh_nodes.extend(surf_topo["node_mesh"]) vol_mesh_nodes = [] for vol_topo in self.volume_topology: vol_mesh_nodes.extend(vol_topo["node_mesh"]) element_material_nodes = [] for element_topo in self.element_topology: element_material_nodes.extend(element_topo["nodes"]) nodes_of_interest = ( node_mesh_nodes + line_mesh_nodes + surf_mesh_nodes + vol_mesh_nodes + element_material_nodes ) # make node_list unique self.nodes_of_interest = list(set(nodes_of_interest))
def _sort_node_coordinates(self): """Sort node coordinates based on the node mesh.""" self.node_coordinates["coordinates"] = [ coord for _, coord in sorted( zip(self.node_coordinates["node_mesh"], self.node_coordinates["coordinates"]), key=lambda pair: pair[0], ) ] self.node_coordinates["node_mesh"] = sorted(self.node_coordinates["node_mesh"]) # -------------- write random fields to dat file ---------------------------------------------- # pylint: disable=too-many-branches
[docs] def write_random_fields_to_dat(self): """Write placeholders for random fields to the dat file.""" # copy the dat file and rename it for the current simulation shutil.copy2(self.path_to_dat_file, self.path_to_preprocessed_dat_file) # this has to be done outside of the file read as order is not known a priori self._create_new_node_sets(self.random_fields) # potentially organize new material definitions self._organize_new_material_definitions() # save original file ownership details stat = os.stat(self.path_to_dat_file) uid, gid = stat[4], stat[5] # random_fields_lst is here a list containing a dict description per random field with fileinput.input( self.path_to_preprocessed_dat_file, inplace=True, backup=".bak" ) as my_dat: # read dat file line-wise for line in my_dat: old_line = line line = line.strip() match_bool = self.get_current_dat_section(line) # skip comments outside of section definition if line[0:2] == "//" or match_bool or line.isspace() or line == "": print(old_line, end="") else: # check if in sec. DNODE-NODE topology and if so adjust this section in case # of random BCs; write this only once if self.current_dat_section == "DNODE-NODE TOPOLOGY" and not self.nodes_written: self._write_new_node_sets() self.nodes_written = True # print the current line that was overwritten print(old_line, end="") # check if in sec. for random dirichlet cond. and if point dirich exists extend elif ( self.current_dat_section == "DESIGN POINT DIRICH CONDITIONS" and not self.random_dirich_flag ): self._write_design_point_dirichlet_conditions(self.random_fields, line) self.random_dirich_flag = True elif ( self.current_dat_section == "DESIGN POINT TRANSPORT DIRICH CONDITIONS" and not self.random_transport_dirich_flag ): self._write_design_point_dirichlet_transport_conditions() self.random_transport_dirich_flag = True elif ( self.current_dat_section == "DESIGN POINT NEUM" and not self.random_neumann_flag ): self._write_design_point_neumann_conditions() self.random_neumann_flag = True # materials and elements / constitutive random fields ----------------------- elif self.current_dat_section == "STRUCTURE ELEMENTS": self._assign_elementwise_material_to_structure_ele(line) elif self.current_dat_section == "FLUID ELEMENTS": raise NotImplementedError() elif self.current_dat_section == "ALE ELEMENTS": raise NotImplementedError() elif self.current_dat_section == "TRANSPORT ELEMENTS": raise NotImplementedError() elif ( self.current_dat_section == "MATERIALS" and self.list_associated_material_numbers ): self._write_elementwise_materials(line, self.random_fields) # If end of dat file is reached but certain sections did not exist so far, # write them now elif self.current_dat_section == "END": bcs_list = [random_field["type"] for random_field in self.random_fields] if ("dirichlet" in bcs_list) and not self.random_dirich_flag: print( "----------------------------------------------DESIGN POINT " "DIRICH CONDITIONS\n" ) self._write_design_point_dirichlet_conditions(self.random_fields, line) elif ("transport_dirichlet" in bcs_list) and ( not self.random_transport_dirich_flag ): print( "----------------------------------------------DESIGN POINT " "TRANSPORT DIRICH CONDITIONS\n" ) self._write_design_point_dirichlet_transport_conditions() elif ("neumann" in bcs_list) and not self.random_neumann_flag: print( "---------------------------------------------DESIGN POINT " "NEUMANN CONDITIONS\n" ) self._write_design_point_neumann_conditions() print( "---------------------------------------------------------------------" "----END\n" ) else: print(old_line, end="") os.chown(self.path_to_preprocessed_dat_file, uid, gid)
# pylint: enable=too-many-branches # ------ write random material fields ----------------------------------------------- def _organize_new_material_definitions(self): """Organize and generate new material definitions. The new material naming continues from the maximum material number that was found in the dat file. """ # Material definitions -> these are the mat numbers # note that this is a list of lists materials_copy = copy.copy(self.original_materials_in_dat) # remove materials of interest and nested dependencies from material copy # note we remove here the entire sublist that belongs to a geometric set of interest if self.list_associated_material_numbers: for associated_materials in self.list_associated_material_numbers: for material in associated_materials: materials_copy.remove(material) # set number of new materials equal to length of element_topology of interest # TODO atm we just take the first list entry here and dont loop over several fields # pylint: disable=fixme n_mats = len(self.element_topology[0]["element_number"]) # Check the largest material definition to avoid overwriting existing original_materials material_numbers_flat = [item for sublist in materials_copy for item in sublist] if material_numbers_flat: max_material_number = max(material_numbers_flat) else: max_material_number = int(0) # TODO at the moment we only take the first given materials (list in list of lists) # pylint: disable=fixme # and cannot handle several material definitions # pylint: disable=fixme if len(self.list_associated_material_numbers[0]) == 1: # below is also a list in a list such that we cover the general case of base and # nested material even if in this if branch only the base material is of interest self.new_material_numbers = [ list((max_material_number + 1 + np.arange(0, n_mats, 1)).astype(int)) ] elif len(self.list_associated_material_numbers[0]) == 2: self.new_material_numbers = [ list((max_material_number + 1 + np.arange(0, n_mats, 1)).astype(int)) ] list_nested = list( (self.new_material_numbers[0][-1] + 1 + np.arange(0, n_mats, 1)).astype(int) ) # note this is a list of two lists containing the numbers of the base material # and the nested material separately self.new_material_numbers.append(list_nested) else: raise RuntimeError( "At the moment we can only handle one nested material but you " "provided {len(materials_copy[0])} material nestings. Abort..." ) def _assign_elementwise_material_to_structure_ele(self, line): """Assign and write the new material definitions. One per element in desired geometric set to the associated structure element under the dat section `STRUCTURE ELEMENTS` """ # TODO below we take the first list entry and dont loop for now over lists of list. # pylint: disable=fixme # The first material in the sublist is assumed to be the base material, the following # numbers are associated to nested materials. Hence we take the first number to write the # element if self.list_associated_material_numbers: material_expression = "MAT " + str(self.list_associated_material_numbers[0][0]) if material_expression in line: # Current element number current_element_number = int(line.split()[0]) # TODO atm we just take first list entry below and dont loop over all element # pylint: disable=fixme # topologies get index of current element within element_topology element_idx = self.element_topology[0]["element_number"].index( current_element_number ) # TODO note that new materials is atm only one list not a list of lists as iteration # pylint: disable=fixme # over several topologies is not implemented so far # pylint: disable=fixme # we use first list here as element depend normally on base material new_material_number = self.new_material_numbers[0][element_idx] # exchange material number for new number in structure element new_line = line.replace(material_expression, "MAT " + str(new_material_number)) print(new_line) else: print(line) def _write_elementwise_materials(self, line, random_field_lst): """Write the new material definitions under `MATERIALS`. We have one new material per element that is part of the geometric set where the random field is defined on. Args: line (str): Current line in the dat-file random_field_lst (lst): List containing dictionaries with random field description and realizations per associated geometric set """ current_material_number = int(line.split()[1]) # pylint: disable=fixme # TODO see how to use the random field lst here # TODO but also only address first random_field for now # TODO maybe directly separate the random_field types as different attributes # pylint: enable=fixme # get random fields of type material material_fields = [field for field in random_field_lst if field["type"] == "material"] material_field_placeholders = [ material_fields[0]["name"] + "_" + str(i) for i in range(len(self.element_centers)) ] self._write_coords_to_dict( material_fields[0]["name"], material_field_placeholders, np.array(self.element_centers) ) # check if the current material number is equal to base material and rewrite the base # materials as well as the potentially associated nested materials here # TODO atm we only focus on first sublist and do not iterate over several materials # pylint: disable=fixme if current_material_number == self.list_associated_material_numbers[0][0]: self._write_base_materials(current_material_number, line, material_fields) # in case the current material number is a nested material overwrite now the nested # material block. Now we are in the next line and the base material was already written # TODO note that we again assume only one topology (first sublist) # pylint: disable=fixme elif (current_material_number in self.list_associated_material_numbers[0]) and ( current_material_number != self.list_associated_material_numbers[0][0] ): self._write_nested_materials(line, material_fields) else: print(line) def _write_base_materials(self, current_material_number, line, material_fields): """Write the base materials field elementwise. Also materials without nested dependencies. Args: current_material_number (int): Old/current material number line (str): Current line in the dat-file material_fields (lst): List of dictionaries containing descriptions of material fields """ # Add new material definitions # TODO Note: new_material numbers is atm just a list for the first topology # pylint: disable=fixme for idx, material_num in enumerate(self.new_material_numbers[0]): old_material_expression = "MAT " + str(current_material_number) new_material_expression = "MAT " + str(material_num) # potentially replace material parameter # TODO idx seems to be wrong here # pylint: disable=fixme line_new = FourcDatExternalGeometry._parse_material_value_dependent_on_element_center( line, idx, material_fields ) # TODO check if associated material intends nested material # pylint: disable=fixme if len(self.list_associated_material_numbers[0]) == 1: # Update the old material line/definition new_line = line_new.replace(old_material_expression, new_material_expression) # print the new material definition print(new_line) elif len(self.list_associated_material_numbers[0]) == 2: old_base_mat_str = "MATIDS " + line_new.split("MATIDS")[1].split()[0] new_base_mat_str = "MATIDS " + str(self.new_material_numbers[1][idx]) # Update the old material line/definition new_line = line_new.replace(old_material_expression, new_material_expression) new_line = new_line.replace(old_base_mat_str, new_base_mat_str) # print the new material definition print(new_line) else: raise RuntimeError( "At the moment we can only handle one nested material but " "you provided " f"{len(self.list_associated_material_numbers[0])} nested" " materials. Abort...." ) def _write_nested_materials(self, line, material_fields): """Write the nested materials field elementwise. Args: line (str): Current line in the dat-file material_fields (lst): List of dictionaries containing descriptions of material fields """ # below we loop over numbers of nested materials for idx, material_num in enumerate(self.new_material_numbers[1]): # potentially replace material parameter line_new = FourcDatExternalGeometry._parse_material_value_dependent_on_element_center( line, idx, material_fields ) # correct and print the nested material # note we use every second element here as the first ones are the base materials new_line = line_new.replace( "MAT " + str(self.list_associated_material_numbers[0][1]), "MAT " + str(material_num), ) print(new_line) @staticmethod def _parse_material_value_dependent_on_element_center(line, realization_index, material_fields): """Parse the material value. Only in case the line contains the material parameter name. Args: line (str): Current line of the dat-input file realization_index (int): Index of field value that corresponds to current element / line which should be changed material_fields (lst): List containing the random field descriptions and realizations Returns: line_new (str): New updated line that contains material value of field realization """ # TODO atm we only assume one random material field (see [0]). This should be generalized! # pylint: disable=fixme mat_param_name = material_fields[0][ "name" ] # TODO see if name is correct here # pylint: disable=fixme # potentially replace material parameter line_new = line if mat_param_name in line: string_to_replace = "{ " + mat_param_name + " }" line_new = line.replace( string_to_replace, f"{{ {mat_param_name}_{realization_index} }}" ) # TODO key field realization prob wrong # pylint: disable=fixme return line_new def _write_design_point_dirichlet_transport_conditions(self): pass def _write_design_point_neumann_conditions(self): pass def _write_design_point_dirichlet_conditions(self, random_field_lst, line): """Write Dirichlet design conditions. Convert the random fields, defined on the geometric set of interest, into design point Dirichlet BCs such that each dpoint contains a placeholder value of the random field. Args: random_field_lst (lst): List containing the design description of the involved random fields line (str): String for the current line in the dat-file that is read in """ # random fields for these sets if Dirichlet for geometric_set in self.list_geometric_sets: fields_dirich_on_geo_set = [ field for field in random_field_lst if (field["type"] == "dirichlet") and (field["external_instance"] == geometric_set) ] if fields_dirich_on_geo_set: old_num = FourcDatExternalGeometry._get_old_num_design_point_dirichlet_conditions( line ) self._overwrite_num_design_point_dirichlet_conditions(random_field_lst, old_num) # select correct node set node_set = [ node_set for node_set in self.new_nodes_lst if node_set["name"] == geometric_set ][0] # assign random dirichlet fields ( realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ) = self._assign_random_dirichlet_fields_per_geo_set(fields_dirich_on_geo_set) # take care of remaining deterministic dofs on the geometric set # we take the first field to get deterministic dofs ( realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ) = self._assign_deterministic_dirichlet_fields_per_geo_set( fields_dirich_on_geo_set, realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ) # write the new fields to the dat file -------------------------------------------- for topo_node, random_field_1, random_field_2, random_field_3, f1, f2, f3 in zip( node_set["topo_dnodes"], realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ): print( f"E {topo_node} - NUMDOF 3 ONOFF 1 1 1 VAL {random_field_1} " f"{random_field_2} {random_field_3} FUNCT {int(f1)} {int(f2)} {int(f3)}" ) else: print(line) @staticmethod def _get_old_num_design_point_dirichlet_conditions(line): """Return the former number of dirichlet point conditions. Args: line (str): String of current dat-file line Returns: old_num (int): old number of dirichlet point conditions """ old_num = int(line.split()[1]) return old_num def _overwrite_num_design_point_dirichlet_conditions(self, random_field_lst, old_num): """Write the new number of design point dirichlet conditions. Write them to the design description. Args: random_field_lst (lst): List containing vectors with the values of the realized random fields old_num (int): Former number of design point Dirichlet conditions """ # loop over all dirichlet nodes field_values = [] for geometric_set in self.list_geometric_sets: field_values.extend( [ [ field["name"] + "_" + str(i) for i in range(len(self.node_coordinates["node_mesh"])) ] for field in random_field_lst if (field["type"] == "dirichlet") and (field["external_instance"] == geometric_set) ] ) if field_values: num_new_dpoints = len(field_values[0]) num_existing_dpoints = old_num total_num_dpoints = num_new_dpoints + num_existing_dpoints print(f"DPOINT {total_num_dpoints}") def _assign_random_dirichlet_fields_per_geo_set(self, fields_dirich_on_geo_set): """Assign random Dirichlet fields. Args: fields_dirich_on_geo_set (lst): List containing the descriptions and a realization of the Dirichlet BCs random fields. Returns: realized_random_field_1 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_2 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_3 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. fun_1 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a 4C specific function that might, e.g., vary in time. fun_2 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a 4C specific function that might, e.g., vary in time. fun_3 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a 4C specific function that might, e.g., vary in time. """ # take care of random dirichlet fields realized_random_field_1 = realized_random_field_2 = realized_random_field_3 = None fun_1 = fun_2 = fun_3 = None for dirich_field in fields_dirich_on_geo_set: set_shape = len(self.node_coordinates["node_mesh"]) placeholders = [dirich_field["name"] + "_" + str(i) for i in range(set_shape)] if dirich_field["dof_for_field"] == 1: realized_random_field_1 = [ "{{ " + placeholder + " }}" for placeholder in placeholders ] fun_1 = dirich_field["funct_for_field"] * np.ones(set_shape) elif dirich_field["dof_for_field"] == 2: realized_random_field_2 = [ "{{ " + placeholder + " }}" for placeholder in placeholders ] fun_2 = dirich_field["funct_for_field"] * np.ones(set_shape) elif dirich_field["dof_for_field"] == 3: realized_random_field_3 = [ "{{ " + placeholder + " }}" for placeholder in placeholders ] fun_3 = dirich_field["funct_for_field"] * np.ones(set_shape) self._write_coords_to_dict( dirich_field["name"], placeholders, np.array(self.node_coordinates["coordinates"]) ) return ( realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ) def _assign_deterministic_dirichlet_fields_per_geo_set( self, fields_dirich_on_geo_set, realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ): """Adopt and write the DOFs of the Dirichlet point conditions. These conditions did not exist before but only one DOF at each discrete point might be rawn form a random field; the other DOFs might be a constant value, e.g., 0. Args: fields_dirich_on_geo_set (lst): List containing the descriptions and a realization of the Dirichlet BCs random fields. realized_random_field_1 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_2 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_3 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. fun_1 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. fun_2 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. fun_3 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. Returns: realized_random_field_1 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_2 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. realized_random_field_3 (np.array): Array containing values of the random field (each row is associated to a corresponding row in the coordinate matrix) for the depicted dimension of the Dirichlet DOF. fun_1 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. fun_2 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. fun_3 (np.array): Array containing integer identifiers for functions that are applied to associated dimension of the random field. This is a fourc specific function that might, e.g., vary in time. """ # TODO see how this behaves for several fields # pylint: disable=fixme set_shape = len(self.node_coordinates["node_mesh"]) for deter_dof, value_deter_dof, funct_deter in zip( fields_dirich_on_geo_set[0]["dofs_deterministic"], fields_dirich_on_geo_set[0]["value_dofs_deterministic"], fields_dirich_on_geo_set[0]["funct_for_deterministic_dofs"], ): if deter_dof == 1: if realized_random_field_1 is None: realized_random_field_1 = value_deter_dof * np.ones(set_shape) fun_1 = funct_deter * np.ones(set_shape) else: raise ValueError( "Dof 1 of geometric set is already defined and cannot be set to a " "deterministic value now. Abort..." ) elif deter_dof == 2: if realized_random_field_2 is None: realized_random_field_2 = value_deter_dof * np.ones(set_shape) fun_2 = funct_deter * np.ones(set_shape) else: raise ValueError( "Dof 2 of geometric set is already defined and cannot be set to a " "deterministic value now. Abort..." ) elif deter_dof == 3: if realized_random_field_2 is None: realized_random_field_2 = value_deter_dof * np.ones(set_shape) fun_2 = funct_deter * np.ones(set_shape) else: raise ValueError( "Dof 3 of geometric set is already defined and cannot be set to a " "deterministic value now. Abort..." ) # catch fields that were not set if fun_1 is None or fun_2 is None or fun_3 is None: raise ValueError("One fourc function of a Dirichlet DOF was not defined! Abort...") if ( realized_random_field_1 is None or realized_random_field_2 is None or realized_random_field_3 is None ): raise ValueError( "One random fields realization for a Dirichlet BC was not defined. Abort..." ) return ( realized_random_field_1, realized_random_field_2, realized_random_field_3, fun_1, fun_2, fun_3, ) def _get_my_topology(self, geo_set_name_type): """Get the topology of a geometric set. I.e.e its node mappings, based on the type of the geometric set. Args: geo_set_name_type (str): Name of the geometric set type. Returns: my_topology (lst): List with desired geometric topology """ # TODO this is problematic as topology has not been read it for the new object # pylint: disable=fixme if geo_set_name_type == "DNODE": my_topology = self.node_topology elif geo_set_name_type == "DLINE": my_topology = self.line_topology elif geo_set_name_type == "DSURFACE": my_topology = self.surface_topology elif geo_set_name_type == "DVOL": my_topology = self.volume_topology else: my_topology = None return my_topology def _create_new_node_sets(self, random_fields_lst): """Create nodeset form geometric associated nodes. From a given geometric set of interest: Identify associated nodes and add them as a new node-set to the geometric set-description of the external external_geometry_obj. Args: random_fields_lst (lst): List containing descriptions of involved random fields """ # iterate through all random fields that encode BCs boundary_conditions_random_fields = ( random_field for random_field in random_fields_lst if ( (random_field["type"] == "dirichlet") or (random_field["type"] == "neumann") or (random_field["type"] == "transport_dirichlet") ) ) nodes_mesh_lst = [] # note, this is a list of dicts for random_field in boundary_conditions_random_fields: # get associated geometric set topology_name = random_field["external_instance"] topology_type = topology_name.split()[0] # check if line, surf or vol my_topology_lst = self._get_my_topology(topology_type) nodes_mesh_lst.extend( [ {"node_mesh": topo["node_mesh"], "topo_dnodes": [], "name": topology_name} for topo in my_topology_lst if topo["topology_name"] == topology_name ] ) ndnode_min = len(self.nodeset_names) for num, _ in enumerate(nodes_mesh_lst): if num == 0: nodes_mesh_lst[num]["topo_dnodes"] = np.arange( ndnode_min, ndnode_min + len(nodes_mesh_lst[num]["node_mesh"]) ) else: last_node = nodes_mesh_lst[num - 1]["topo_dnodes"][-1] nodes_mesh_lst[num]["topo_dnodes"] = np.arange( last_node, last_node + len(nodes_mesh_lst[num]["node_mesh"]) ) self.new_nodes_lst = nodes_mesh_lst def _write_new_node_sets(self): """Write the new node sets to the dat-file as individual dnodes.""" for node_set in self.new_nodes_lst: for mesh_node, topo_node in zip(node_set["node_mesh"], node_set["topo_dnodes"]): print(f"NODE {mesh_node} DNODE {topo_node}") def _write_coords_to_dict(self, field_name, field_keys, field_coords): """Write random field coordinates to dict. Args: field_name (str): Name of the random field field_keys (str): Placeholders for the discretized random field field_coords (np.ndarray): Coordinates of the discretized random field """ self.coords_dict[field_name] = {"keys": field_keys, "coords": field_coords}