Skip to content

Commit

Permalink
Enable changing the unit system (#567)
Browse files Browse the repository at this point in the history
Allow setting the `unit_system` attribute on the model.

The `UnitSystemType` enum is extended by a value `FROM_FILE = "from_file"`,
which is an alias to `UNDEFINED`. This allows for using the more descriptive
`"from_file"` when loading an FE model.
When getting the `unit_system` attribute from the model, the `FROM_FILE`
is never returned; the "primary" value `UNDEFINED` is used instead, as before.
  • Loading branch information
greschd authored Oct 23, 2024
1 parent 6c09782 commit d3986e6
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 12 deletions.
11 changes: 8 additions & 3 deletions src/ansys/acp/core/_server/acp_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ def import_model(
into ACP composite definitions.
Available only when the format is not ``"acp:h5"``.
unit_system:
Set the unit system of the model to the given value. Ignored
if the unit system is already set in the FE file.
Defines the unit system of the imported file. Must be set if the
input file does not have units. If the input file does have units,
``unit_system`` must be either ``"from_file"``, or match the input
unit system.
Available only when the format is not ``"acp:h5"``.
Returns
Expand All @@ -180,7 +182,10 @@ def import_model(
model = Model._from_file(path=path, server_wrapper=server_wrapper)
else:
model = Model._from_fe_file(
path=path, server_wrapper=server_wrapper, format=format, **kwargs
path=path,
server_wrapper=server_wrapper,
format=format,
**kwargs,
)
if name is not None:
model.name = name
Expand Down
24 changes: 23 additions & 1 deletion src/ansys/acp/core/_tree_objects/_grpc_helpers/enum_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from collections.abc import Callable
from collections.abc import Callable, Mapping
import types
from typing import Any

__all__ = ["wrap_to_string_enum"]
Expand All @@ -41,11 +42,29 @@ def wrap_to_string_enum(
value_converter: Callable[[str], str] = lambda val: val.lower(),
doc: str,
explicit_value_list: tuple[int, ...] | None = None,
extra_aliases: Mapping[str, tuple[str, str]] = types.MappingProxyType({}),
) -> tuple[_StrEnumT, Callable[[_StrEnumT], int], Callable[[int], _StrEnumT]]:
"""Create a string Enum with the same keys as the given protobuf Enum.
Values of the enum are the keys, converted to lowercase.
Parameters
----------
key_converter :
A callable which converts the protobuf field names to the string enum field names.
value_converter :
A callable which converts the protobuf field names to the string enum values.
doc :
The docstring of the enum.
explicit_value_list :
A list of values that should be included in the enum. If None, all values are included.
extra_aliases :
Allows defining additional fields in the enum which correspond to the same protobuf value.
The keys are the primary enum field values, and the values are tuples of the alias field name
and the alias field value.
Note that the alias will not be used when converting from the protobuf value to the string
enum: the primary field name will be used instead.
Returns
-------
:
Expand All @@ -66,6 +85,9 @@ def wrap_to_string_enum(
fields.append((enum_key, enum_value))
to_pb_conversion_dict[enum_value] = pb_value
from_pb_conversion_dict[pb_value] = enum_value
for primary_enum_value, (alias_enum_key, alias_enum_value) in extra_aliases.items():
fields.append((alias_enum_key, alias_enum_value))
to_pb_conversion_dict[alias_enum_value] = to_pb_conversion_dict[primary_enum_value]

res_enum: _StrEnumT = StrEnum(class_name, fields, module=module) # type: ignore
res_enum.__doc__ = doc
Expand Down
3 changes: 3 additions & 0 deletions src/ansys/acp/core/_tree_objects/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@
unit_system_pb2.UnitSystemType,
module=__name__,
doc="Available choices for the unit system.",
# When loading from a file, the value 'from_file' is more descriptive than 'undefined',
# so we add an alias for it.
extra_aliases={"undefined": ("FROM_FILE", "from_file")},
)

(
Expand Down
27 changes: 22 additions & 5 deletions src/ansys/acp/core/_tree_objects/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@
from ._grpc_helpers.exceptions import wrap_grpc_errors
from ._grpc_helpers.mapping import define_create_method, define_mutable_mapping
from ._grpc_helpers.property_helper import (
_PROTOBUF_T,
_set_data_attribute,
grpc_data_property,
grpc_data_property_read_only,
mark_grpc_properties,
)
from ._grpc_helpers.protocols import ObjectInfo
from ._grpc_helpers.supported_since import supported_since
from ._mesh_data import (
ElementalData,
Expand Down Expand Up @@ -269,8 +272,20 @@ def _create_stub(self) -> model_pb2_grpc.ObjectServiceStub:
minimum_analysis_ply_thickness: ReadWriteProperty[float, float] = grpc_data_property(
"properties.minimum_analysis_ply_thickness"
)
unit_system = grpc_data_property_read_only(
"properties.unit_system", from_protobuf=unit_system_type_from_pb

@staticmethod
def _set_unit_system_data_attribute(pb_obj: ObjectInfo, name: str, value: _PROTOBUF_T) -> None:
# remove the 'minimum_analysis_ply_thickness' property from the pb object, to
# allow the backend to convert it to the new unit system.
pb_obj.properties.ClearField("minimum_analysis_ply_thickness")
_set_data_attribute(pb_obj, name, value)

unit_system = grpc_data_property(
"properties.unit_system",
from_protobuf=unit_system_type_from_pb,
to_protobuf=unit_system_type_to_pb,
setter_func=_set_unit_system_data_attribute,
writable_since="25.1",
)

average_element_size: ReadOnlyProperty[float] = grpc_data_property_read_only(
Expand Down Expand Up @@ -304,7 +319,7 @@ def _from_fe_file(
format: FeFormat, # type: ignore
ignored_entities: Iterable[IgnorableEntity] = (), # type: ignore
convert_section_data: bool = False,
unit_system: UnitSystemType = UnitSystemType.UNDEFINED,
unit_system: UnitSystemType = UnitSystemType.FROM_FILE,
) -> Model:
"""Load the model from an FE file.
Expand All @@ -326,8 +341,10 @@ def _from_fe_file(
Whether to import the section data of a shell model and convert it
into ACP composite definitions.
unit_system:
Set the unit system of the model to the given value. Ignored
if the unit system is already set in the FE file.
Defines the unit system of the imported file. Must be set if the
input file does not have units. If the input file does have units,
``unit_system`` must be either ``"from_file"``, or match the input
unit system.
"""
format_pb = fe_format_to_pb(format)
ignored_entities_pb = [ignorable_entity_to_pb(val) for val in ignored_entities]
Expand Down
36 changes: 36 additions & 0 deletions tests/unittests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,39 @@ def test_parent_access_raises(minimal_complete_model):
with pytest.raises(RuntimeError) as exc:
minimal_complete_model.parent
assert "parent" in str(exc.value)


@pytest.mark.parametrize("unit_system", UnitSystemType)
def test_change_unit_system(minimal_complete_model, unit_system, raises_before_version):
assert minimal_complete_model.unit_system == UnitSystemType.MPA

initial_node_coords = minimal_complete_model.mesh.node_coordinates
initial_minimum_analysis_ply_thickness = minimal_complete_model.minimum_analysis_ply_thickness

conversion_factor_by_us = {
"mpa": 1.0,
"mks": 1e-3,
"cgs": 0.1,
"si": 1e-3,
"bin": 0.03937008,
"bft": 0.00328084,
"umks": 1e3,
}

with raises_before_version("25.1"):
if unit_system in (UnitSystemType.UNDEFINED, UnitSystemType.FROM_FILE):
with pytest.raises(ValueError):
minimal_complete_model.unit_system = unit_system
else:
minimal_complete_model.unit_system = unit_system
assert minimal_complete_model.unit_system == unit_system
minimal_complete_model.update()

np.testing.assert_allclose(
minimal_complete_model.mesh.node_coordinates,
initial_node_coords * conversion_factor_by_us[unit_system.value],
)
np.testing.assert_allclose(
minimal_complete_model.minimum_analysis_ply_thickness,
initial_minimum_analysis_ply_thickness * conversion_factor_by_us[unit_system.value],
)
10 changes: 7 additions & 3 deletions tests/unittests/test_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ def test_workflow_unit_system_dat(acp_instance, model_data_dir, unit_system):
# input file does contain the unit system.
# Since 25.1, we also allow it if the unit systems match.
if parse_version(acp_instance.server_version) < parse_version("25.1"):
allowed_unit_system_values = [UnitSystemType.UNDEFINED]
allowed_unit_system_values = [UnitSystemType.UNDEFINED, UnitSystemType.FROM_FILE]
else:
allowed_unit_system_values = [UnitSystemType.UNDEFINED, UnitSystemType.MKS]
allowed_unit_system_values = [
UnitSystemType.UNDEFINED,
UnitSystemType.FROM_FILE,
UnitSystemType.MKS,
]

if unit_system not in allowed_unit_system_values:
with pytest.raises(ValueError) as ex:
Expand All @@ -134,7 +138,7 @@ def test_workflow_unit_system_cdb(acp_instance, model_data_dir, unit_system):

input_file_path = model_data_dir / "minimal_model_2.cdb"

if unit_system == UnitSystemType.UNDEFINED:
if unit_system in (UnitSystemType.UNDEFINED, UnitSystemType.FROM_FILE):
with pytest.raises(ValueError) as ex:
# Initializing a workflow with an undefined unit system is not allowed
# if the input file does not contain the unit system.
Expand Down

0 comments on commit d3986e6

Please sign in to comment.