Source code for pyfem.util.dataStructures

# SPDX-License-Identifier: MIT
# Copyright (c) 2011–2026 Joris J.C. Remmers

import time
from typing import Any, Dict, Iterator, List, Optional, Sequence

import numpy as np
from numpy import zeros
from pyfem.util.logger import getLogger, separator

"""
Utilities for lightweight data structures used across PyFEM.

This module provides simple containers and helpers used by the solver and
pre/post-processing layers: cleaning input values, status tracking,
property containers, global data for assembly, and per-element storage.

All modifications in this file are non-functional documentation and type
annotations to improve readability and editor support; runtime behavior is
preserved.
"""

logger = getLogger()

#-------------------------------------------------------------------------------
#
#-------------------------------------------------------------------------------

[docs] def cleanVariable(a: Any) -> Any: """ Convert a textual configuration value into a Python object. - 'true' -> True - 'false' -> False - otherwise: try `eval`, fall back to original string Args: a: Input value (typically a string) read from configuration. Returns: Parsed Python object or the original value if parsing fails. """ if a == 'true': return True elif a == 'false': return False else: try: return eval(a) except Exception: return a
#------------------------------------------------------------------------------- # #-------------------------------------------------------------------------------
[docs] class solverStatus: """Container for tracking solver progress and timing. Attributes: cycle (int): Current step/cycle number. iiter (int): Current iteration count within step. time (float): Current simulation time. time0 (float): Reference time (unused internally here). dtime (float): Time increment for a cycle. lam (float): Load or continuation parameter. """ def __init__(self) -> None: self.cycle: int = 0 self.iiter: int = 0 self.time: float = 0.0 self.time0: float = 0.0 self.dtime: float = 0.0 self.lam: float = 1.0
[docs] def increaseStep(self) -> None: """Advance to the next solver step and reset iteration counter.""" self.cycle += 1 self.time += self.dtime self.iiter = 0
#------------------------------------------------------------------------------- # #-------------------------------------------------------------------------------
[docs] class Properties: """Simple attribute container built from a dictionary. Instances expose dictionary keys as attributes and support iteration over (name, value) pairs. This is a lightweight alternative to `types.SimpleNamespace`. """ def __init__(self, dictionary: Dict[str, Any] = {}) -> None: for key in dictionary.keys(): setattr(self, key, dictionary[key]) #------------------------------------------------------------------------------- # #------------------------------------------------------------------------------- def __str__(self) -> str: """Return a multi-line representation listing public attributes.""" myStr = '' for att in dir(self): # Ignore private members and standard routines if att.startswith('__'): continue myStr += 'Attribute: ' + att + '\n' myStr += str(getattr(self, att)) + '\n' return myStr #------------------------------------------------------------------------------- # #------------------------------------------------------------------------------- def __iter__(self) -> Iterator: """Iterate over (name, value) pairs for public attributes.""" propsList: List = [] for att in dir(self): # Ignore private members and standard routines if att.startswith('__'): continue propsList.append((att, getattr(self, att))) return iter(propsList) #------------------------------------------------------------------------------- # #-------------------------------------------------------------------------------
[docs] def store(self, key: str, val: Any) -> None: """Store a property given a dotted key path or a simple key. Examples: store('alpha', 1) store('parent.child.value', 'x') """ if '.' not in key: setattr(self, key, val) else: kets = key.split('.') props = self for y in kets[:-1]: props = getattr(props, y) setattr(props, kets[-1], cleanVariable(val))
#------------------------------------------------------------------------------- # GlobalData - Global data container for finite element analysis #-------------------------------------------------------------------------------
[docs] class GlobalData(Properties): """Global data container for finite element analysis. This class holds the global state vectors, forces, boundary conditions, and manages I/O operations for nodal data. It inherits from Properties to provide flexible attribute storage. Attributes: nodes: Node container with coordinates and connectivity. elements: Element container with element definitions. dofs: Degree of freedom manager. state: Current state vector (displacements, temperatures, etc.). Dstate: Incremental state vector. fint: Internal force vector. fhat: Applied external force vector. velo: Velocity vector (for dynamic analysis). acce: Acceleration vector (for dynamic analysis). solverStatus: Solver status tracker. outputNames: List of output variable names. """ def __init__(self, nodes: Any, elements: Any, dofs: Any) -> None: """Initialize the global data container. Args: nodes: Node container with nodal coordinates. elements: Element container with element definitions. dofs: Degree of freedom manager. """ # Initialize base Properties class with nodes, elements, and dofs Properties.__init__(self, {'nodes': nodes, 'elements': elements, 'dofs': dofs}) # Initialize state vectors self.state = zeros(len(self.dofs)) self.Dstate = zeros(len(self.dofs)) self.fint = zeros(len(self.dofs)) self.fhat = zeros(len(self.dofs)) # Initialize dynamic analysis vectors self.velo = zeros(len(self.dofs)) self.acce = zeros(len(self.dofs)) # Set solver status from elements self.solverStatus = elements.solverStat # Initialize output names list self.outputNames = [] #--------------------------------------------------------------------------- # readFromFile - Read external forces from input file #---------------------------------------------------------------------------
[docs] def readFromFile(self, fname: str) -> None: """Read external forces from an input file. Parses the <ExternalForces> section of the input file and populates the fhat vector with prescribed external forces. Args: fname: Path to the input file containing external forces. Returns: None Note: Expected format in file: <ExternalForces> dofType[nodeID] = value; </ExternalForces> """ logger.info("Reading external forces ......") # Re-open file for parsing fin = open(fname) lines = fin.readlines() inside = False for line in lines: if "<ExternalForces>" in line: inside = True continue if "</ExternalForces>" in line: inside = False break # remove this if there may be multiple blocks if inside: # Parse force specification line a = line.strip().split(';') if len(a) == 2: b = a[0].split('=') if len(b) == 2: c = b[0].split('[') dofType = c[0] nodeID = eval(c[1].split(']')[0]) # Set external force value self.fhat[self.dofs.getForType(nodeID, dofType)] = eval(b[1]) fin.close()
#--------------------------------------------------------------------------- # printNodes - Print nodal data to file or screen #---------------------------------------------------------------------------
[docs] def printNodes(self, fileName: Optional[str] = None, inodes: Optional[List[int]] = None) -> None: """Print nodal data to file or screen. Outputs a formatted table containing nodal state values, internal forces, and any additional output variables. Args: fileName: Path to output file. If None, prints to screen. inodes: List of node IDs to print. If None, prints all nodes. Returns: None """ # Determine output destination if fileName is None: f = None else: f = open(fileName, "w") # Get node list if not provided if inodes is None: inodes = list(self.nodes.keys()) # Print header row print(' Node | ', file=f, end=' ') # Print DOF type headers for dofType in self.dofs.dofTypes: print(f" {dofType:<10}", file=f, end=' ') # Print internal force headers if available if hasattr(self, 'fint'): for dofType in self.dofs.dofTypes: print(f" fint-{dofType:<6}", file=f, end=' ') # Print additional output variable headers for name in self.outputNames: print(f" {name:<11}", file=f, end=' ') print(" ", file=f) print(' ', ('-' * 81), file=f) # Print data for each node for nodeID in inodes: print(f' {nodeID:4d} | ', file=f, end=' ') # Print state values for dofType in self.dofs.dofTypes: print(f' {self.state[self.dofs.getForType(nodeID, dofType)]:10.3e} ', file=f, end=' ') # Print internal forces for dofType in self.dofs.dofTypes: print(f' {self.fint[self.dofs.getForType(nodeID, dofType)]:10.3e} ', file=f, end=' ') # Print additional output data for name in self.outputNames: print(f' {self.getData(name, nodeID):10.3e} ', file=f, end=' ') print(" ", file=f) print(" ", file=f) # Close file if writing to file if fileName is not None: f.close()
#--------------------------------------------------------------------------- # getData - Retrieve weighted output data for nodes #---------------------------------------------------------------------------
[docs] def getData(self, outputName: str, inodes: Any) -> Any: """Retrieve weighted output data for specified nodes. Computes weighted average of output data for nodes. The weights are typically used to average data from multiple elements sharing a node. Args: outputName: Name of the output variable to retrieve. inodes: Node ID (int) or list of node IDs to retrieve data for. Returns: Weighted output value(s). Returns float for single node, list of values for multiple nodes. """ # Get data and weight arrays data = getattr(self, outputName) weights = getattr(self, outputName + 'Weights') # Handle single node if type(inodes) is int: i = list(self.nodes.keys()).index(inodes) return data[i] / weights[i] else: # Handle multiple nodes outdata = [] for row, w in zip(data[inodes], weights[inodes]): if w != 0: outdata.append(row / w) else: outdata.append(row) return outdata
#--------------------------------------------------------------------------- # resetNodalOutput - Clear all nodal output data #---------------------------------------------------------------------------
[docs] def resetNodalOutput(self) -> None: """Clear all nodal output data and weights. Removes all output variables and their corresponding weights from the global data structure. Called at the start of each analysis step. Returns: None """ # Remove all output data and weights for outputName in self.outputNames: delattr(self, outputName) delattr(self, outputName + 'Weights') # Clear output names list self.outputNames = []
#--------------------------------------------------------------------------- # close - Finalize analysis and print summary #---------------------------------------------------------------------------
[docs] def close(self) -> None: """Finalize analysis and print execution summary. Prints the total elapsed time and a success message to the log. Called at the end of a successful analysis. Returns: None """ from pyfem.util.plotUtils import plotTime logger.info("") separator("=") logger.info(" Total elapsed time.......... : " + plotTime(time.time() - self.startTime)) logger.info(" PyFem analysis terminated successfully.") separator("=")
#------------------------------------------------------------------------------- # elementData - Container for element-level data #-------------------------------------------------------------------------------
[docs] class elementData(): """Container for element-level state and computed quantities. This class stores element state vectors, element matrices (stiffness, mass), element force vectors, and output labels for post-processing. Attributes: state: Element state vector (displacements, etc.). Dstate: Element incremental state vector. stiff: Element tangent stiffness matrix. fint: Element internal force vector. mass: Element mass matrix. lumped: Element lumped mass vector. diss: Element dissipation (energy dissipated). outlabel: List of output variable labels for this element. """ def __init__(self, elstate: np.ndarray, elDstate: np.ndarray) -> None: """Initialize element data container. Args: elstate: Element state vector. elDstate: Element incremental state vector. """ # Get number of DOFs from state vector nDof = len(elstate) # Store state vectors self.state = elstate self.Dstate = elDstate # Initialize element matrices and vectors self.stiff = zeros(shape=(nDof, nDof)) self.fint = zeros(shape=(nDof)) self.mass = zeros(shape=(nDof, nDof)) self.lumped = zeros(shape=(nDof)) self.diss = 0.0 # Initialize output labels self.outlabel = [] def __str__(self) -> str: """Return string representation of element state. Returns: String representation of the state vector. """ return str(self.state)