Skip to content

Conversation

@rmanno91
Copy link
Collaborator

@rmanno91 rmanno91 commented Oct 14, 2025

In this PR the write material method is refactored as sketched in the following code.
A first draft of the LS-Dyna writer is sketched as well.

from typing import Protocol, Any
from pydantic import BaseModel
from ansys.units import Quantity
from pathlib import Path

class InterpolationOptions(BaseModel):
	""""Interface to the specific interpolation option"""

class IndependentParameters(BaseModel):
	"""Independent parameters"""
	name: str
	values: Quantity

class MaterialModel(BaseModel):
	"""Interface to the specific material models"""
	name: str
	independent_parameters: list[IndependentParameters]
	interpolation_options: InterpolationOptions

class Material
	name: str
	guid: str
	models: MaterialModel
	
class MatmlReader:
	def parse_from_file(path: str | Path):
		....

class MatmlWriter:
	def write_to_file(path: str | Path):
		....
		
class Writer(Protocol):
	def write_material(...) -> None:
		...
		
class Reader(Protocol):
	def read_material(...) -> None:
		...

@register_writer("MapdlGrpc")
class WriterMapdl:
	def write_material() -> None:
		...
class ReaderMapdl:
	def read_material() -> None:
		...

WRITER_MAP:  dict[str, Type[Writer]] = {}
READER_MAP: dict[str, Type[Reader]] = {} 

def register_writer(name: str):
    def decorator(cls):
        WRITER_MAP[name] = cls
        return cls
    return decorator

def get_writer(client: Any) -> Writer:
	try:
		cls = WRITER_MAP[client]
                return cls()
	except:
		raise Exception("...")
	
def get_reader(client: Any) -> Reader
    try:
		return READER_MAP[client]
	except:
		raise Exception("...")
		
class MaterialManager
	materials: dict[str, Material]
	
	def add_material(self, material: Material) -> None:
		"""Add a material to the library."""
		pass
		
	def extend_material(self, material: str, material_models: list[MaterialModel]) -> None:
		"""Add material models to a material."""
		pass
	
	def delete_material(self, material: str) -> None:
		"""Delete a material"""
		pass
		
	def read_from_matml(self, path: str | Path) -> None:
		"""Read materials from MatML file and add to library."""
		parsed_data = MatmlReader.parse_from_file(path)
		......
		
	def write_matml(self, path: str | Path) -> None:
		"""Write file to MatMl."""
		writer = MatmlWriter(...)
		writer.write_to_file(path)
		
	def write_material_to_solver(self, client: Any, material: str, id: int | None = None) -> None:
		material = self.materials.get(material)
		writer = get_writer(client)
		writer.write_material()
		....
	
	def read_from_solver(self, client: Any, material: str, id: int | None = None) -> None:
        reader = get_reader(client)
		reader.read_material()
		....

@github-actions github-actions bot added the enhancement New features or code improvements label Oct 14, 2025
@codecov
Copy link

codecov bot commented Oct 14, 2025

Codecov Report

❌ Patch coverage is 88.95899% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.96%. Comparing base (18844ba) to head (080c7f7).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #414      +/-   ##
==========================================
+ Coverage   87.67%   89.96%   +2.29%     
==========================================
  Files          55       61       +6     
  Lines        2052     1904     -148     
==========================================
- Hits         1799     1713      -86     
+ Misses        253      191      -62     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions bot added the testing Involves the development of new unit/functional tests label Oct 14, 2025
@github-actions github-actions bot added dependencies Related with project dependencies maintenance Package and maintenance related labels Oct 15, 2025
@rmanno91
Copy link
Collaborator Author

rmanno91 commented Oct 15, 2025

@koubaa I have starting to sketch a possible way for writing to ls-dyna keyword. Could you please have a look at it? Do you have a better idea on how to move forward? (I have also added in herein the changes of your pr)

@koubaa
Copy link
Contributor

koubaa commented Oct 15, 2025

I think the matml writer and the solver writers are inherently the same kind of operation (an algorithm that visits a material and emits the data into some structure). I like that you've decoupled the material models from the mapdl writer but I think it would be nice to use the visitor pattern like so:

class BaseVisitor:
   def visit_material(self, material):
       if isinstance(material, Elastic):
           self.visit_elastic(material)
       ...

   def visit_materials(self, materials):
       for material in materials:
           if not self.is_supported(material):
                continue
           visit_material(self, material)

class MatMLVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...
    def write(self, path):
       ...

class MAPDLVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...

class DYNAVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...

So that any visitor can be applied on a material model or list of material models.

def export_to_matml(materials, path):
   visitor = MatMLVisitor():
   ansys.materials.manager.visit_materials(visitor, materials)
   visitor.export(path)

def send_to_fluent(materials, fluent):
   visitor = FluentVisitor(fluent)
   ansys.materials.manager.visit_materials(visitor, materials)

def to_dyna(materials, keyword_file):
   visitor = DynaVisitor()
   ansys.materials.manager.visit_materials(visitor, materials)
   visitor.deck.export(keyword_file)

@rmanno91
Copy link
Collaborator Author

I think the matml writer and the solver writers are inherently the same kind of operation (an algorithm that visits a material and emits the data into some structure). I like that you've decoupled the material models from the mapdl writer but I think it would be nice to use the visitor pattern like so:

class BaseVisitor:
   def visit_material(self, material):
       if isinstance(material, Elastic):
           self.visit_elastic(material)
       ...

   def visit_materials(self, materials):
       for material in materials:
           if not self.is_supported(material):
                continue
           visit_material(self, material)

class MatMLVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...
    def write(self, path):
       ...

class MAPDLVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...

class DYNAVisitor(BaseVisitor):
    def visit_elastic_material(self, mat):
        ...

So that any visitor can be applied on a material model or list of material models.

def export_to_matml(materials, path):
   visitor = MatMLVisitor():
   ansys.materials.manager.visit_materials(visitor, materials)
   visitor.export(path)

def send_to_fluent(materials, fluent):
   visitor = FluentVisitor(fluent)
   ansys.materials.manager.visit_materials(visitor, materials)

def to_dyna(materials, keyword_file):
   visitor = DynaVisitor()
   ansys.materials.manager.visit_materials(visitor, materials)
   visitor.deck.export(keyword_file)

Thanks Mohamed, I appreciate you taking a look at it.
I like this angle. Problem is that probably the visit_material method in the BaseVisitor cannot be scared across visitors. As different material models require different definition e.g.:
Elastoplasticity in dyna is a material card encompassing elastic and plastic behaviour. In mapdl properties are written separately. Also, Matml does not really distinguish between material models. It parses the information on the fly and instantiate the classes via the name and the behaviour. The is not really a visit material with if else statement in there.
I would keep it like that for the time being and see how it feels using it. But I agree with you for the matml writer, we could have the exact same structure of what is implemented currently. I will adapt it.
Regarding the way we define which material card to use according to the material models defined in the material. Do you think there is a better way we can do that?

@koubaa
Copy link
Contributor

koubaa commented Oct 16, 2025

Elastoplasticity in dyna is a material card encompassing elastic and plastic behaviour.

This is completely fine. The visitor can visit three material models and produce one, two, three, or twelve keywords. It doesn't have to be one keyword per model in the visitor pattern, since the visitor class will have state that is maintained across visits. It is an implementation detail of the visitor for how to accomplish this.

Matml does not really distinguish between material models. It parses the information

Sounds like you are talking here about reading a matml file, which should not use a visitor pattern.

Regarding the way we define which material card to use according to the material models defined in the material. Do you think there is a better way we can do that?

I hesitate to decide at the start, since designs often have to change multiple times to accomodate new information. My preference is to keep the dyna conversion in one place so that changes to its structure do not need to affect other parts of the library.

@rmanno91
Copy link
Collaborator Author

Elastoplasticity in dyna is a material card encompassing elastic and plastic behaviour.

This is completely fine. The visitor can visit three material models and produce one, two, three, or twelve keywords. It doesn't have to be one keyword per model in the visitor pattern, since the visitor class will have state that is maintained across visits. It is an implementation detail of the visitor for how to accomplish this.

Matml does not really distinguish between material models. It parses the information

Sounds like you are talking here about reading a matml file, which should not use a visitor pattern.

Regarding the way we define which material card to use according to the material models defined in the material. Do you think there is a better way we can do that?

I hesitate to decide at the start, since designs often have to change multiple times to accomodate new information. My preference is to keep the dyna conversion in one place so that changes to its structure do not need to affect other parts of the library.

Elastoplasticity in dyna is a material card encompassing elastic and plastic behaviour.

This is completely fine. The visitor can visit three material models and produce one, two, three, or twelve keywords. It doesn't have to be one keyword per model in the visitor pattern, since the visitor class will have state that is maintained across visits. It is an implementation detail of the visitor for how to accomplish this.

Matml does not really distinguish between material models. It parses the information

Sounds like you are talking here about reading a matml file, which should not use a visitor pattern.

Regarding the way we define which material card to use according to the material models defined in the material. Do you think there is a better way we can do that?

I hesitate to decide at the start, since designs often have to change multiple times to accomodate new information. My preference is to keep the dyna conversion in one place so that changes to its structure do not need to affect other parts of the library.

okay I think I start seeing the wider picture.
This would decouple further the actual material model and its solver/MatML representation.
While probably for solver representation the visitor pattern is easier to implement, MatML requires further reasoning.

I undestand now the added value representing things in this way. What do you think if I merge this where we have an initial separation between material model and solver representation and I add an issue with the discussion we are having here? So I can tackle it in a new pr. This way I can start having a small refactor MD to remove the write to dyna of simple materials.

@koubaa
Copy link
Contributor

koubaa commented Oct 17, 2025

Fine with me!

Copy link
Collaborator

@Andy-Grigg Andy-Grigg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nitpicky comments, mainly around naming and package structure. Just trying to think about which aspects of the implementation are relevant at each level of abstraction.

Overall this is good, and I really like the fact that we're now separating the abstract model definition from the writing logic. As you say, there will be more iteration on some of the subtleties, but having this initial separation of logic should make that iteration easier.

@@ -0,0 +1,58 @@
# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nitpicky suggestions about the layout of these writer classes:

  1. This module could just be called writer, the fact that it's in the fluent module tells you it's for fluent anyway. It could then be imported as from .util.fluent import writer as FluentWriter. Same for the other writer implementations.
  2. Would it make more sense to rename the util folder to writers, or at least to move the writer code into a new writers subpackage.

@@ -0,0 +1,109 @@
# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nitpick, I think this is a private module? i.e. it's only ever imported by writer_ls_dyna.py. In that case, the modules in this folder could be renamed to:

  • writer.py
  • _utils.py

This would make the import statements more concise, and would help developers understand that there's nothing in this module that they should care about unless they're dealing with the dyna writer internals.

"""FieldInfo for dependent parameters in material models."""

def __init__(self, *, matml_name=None, **kwargs):
def __init__(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame that we have references to specific solvers here. An alternative would be for a solver-agnostic name here, and then for each writer implementation to define some way of getting from the solver-agnostic name to the name that solver expects. This could be a mapping or some kind of algorithm, but would be on the writer code to implement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you Andy, In the new version we will try to get rid of it. Still I think each material model attribute need to specify if it is supported by a specific solver. This for the user to know what to provide and what not for a specific model and specific solver.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Related with project dependencies enhancement New features or code improvements maintenance Package and maintenance related testing Involves the development of new unit/functional tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants