Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions documentation/source/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@


Materials are often needed in specific formats for a given code.
We aim to offer a variety of converter to different formats where direct interfacing cannot easily be achieved.
We aim to offer a variety of converters to different formats where direct interfacing cannot easily be achieved.

We currently offer coverters to a few select neutronics packages but this area will be expanded as the `matproplib` matures.
We currently offer coverters to a few different formats, this will be expanded as the `matproplib` matures.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
We currently offer coverters to a few different formats, this will be expanded as the `matproplib` matures.
We currently offer converters to a few different formats, this will be expanded as `matproplib` matures.



## Neutronics Converters
Expand Down Expand Up @@ -50,3 +50,77 @@ mcnp_mat = my_steel.convert(MCNPNeutronicConfig.name, op_cond)
```

All neutronics converters translate elements to thier natural nucleide abundances. The nucleides are then combined with any other isotopes on the material.

## Finite Element Converters

Finite Element codes and platforms, such as ANSYS, can be interfaced with using the following converters.

### MatML

The MatML 3.1 xml format can be imported and exported by simulation suites such as ANSYS. We can read from these xml files and write to them. This interface has been generated from the MatML 3.1 specification, a copy is available [here](https://github.com/Fusion-Power-Plant-Framework/matproplib/tree/main/matproplib/tools/matml/matml31.xsd)
The interface is limited to bulk material properties at this time.

#### Usage

To use this functionality you will need to install the extra required dependencies:

```bash
pip install matproplib[matml]
```

##### Import

To import a material from a MatML 3.1 xml file. If the importer is unable to process a specific property you specifiy it in `skip_properties`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
To import a material from a MatML 3.1 xml file. If the importer is unable to process a specific property you specifiy it in `skip_properties`
To import a material from a MatML 3.1 xml file. If the importer is unable to process a specific property, you can specify it in `skip_properties`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

First sentence is not really a sentence!

to ignore it. Please bare with us while we enable importing for different property types.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
to ignore it. Please bare with us while we enable importing for different property types.
to ignore it. Please bear with us while we enable importing for different property types.


```python
from matproplib.converters.matml import MatML, ANSYS_SKIPPED

MyMaterial = MatML.import_from('my_material.xml')

# skipping properties
my_skips = ANSYS_SKIPPED + ['my property']
MyMaterial = MatML.import_from('my_material.xml', skip_properties=my_skips)

my_material = MyMaterial()
```

##### Export

As with all converters they can be added during or after initialisation of the material.

```python
from matproplib.converters.matml import MatML
from matproplib.properties.group import props
from matproplib.material import material

Steel = material(
"Steel",
elements="C1Fe12",
properties=props(
density=5,
specific_heat_capacity=6
),
converters=MatML(), # alternatively a list of converters
)

my_steel = Steel()

# alternatively
# my_steel.converters.add(MatML())
```

To convert a material to a given format use the `convert` function on the material. The converter name is defined as a variable on the converter class:

```python
from matproplib.conditions import OperationalConditions
from matproplib.converters.matml import MatML

op_cond = OperationalConditions(temperature=298)

# converter name equivalent to 'matml'
matml_mat = my_steel.convert(MatML.name, op_cond)

# to write the xml file
mat_ml.export('my_material.xml')
```
2 changes: 1 addition & 1 deletion matproplib/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def array_validation(value):
def _unitify(self) -> Quantity:
dunit = type(self).model_fields["unit"].default
if isinstance(dunit, Unit) and self.unit == dunit:
return None
return ureg.Quantity(1, self.unit), dunit
if isinstance(dunit, PydanticUndefinedType):
raise NotImplementedError("default unit must be provided on class")
if isinstance(dunit, Unit):
Expand Down
11 changes: 11 additions & 0 deletions matproplib/converters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,14 @@ class Converter(PMBaseModel, ABC):
@abstractmethod
def convert(self, material: Material, op_cond: OpCondT):
"""Function to convert material to secondary format"""

@classmethod
def import_from(cls, obj, /) -> Material:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be possible to have an export_to on materials (or within their converters somehow).

I'm thinking that something along the lines of:

my_steel.export_to_xml("my_file.xml")

or a similar one-liner would be nice (not that I would use it...)

I know that at present it is only my_steel.convert(...).export(); just thinking what a typical tripping up point would be for a new user who maybe doesn't assiduously readthedocs.

"""Import a material from an object

Notes
-----
This could be from a file or a python object as
appropriate for incoming format
"""
raise NotImplementedError("No importer implemented")
238 changes: 238 additions & 0 deletions matproplib/converters/matml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# SPDX-FileCopyrightText: 2025-present The Bluemira Developers <https://github.com/Fusion-Power-Plant-Framework/bluemira>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

"""matproplib matml converter"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, ClassVar, Literal

import numpy as np

from matproplib.conditions import (
DependentPropertyConditionConfigTD,
OperationalConditions,
)
from matproplib.converters.base import Converter
from matproplib.material import material
from matproplib.properties.group import props
from matproplib.tools.matml import MatMLXML
from matproplib.tools.matml.utilities import (
extract_data,
parameter_data,
to_characterisation,
to_data,
to_unit,
)
from matproplib.tools.tools import From1DData

if TYPE_CHECKING:
from collections.abc import Sequence

import numpy.typing as npt
from pint import Unit

from matproplib.material import Material
from matproplib.properties.dependent import DependentPhysicalPropertyTD

__all__ = ["MatML"]

import re

name_pattern = re.compile(r"( )+(?<!^)+(?=[A-Za-z])")

ANSYS_SKIPPED = [
"Color",
"Magnetic Flux Density",
"Magnetic Field Intensity",
"Strain-Life Parameters",
"Alternating Stress",
]

NAME_TRANSLATIONS = {
"specific_heat": "specific_heat_capacity",
"tensile_ultimate_strength": "average_ultimate_tensile_stress",
"tensile_yield_strength": "average_yield_stress",
Comment on lines +55 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm tricky... it's probably fine, but this should technically be something the user decides somehow (e.g. minimum). I would not change anything, just leaving this for posteriority

"coefficient_of_thermal_expansion": "coefficient_thermal_expansion",
"resistivity": "electrical_resistivity",
"yield_strength": "average_yield_stress",
}


def rename(name: str) -> str:
"""Rename unit to snake case""" # noqa: DOC201
return name_pattern.sub("_", name).lower().replace("'", "").replace("-", "_")


def _single_multi_value(val: Sequence[Any]):
if len(val) == 1:
return val[0]
if isinstance(val[0], float):
return np.array(val)
return val


def _op_cond_config_creator(
name: str, value: npt.NDArray, unit: Unit
) -> DependentPropertyConditionConfigTD:
if name in OperationalConditions.model_fields.keys() ^ {"reference"}:
n_unit = (
OperationalConditions.model_fields[name]
.annotation.model_fields["unit"]
.default
)
if unit == n_unit:
return {name: {"lower": value.min(), "upper": value.max()}}
return {name: {"unit": unit, "lower": value.min(), "upper": value.max()}}


def convert_to_properties(
prop_in: dict[str, dict[str, dict[str, float | Unit]]],
) -> dict[str, DependentPhysicalPropertyTD]:
"""Convert data to dependent property dictionary

Raises
------
ValueError
No independent property found for interpolation
""" # noqa: DOC201
properties = {}
for v in prop_in.values():
prop_out = {}
for k, _pa in v.items():
if _pa.get("dependent") is not None:
prop_out["name"] = k
prop_out["value"] = _single_multi_value(_pa["value"])
prop_out["unit"] = _pa["unit"]

if _pa.get("independent") is not None:
prop_out["indep"] = {
"name": k,
"unit": _pa["unit"],
"value": _single_multi_value(_pa["value"]),
}

if prop_out.get("value") is not None:
if prop_out.get("indep") is not None:
indep = prop_out.pop("indep")
name = NAME_TRANSLATIONS.get(indep["name"], indep["name"])
prop_out["value"] = From1DData(
prop_out["indep"]["value"],
prop_out["value"],
name,
)
prop_out["op_cond_config"] = _op_cond_config_creator(
name, prop_out["value"].x, prop_out["indep"]["unit"]
)
elif isinstance(prop_out["value"], np.ndarray):
raise ValueError("No independent property to fit against")

name = prop_out.pop("name")
properties[NAME_TRANSLATIONS.get(name, name)] = prop_out
return properties


class MatML(Converter):
"""Converter to and from MatML xml v3.1

Notes
-----
Only considers bulk material properties and elemental contribution
"""

name: ClassVar[Literal["matml"]] = "matml"

xml_model: MatMLXML | None = None

@staticmethod
def convert(material: Material, op_cond: OperationalConditions) -> MatMLXML:
"""Convert material to matml object""" # noqa: DOC201
delimiter = ","
pr_ds, pa_ds, pr_vs = [], [], []

for id_, prop_name in enumerate(material.list_properties(), start=1):
prop = getattr(material, prop_name)
pr_v, pr_d, pa_d = parameter_data(
f"pr{id_:02}",
f"pa{id_:02}",
prop_name,
prop(op_cond),
prop.unit,
delimiter,
)
pr_vs.append(pr_v)
pr_ds.append(pr_d)
pa_ds.append(pa_d)

return MatMLXML.model_validate({
"material": [
{
"bulk_details": {
"name": {"value": material.name},
"property_data": pr_vs,
"delimiter": delimiter,
"characterisation": to_characterisation(material.elements),
}
}
],
"metadata": {
"parameter_details": pa_ds,
"property_details": pr_ds,
},
})

@classmethod
def get_model(cls, filename: str):
"""Get xml model from file""" # noqa: DOC201
return cls(xml_model=MatMLXML.from_file(filename))

@classmethod
def import_from(cls, filename, /, *, skip_properties=ANSYS_SKIPPED) -> Material:
"""Import material from file

Returns
-------
:
Material object
"""
self = cls.get_model(filename)

materials, property_details, parameter_details = extract_data(self.xml_model)

if len(materials) > 1:
# When implemented only add converter to mixture.
raise NotImplementedError("No fractional mixing of materials known")
mat = next(iter(materials.values()))
properties = {}
for p_id, prop in mat["properties"].items():
if (_name := property_details[p_id].name.value) in skip_properties:
continue
if (name := rename(_name)) not in properties:
properties[name] = {}
for pr_v in prop["value"]:
for pa_v in pr_v.parameter_value:
pd = parameter_details[pa_v.parameter]
if (_name := pd.name.value) in skip_properties:
continue
data = properties[name][rename(_name)] = {}
if "Dependent" in pa_v.qualifier:
data |= {
"value": to_data(pa_v, pr_v),
"unit": to_unit(pd),
"dependent": True,
}
elif "Independent" in pa_v.qualifier:
data |= {
"value": to_data(pa_v, pr_v),
"unit": to_unit(pd),
"dependent": False,
}
# Dont currently deal with other types

return material(
name=mat["name"],
elements=mat["elements"],
properties=props(**convert_to_properties(properties)),
converters=self,
)
8 changes: 6 additions & 2 deletions matproplib/properties/dependent.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ def _from_static_value(self):
"unit": model.unit,
"op_cond_config": model.op_cond_config,
}

if not isinstance(self, DependentPhysicalProperty):
if not isinstance(self, dict):
# Single number or a function not wrapped in a dictionary
Expand Down Expand Up @@ -222,7 +221,12 @@ def _unitify(self):
log.debug("Non default unit used, wrapping value")
if isinstance(self.value, _NoDependence):
wrap_callable = _no_dependence(
unit_conversion(unit_val * self.value(None), default)
unit_conversion(
ureg.Quantity(
unit_val.magnitude * self.value(None), unit_val.units
),
default,
)
)
else:
wrap_callable = _WrapCallable(self.value, unit_val, default)
Expand Down
9 changes: 9 additions & 0 deletions matproplib/tools/matml/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2025-present The Bluemira Developers <https://github.com/Fusion-Power-Plant-Framework/bluemira>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

"""matproplib matml tools"""

from matproplib.tools.matml.matml import MatMLXML

__all__ = ["MatMLXML"]
Loading
Loading