from abc import ABC, abstractmethod
from typing import List, Dict, Type, Optional, Union, Any
from femora.components.Material.materialBase import Material, MaterialManager
from femora.components.section.section_base import Section, SectionManager
from femora.components.transformation.transformation import GeometricTransformation, GeometricTransformationManager
class Element(ABC):
"""
Base abstract class for all OpenSees elements
Material, section, and transformation validation is handled by child classes
"""
_elements = {} # Dictionary mapping tags to elements
_element_to_tag = {} # Dictionary mapping elements to their tags
_next_tag = 1 # Track the next available tag
def __init__(self, element_type: str,
ndof: int,
material: Union[Material, List[Material], None] = None,
section: Section = None,
transformation: GeometricTransformation = None,
**element_params
):
"""
Initialize a new element with flexible dependency support.
Child classes handle validation of materials, sections, and transformations.
Args:
element_type (str): The type of element (e.g., 'quad', 'truss')
ndof (int): Number of degrees of freedom for the element
material (Union[Material, List[Material], None]): Material(s) for the element
section (Section, optional): Section for section-based elements
transformation (GeometricTransformation, optional): Transformation for beam elements
**element_params: Element-specific parameters (thickness, body forces, etc.)
"""
self.element_type = element_type
self._ndof = ndof
self.element_params = element_params
# Store dependencies without validation (child classes handle validation)
self._material = material
self._section = section
self._transformation = transformation
# Handle multiple materials for list case
if isinstance(material, list):
self._materials = material
self._material = material[0] if material else None # Primary material
else:
self._materials = [material] if material else []
# Assign the next available tag
self.tag = self._next_tag
Element._next_tag += 1
# Register this element in both mapping dictionaries
Element._elements[self.tag] = self
Element._element_to_tag[self] = self.tag
@classmethod
def _retag_elements(cls):
"""
Retag all elements sequentially from 1 to n based on their current order.
Updates both mapping dictionaries.
"""
# Get all current elements sorted by their tags
sorted_elements = sorted(cls._elements.items(), key=lambda x: x[0])
# Clear existing mappings
cls._elements.clear()
cls._element_to_tag.clear()
# Reassign tags sequentially
for new_tag, (old_tag, element) in enumerate(sorted_elements, start=1):
element.tag = new_tag
cls._elements[new_tag] = element
cls._element_to_tag[element] = new_tag
# Update next available tag
cls._next_tag = len(sorted_elements) + 1
@classmethod
def delete_element(cls, tag: int) -> None:
"""
Delete an element by its tag and retag remaining elements.
Args:
tag (int): The tag of the element to delete
"""
if tag in cls._elements:
element = cls._elements[tag]
# Remove from both dictionaries
del cls._elements[tag]
del cls._element_to_tag[element]
# Retag remaining elements
cls._retag_elements()
@classmethod
def get_element_by_tag(cls, tag: int) -> Optional['Element']:
"""
Get an element by its tag.
Args:
tag (int): The tag to look up
Returns:
Optional[Element]: The element with the given tag, or None if not found
"""
return cls._elements.get(tag)
@classmethod
def get_tag_by_element(cls, element: 'Element') -> Optional[int]:
"""
Get an element's tag.
Args:
element (Element): The element to look up
Returns:
Optional[int]: The tag for the given element, or None if not found
"""
return cls._element_to_tag.get(element)
@classmethod
def set_tag_start(cls, start_number: int):
"""
Set the starting number for element tags and retag all existing elements.
Args:
start_number (int): The first tag number to use
"""
if not cls._elements:
cls._next_tag = start_number
else:
offset = start_number - 1
# Create new mappings with offset tags
new_elements = {(tag + offset): element for tag, element in cls._elements.items()}
new_element_to_tag = {element: (tag + offset) for element, tag in cls._element_to_tag.items()}
# Update all element tags
for element in cls._elements.values():
element.tag += offset
# Replace the mappings
cls._elements = new_elements
cls._element_to_tag = new_element_to_tag
cls._next_tag = max(cls._elements.keys()) + 1
@classmethod
def get_all_elements(cls) -> Dict[int, 'Element']:
"""
Retrieve all created elements.
Returns:
Dict[int, Element]: A dictionary of all elements, keyed by their unique tags
"""
return cls._elements
@classmethod
def clear_all_elements(cls):
"""Clear all elements and reset tag counter."""
cls._elements.clear()
cls._element_to_tag.clear()
cls._next_tag = 1
def assign_material(self, material: Union[Material, List[Material]]):
"""
Assign a material to the element.
Note: Child classes may override this to add validation.
Args:
material (Union[Material, List[Material]]): The material(s) to assign
"""
if isinstance(material, list):
self._materials = material
self._material = material[0] if material else None
else:
self._material = material
self._materials = [material] if material else []
def assign_section(self, section: Section):
"""
Assign a section to the element.
Note: Child classes may override this to add validation.
Args:
section (Section): The section to assign
"""
self._section = section
def assign_transformation(self, transformation: GeometricTransformation):
"""
Assign a geometric transformation to the element.
Note: Child classes may override this to add validation.
Args:
transformation (GeometricTransformation): The transformation to assign
"""
self._transformation = transformation
def assign_ndof(self, ndof: int):
"""
Assign the number of DOFs for the element
Args:
ndof (int): Number of DOFs for the element
"""
self._ndof = ndof
def get_material(self) -> Optional[Material]:
"""
Retrieve the primary assigned material
Returns:
Optional[Material]: The primary material assigned to this element, or None
"""
return self._material
def get_materials(self) -> List[Material]:
"""
Retrieve all assigned materials
Returns:
List[Material]: List of all materials assigned to this element
"""
return self._materials.copy()
def get_section(self) -> Optional[Section]:
"""
Retrieve the assigned section
Returns:
Optional[Section]: The section assigned to this element, or None
"""
return self._section
def get_transformation(self) -> Optional[GeometricTransformation]:
"""
Retrieve the assigned geometric transformation
Returns:
Optional[GeometricTransformation]: The transformation assigned to this element, or None
"""
return self._transformation
def get_element_params(self) -> Dict[str, Any]:
"""
Retrieve element-specific parameters
Returns:
Dict[str, Any]: Dictionary of element-specific parameters
"""
return self.element_params.copy()
def update_element_params(self, **new_params):
"""
Update element-specific parameters
Args:
**new_params: New parameter values to update
"""
self.element_params.update(new_params)
def get_ndof(self) -> int:
"""
Get the number of degrees of freedom for this element
Returns:
int: Number of DOFs
"""
return self._ndof
# Abstract methods for element implementation
@classmethod
@abstractmethod
def get_parameters(cls) -> List[str]:
"""
Get the list of parameters for this element type.
Returns:
List[str]: List of parameter names
"""
pass
@classmethod
@abstractmethod
def get_possible_dofs(cls) -> List[str]:
"""
Get the list of possible DOFs for this element type.
Returns:
List[str]: List of possible DOFs
"""
pass
@classmethod
@abstractmethod
def get_description(cls) -> List[str]:
"""
Get the list of parameter descriptions for this element type.
Returns:
List[str]: List of parameter descriptions
"""
pass
@classmethod
@abstractmethod
def validate_element_parameters(cls, **kwargs) -> Dict[str, Union[int, float, str]]:
"""
Check if the element input parameters are valid.
Args:
**kwargs: Element parameters to validate
Returns:
Dict[str, Union[int, float, str]]: Dictionary of parameters with valid values
"""
pass
@abstractmethod
def get_values(self, keys: List[str]) -> Dict[str, Union[int, float, str]]:
"""
Retrieve values for specific parameters.
Args:
keys (List[str]): List of parameter names to retrieve
Returns:
Dict[str, Union[int, float, str]]: Dictionary of parameter values
"""
pass
@abstractmethod
def update_values(self, values: Dict[str, Union[int, float, str]]) -> None:
"""
Update element parameters.
Args:
values (Dict[str, Union[int, float, str]]): Dictionary of parameter names and values to update
"""
pass
@abstractmethod
def to_tcl(self, tag: int, nodes: List[int]) -> str:
"""
Convert the element to a TCL command string.
Args:
tag (int): The tag of the element
nodes (List[int]): List of node tags for the element
Returns:
str: TCL command string representation of the element
"""
pass
def __str__(self) -> str:
"""String representation of the element"""
return f"{self.element_type} Element (Tag: {self.tag}, DOF: {self._ndof})"
def __repr__(self) -> str:
"""Detailed string representation of the element"""
return f"Element(type='{self.element_type}', tag={self.tag}, ndof={self._ndof}, " \
f"material={'Yes' if self._material else 'No'}, " \
f"section={'Yes' if self._section else 'No'}, " \
f"transformation={'Yes' if self._transformation else 'No'})"
[docs]
class ElementRegistry:
"""
A singleton registry to manage element types and their creation with full dependency resolution.
"""
_instance = None
_element_types = {}
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(ElementRegistry, cls).__new__(cls)
return cls._instance
[docs]
@classmethod
def register_element_type(cls, name: str, element_class: Type[Element]):
"""
Register a new element type for easy creation.
Args:
name (str): The name of the element type
element_class (Type[Element]): The class of the element
"""
cls._element_types[name] = element_class
[docs]
@classmethod
def unregister_element_type(cls, name: str):
"""
Unregister an element type.
Args:
name (str): The name of the element type to remove
"""
if name in cls._element_types:
del cls._element_types[name]
[docs]
@classmethod
def get_element_types(cls) -> List[str]:
"""
Get available element types.
Returns:
List[str]: Available element types
"""
return list(cls._element_types.keys())
[docs]
@classmethod
def is_element_type_registered(cls, name: str) -> bool:
"""
Check if an element type is registered.
Args:
name (str): The element type name to check
Returns:
bool: True if registered, False otherwise
"""
return name in cls._element_types
[docs]
@classmethod
def create_element(cls,
element_type: str,
**kwargs) -> Element:
"""
Create a new element of a specific type with full dependency resolution.
Args:
element_type (str): Type of element to create
**kwargs: All element parameters including ndof, material, section, transformation
Returns:
Element: A new element instance
Raises:
KeyError: If the element type is not registered
ValueError: If dependencies cannot be resolved
"""
if element_type not in cls._element_types:
raise KeyError(f"Element type '{element_type}' not registered. "
f"Available types: {list(cls._element_types.keys())}")
# Resolve dependencies
resolved_kwargs = kwargs.copy()
if 'material' in resolved_kwargs:
resolved_kwargs['material'] = cls._resolve_materials(resolved_kwargs['material'])
if 'section' in resolved_kwargs:
resolved_kwargs['section'] = cls._resolve_section(resolved_kwargs['section'])
if 'transformation' in resolved_kwargs:
resolved_kwargs['transformation'] = cls._resolve_transformation(resolved_kwargs['transformation'])
return cls._element_types[element_type](**resolved_kwargs)
@classmethod
def _resolve_materials(cls, material):
"""
Resolve material references to actual Material objects
Args:
material: Material reference (str, int, Material, or list of these)
Returns:
Resolved material object(s) or None
"""
if material is None:
return None
if isinstance(material, list):
# List of materials
resolved_materials = []
for i, mat in enumerate(material):
if isinstance(mat, (str, int)):
resolved_mat = MaterialManager.get_material(mat)
if resolved_mat is None:
raise ValueError(f"Material {mat} at index {i} not found")
resolved_materials.append(resolved_mat)
elif isinstance(mat, Material):
resolved_materials.append(mat)
else:
raise ValueError(f"Invalid material type at index {i}: {type(mat)}")
return resolved_materials
elif isinstance(material, (str, int)):
# Single material by reference
resolved_material = MaterialManager.get_material(material)
if resolved_material is None:
raise ValueError(f"Material '{material}' not found")
return resolved_material
elif isinstance(material, Material):
# Already a Material object
return material
else:
raise ValueError(f"Invalid material type: {type(material)}")
@classmethod
def _resolve_section(cls, section):
"""
Resolve section references to actual Section objects
Args:
section: Section reference (str, int, Section, or None)
Returns:
Resolved section object or None
"""
if section is None:
return None
if isinstance(section, (str, int)):
resolved_section = SectionManager.get_section(section)
if resolved_section is None:
raise ValueError(f"Section '{section}' not found")
return resolved_section
elif isinstance(section, Section):
return section
else:
raise ValueError(f"Invalid section type: {type(section)}")
@classmethod
def _resolve_transformation(cls, transformation):
"""
Resolve transformation references to actual GeometricTransformation objects
Args:
transformation: Transformation reference (str, int, GeometricTransformation, or None)
Returns:
Resolved transformation object or None
"""
if transformation is None:
return None
if isinstance(transformation, (str, int)):
resolved_transformation = GeometricTransformationManager.get_transformation(transformation)
if resolved_transformation is None:
raise ValueError(f"Transformation '{transformation}' not found")
return resolved_transformation
elif isinstance(transformation, GeometricTransformation):
return transformation
else:
raise ValueError(f"Invalid transformation type: {type(transformation)}")
[docs]
@classmethod
def get_element(cls, tag: int) -> Optional[Element]:
"""
Get an element by its tag.
Args:
tag (int): The tag of the element to retrieve
Returns:
Optional[Element]: The element with the given tag, or None if not found
"""
return Element.get_element_by_tag(tag)
[docs]
@classmethod
def get_element_count(cls) -> int:
"""
Get the total number of registered elements.
Returns:
int: Number of elements
"""
return len(Element.get_all_elements())
[docs]
@classmethod
def clear_all_elements(cls):
"""Clear all elements."""
Element.clear_all_elements()
# Import existing element implementations
from femora.components.Element.elementsOpenSees import *
from femora.components.Element.elements_opensees_beam import *