from abc import ABC, abstractmethod
from typing import List, Dict, Type, Optional, Any
class Material(ABC):
"""
Base abstract class for all materials with simple sequential tagging
"""
_materials = {} # Class-level dictionary to track all materials
_matTags = {} # Class-level dictionary to track material tags
_names = {} # Class-level dictionary to track material names
_next_tag = 1 # Class variable to track the next tag to assign
_start_tag = 1 # Class variable to track the starting tag number
def __init__(self, material_type: str, material_name: str, user_name: str):
"""
Initialize a new material with a sequential tag
Args:
material_type (str): The type of material (e.g., 'nDMaterial', 'uniaxialMaterial')
material_name (str): The specific material name (e.g., 'ElasticIsotropic')
user_name (str): User-specified name for the material
"""
if user_name in self._names:
raise ValueError(f"Material name '{user_name}' already exists")
self.tag = Material._next_tag
Material._next_tag += 1
self.material_type = material_type
self.material_name = material_name
self.user_name = user_name
# Register this material in the class-level tracking dictionaries
self._materials[self.tag] = self
self._matTags[self] = self.tag
self._names[user_name] = self
@classmethod
def delete_material(cls, tag: int) -> None:
"""
Delete a material by its tag and retag all remaining materials sequentially
Args:
tag (int): The tag of the material to delete
"""
if tag in cls._materials:
# Remove the material from tracking dictionaries
material_to_delete = cls._materials[tag]
cls._names.pop(material_to_delete.user_name)
cls._matTags.pop(material_to_delete)
cls._materials.pop(tag)
# Retag all remaining materials
cls.retag_all()
@classmethod
def get_material_by_tag(cls, tag: int) -> 'Material':
"""
Retrieve a specific material by its tag.
Args:
tag (int): The tag of the material
Returns:
Material: The material with the specified tag
Raises:
KeyError: If no material with the given tag exists
"""
if tag not in cls._materials:
raise KeyError(f"No material found with tag {tag}")
return cls._materials[tag]
@classmethod
def get_material_by_name(cls, name: str) -> 'Material':
"""
Retrieve a specific material by its user-specified name.
Args:
name (str): The user-specified name of the material
Returns:
Material: The material with the specified name
Raises:
KeyError: If no material with the given name exists
"""
if name not in cls._names:
raise KeyError(f"No material found with name {name}")
return cls._names[name]
@classmethod
def clear_all(cls):
"""
Reset all class-level tracking and start tags from 1 again
"""
cls._materials.clear()
cls._matTags.clear()
cls._names.clear()
cls._next_tag = cls._start_tag
@classmethod
def set_tag_start(cls, start_number: int):
"""
Set the starting number for material tags globally
Args:
start_number (int): The first tag number to use
"""
if start_number < 1:
raise ValueError("Tag start number must be greater than 0")
cls._start_tag = start_number
cls._next_tag = cls._start_tag
cls.retag_all()
@classmethod
def retag_all(cls):
"""
Retag all materials sequentially starting from 1
"""
sorted_materials = sorted(
[(tag, material) for tag, material in cls._materials.items()],
key=lambda x: x[0]
)
# Clear existing dictionaries
cls._materials.clear()
cls._matTags.clear()
# Rebuild dictionaries with new sequential tags
for new_tag, (_, material) in enumerate(sorted_materials, start=cls._start_tag):
material.tag = new_tag # Update the material's tag
cls._materials[new_tag] = material # Update materials dictionary
cls._matTags[material] = new_tag # Update matTags dictionary
# Update next tag
cls._next_tag = cls._start_tag + len(cls._materials)
@classmethod
def get_all_materials(cls) -> Dict[int, 'Material']:
"""
Retrieve all created materials.
Returns:
Dict[int, Material]: A dictionary of all materials, keyed by their unique tags
"""
return cls._materials
@classmethod
def get_material_by_name(cls, name: str) -> 'Material':
"""
Retrieve a specific material by its user-specified name.
Args:
name (str): The user-specified name of the material
Returns:
Material: The material with the specified name
Raises:
KeyError: If no material with the given name exists
"""
return cls._names[name]
@classmethod
@abstractmethod
def get_parameters(cls) -> List[str]:
"""
Get the list of parameters for this material type.
Returns:
List[str]: List of parameter names
"""
pass
@classmethod
@abstractmethod
def get_description(cls) -> List[str]:
"""
Get the list of descriptions for the parameters of this material type.
Returns:
List[str]: List of parameter descriptions
"""
pass
def __str__(self) -> str:
"""
String representation of the material.
Returns:
str: Formatted material definition string
"""
str = f"Material :"
str += f"\tname:{self.material_name}\n"
str += f"\ttype:{self.material_type}\n"
str += f"\ttag:{self.tag}\n"
str += f"\tuser_name:{self.user_name}\n"
str += f"\tparameters:"
for key in self.get_parameters():
str += f"\t\t{str(key)}: {self.params[key]}"
@staticmethod
def validate(params: Dict[str, Any]) -> None:
"""
Validate the material parameters for all instances.
Args:
params (Dict[str, Any]): Dictionary of parameter names and values to validate
Raises:
ValueError: If any parameter is invalid
"""
pass
@abstractmethod
def to_tcl(self) -> str:
"""
Convert the material to a TCL string for OpenSees.
Returns:
str: TCL representation of the material
"""
pass
def get_values(self, keys: List[str]) -> Dict[str, float]:
"""
Default implementation to retrieve values for specific parameters.
Args:
keys (List[str]): List of parameter names to retrieve
Returns:
Dict[str, float]: Dictionary of parameter values
"""
return {key: self.params.get(key) for key in keys}
def update_values(self,**kwargs) -> None:
"""
Default implementation to update material parameters.
Args:
values (Dict[str, float]): Dictionary of parameter names and values to update
"""
self.params.clear()
params = self.validate(**kwargs)
self.params = params if params else {}
def get_param(self, key: str)-> Any:
"""
Get the value of a specific parameter.
Args:
key (str): The parameter name
Returns:
float: The value of the parameter
"""
return self.params[key]
@classmethod
def clear_all_materials(cls):
"""
Clear all materials and reset tags to starting value
"""
cls.clear_all()
class MaterialRegistry:
"""
A registry to manage material types and their creation.
"""
_material_types = {}
@classmethod
def register_material_type(cls, material_category: str, name: str, material_class: Type[Material]):
"""
Register a new material type for easy creation.
Args:
material_category (str): The category of material (nDMaterial, uniaxialMaterial)
name (str): The name of the material type
material_class (Type[Material]): The class of the material
"""
if material_category not in cls._material_types:
cls._material_types[material_category] = {}
cls._material_types[material_category][name] = material_class
@classmethod
def get_material_categories(cls):
"""
Get available material categories.
Returns:
List[str]: Available material categories
"""
return list(cls._material_types.keys())
@classmethod
def get_material_types(cls, category: str):
"""
Get available material types for a given category.
Args:
category (str): Material category
Returns:
List[str]: Available material types for the category
"""
return list(cls._material_types.get(category, {}).keys())
@classmethod
def create_material(cls, material_category: str, material_type: str, user_name: str = "Unnamed", **kwargs) -> Material:
"""
Create a new material of a specific type.
Args:
material_category (str): Category of material (nDMaterial, uniaxialMaterial)
material_type (str): Type of material to create
user_name (str): User-specified name for the material
**kwargs: Parameters for material initialization
Returns:
Material: A new material instance
Raises:
KeyError: If the material category or type is not registered
"""
if material_category not in cls._material_types:
raise KeyError(f"Material category {material_category} not registered")
if material_type not in cls._material_types[material_category]:
raise KeyError(f"Material type {material_type} not registered in {material_category}")
return cls._material_types[material_category][material_type](user_name=user_name, **kwargs)
def updateMaterialStage(self, state: str)-> str:
"""
Update the material stage.
Args:
state (str): The new state of the material
"""
return ""
from femora.components.Material.materialsOpenSees import *
[docs]
class MaterialManager:
"""
Singleton class for managing mesh materials and properties
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(MaterialManager, cls).__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
"""Initialize the singleton instance"""
pass
[docs]
def create_material(self,
material_category: str,
material_type: str,
user_name: str,
**material_params) -> Material:
"""
Create a new material with the given parameters
Args:
material_category (str): Category of material (e.g., 'nDMaterial')
material_type (str): Type of material (e.g., 'Concrete')
user_name (str): Unique name for the material
**material_params: Material-specific parameters
Returns:
Material: The created material instance
Raises:
KeyError: If material category or type doesn't exist
ValueError: If material name already exists
"""
return MaterialRegistry.create_material(
material_category=material_category,
material_type=material_type,
user_name=user_name,
**material_params
)
[docs]
@staticmethod
def get_material(identifier: Any) -> Material:
"""
Get material by either tag or name
Args:
identifier: Either material tag (int) or user_name (str)
Returns:
Material: The requested material
Raises:
KeyError: If material not found
TypeError: If identifier type is invalid
"""
if isinstance(identifier, int):
return Material.get_material_by_tag(identifier)
elif isinstance(identifier, str):
return Material.get_material_by_name(identifier)
else:
raise TypeError("Identifier must be either tag (int) or name (str)")
[docs]
def update_material_params(self,
identifier: Any,
new_params: Dict[str, float]) -> None:
"""
Update parameters of an existing material
Args:
identifier: Either material tag (int) or user_name (str)
new_params: Dictionary of parameter names and new values
Raises:
KeyError: If material not found
"""
material = self.get_material(identifier)
material.update_values(new_params)
[docs]
def delete_material(self, identifier: Any) -> None:
"""
Delete a material by its identifier
Args:
identifier: Either material tag (int) or user_name (str)
Raises:
KeyError: If material not found
"""
if isinstance(identifier, str):
material = Material.get_material_by_name(identifier)
Material.delete_material(material.tag)
elif isinstance(identifier, int):
Material.delete_material(identifier)
else:
raise TypeError("Identifier must be either tag (int) or name (str)")
[docs]
def get_all_materials(self) -> Dict[int, Material]:
"""
Get all registered materials
Returns:
Dict[int, Material]: Dictionary of all materials keyed by their tags
"""
return Material.get_all_materials()
[docs]
def get_available_material_types(self, category: Optional[str] = None) -> Dict[str, List[str]]:
"""
Get available material types, optionally filtered by category
Args:
category (str, optional): Specific category to get types for
Returns:
Dict[str, List[str]]: Dictionary of categories and their material types
"""
if category:
return {category: MaterialRegistry.get_material_types(category)}
return {
cat: MaterialRegistry.get_material_types(cat)
for cat in MaterialRegistry.get_material_categories()
}
[docs]
def set_material_tag_start(self, start_number: int) -> None:
"""
Set the starting number for material tags
Args:
start_number (int): Starting tag number (must be > 0)
Raises:
ValueError: If start_number < 1
"""
Material.set_tag_start(start_number)
[docs]
def clear_all_materials(self) -> None:
"""Clear all registered materials and reset tags"""
Material.clear_all()
[docs]
@classmethod
def get_instance(cls, **kwargs):
"""
Get the singleton instance of MaterialManager
Args:
**kwargs: Keyword arguments to pass to the constructor
Returns:
MaterialManager: The singleton instance
"""
if cls._instance is None:
cls._instance = cls(**kwargs)
return cls._instance
if __name__ == "__main__":
# Example concrete material class for testing
class ConcreteMaterial(Material):
def __init__(self, user_name: str, fc: float = 4000, E: float = 57000*pow(4000, 0.5)):
super().__init__('nDMaterial', 'Concrete', user_name)
self.params = {'fc': fc, 'E': E}
@classmethod
def get_parameters(cls) -> List[str]:
return ['fc', 'E']
@classmethod
def get_description(cls) -> List[str]:
return ['Concrete strength', "Young's modulus"]
def __str__(self) -> str:
return f"nDMaterial Concrete {self.tag} {self.params['fc']} {self.params['E']}"
# Register the concrete material
MaterialRegistry.register_material_type('nDMaterial', 'Concrete', ConcreteMaterial)
def print_material_info():
"""Helper function to print current material information"""
print("\nCurrent Materials:")
for tag, mat in Material.get_all_materials().items():
print(f"Tag: {tag}, Name: {mat.user_name}, Material: {mat}")
print("-" * 50)
# Test 1: Basic material creation and sequential tagging
print("Test 1: Basic material creation and sequential tagging")
try:
mat1 = ConcreteMaterial(user_name="Concrete1")
mat2 = ConcreteMaterial(user_name="Concrete2")
mat3 = ConcreteMaterial(user_name="Concrete3")
print_material_info()
except Exception as e:
print(f"Test 1 failed: {e}")
# Test 2: Deleting a material and checking retag
print("\nTest 2: Deleting a material and checking retag")
try:
Material.delete_material(2) # Delete middle material
print("After deleting material with tag 2:")
print_material_info()
except Exception as e:
print(f"Test 2 failed: {e}")
# Test 3: Setting custom start tag
print("\nTest 3: Setting custom start tag")
try:
Material.clear_all() # Clear existing materials
Material.set_tag_start(100) # Start tags from 100
mat1 = ConcreteMaterial(user_name="HighTagConcrete1")
mat2 = ConcreteMaterial(user_name="HighTagConcrete2")
print_material_info()
except Exception as e:
print(f"Test 3 failed: {e}")
# Test 4: Error handling for duplicate names
print("\nTest 4: Error handling for duplicate names")
try:
mat_duplicate = ConcreteMaterial(user_name="HighTagConcrete1")
print("Should not reach here - duplicate name allowed!")
except ValueError as e:
print(f"Expected error caught: {e}")
# Test 5: Material retrieval by tag and name
print("\nTest 5: Material retrieval by tag and name")
try:
mat_by_tag = Material.get_material_by_tag(100)
print(f"Retrieved by tag 100: {mat_by_tag.user_name}")
mat_by_name = Material.get_material_by_name("HighTagConcrete2")
print(f"Retrieved by name 'HighTagConcrete2': Tag = {mat_by_name.tag}")
except Exception as e:
print(f"Test 5 failed: {e}")
# Test 6: Registry functionality
print("\nTest 6: Registry functionality")
try:
# Get available categories and types
categories = MaterialRegistry.get_material_categories()
print(f"Available categories: {categories}")
types = MaterialRegistry.get_material_types('nDMaterial')
print(f"Available nDMaterial types: {types}")
# Create material through registry
registry_mat = MaterialRegistry.create_material(
'nDMaterial',
'Concrete',
user_name="RegistryMaterial",
fc=5000,
E=4000000
)
print(f"Created through registry: {registry_mat}")
except Exception as e:
print(f"Test 6 failed: {e}")
# Final state
print("\nFinal state of materials:")
print_material_info()