Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load user plugins with table containing reference #413

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CHANGELOG
* We have decided internally to revert the versioning schema back to SemVer, starting at ``1.0.0``, in order to improve better backward and forward compatibility support for plugins.
* Fix bug related to Probabilistic History Matching result reader. The shape of result was wrong.
* Add the heat transfer mechanism for fluid materials composed by a parcel of radiation and convection.
* Add support for references inside tables defined by plugins.


2024.2 (2024-09-10)
Expand Down
8 changes: 8 additions & 0 deletions src/alfasim_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ def get_alfasim_sdk_api_path():
from alfasim_sdk._internal.alfacase.case_description import (
PressureContainerDescription,
)
from alfasim_sdk._internal.alfacase.case_description import (
PluginTableColumnForInternalReference,
)
from alfasim_sdk._internal.alfacase.case_description import (
PluginTableColumnForTracerReference,
)
from alfasim_sdk._internal.alfacase.case_description import (
PressureNodePropertiesDescription,
)
Expand Down Expand Up @@ -470,6 +476,8 @@ def get_alfasim_sdk_api_path():
"PipeThermalModelType",
"PipeThermalPositionInput",
"PluginDescription",
"PluginTableColumnForInternalReference",
"PluginTableColumnForTracerReference",
"PositionalPipeTrendDescription",
"PressureContainerDescription",
"PressureNodePropertiesDescription",
Expand Down
14 changes: 14 additions & 0 deletions src/alfasim_sdk/_internal/alfacase/case_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from barril.units import Array
from barril.units import Scalar

from ..validators import non_empty_str
from .case_description_attributes import attrib_array
from .case_description_attributes import attrib_curve
from .case_description_attributes import attrib_dict_of
Expand All @@ -32,6 +33,7 @@
from .case_description_attributes import dict_of_array
from .case_description_attributes import dict_with_scalar
from .case_description_attributes import InvalidReferenceError
from .case_description_attributes import list_of_optional_integers
from .case_description_attributes import list_of_strings
from .case_description_attributes import Numpy1DArray
from .case_description_attributes import numpy_array_validator
Expand Down Expand Up @@ -86,6 +88,7 @@ class PluginTracerReference:
@attr.s(frozen=True, slots=True)
class PluginInternalReference:
plugin_item_id = attr.ib(default=None)
container_key = attr.ib(default=None)


@attr.s(frozen=True, slots=True)
Expand All @@ -94,6 +97,17 @@ class PluginMultipleReference:
item_id_list = attr.ib(default=attr.Factory(list))


@attr.s(frozen=True, slots=True)
class PluginTableColumnForTracerReference:
tracer_ids = attr.ib(validator=list_of_optional_integers)


@attr.s(frozen=True, slots=True)
class PluginTableColumnForInternalReference:
plugin_item_ids = attr.ib(validator=list_of_optional_integers)
container_key = attr.ib(validator=non_empty_str)


@attr.s(frozen=True, slots=True)
class PluginTableContainer:
columns = attr.ib(default=attr.Factory(dict))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
list_of_strings = deep_iterable(
member_validator=optional(instance_of(str)), iterable_validator=instance_of(list)
)
list_of_optional_integers = deep_iterable(
member_validator=optional(instance_of(int)), iterable_validator=instance_of(list)
)
AttrNothingType = type(attr.NOTHING)
ScalarLike = Union[Tuple[Number, str], Scalar]
ArrayLike = Union[Tuple[Sequence[Number], str], Array]
Expand Down
58 changes: 45 additions & 13 deletions src/alfasim_sdk/_internal/alfacase/plugin_alfacase_to_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
from alfasim_sdk._internal.alfacase.case_description import PluginFileContent
from alfasim_sdk._internal.alfacase.case_description import PluginInternalReference
from alfasim_sdk._internal.alfacase.case_description import PluginMultipleReference
from alfasim_sdk._internal.alfacase.case_description import (
PluginTableColumnForInternalReference,
)
from alfasim_sdk._internal.alfacase.case_description import (
PluginTableColumnForTracerReference,
)
from alfasim_sdk._internal.alfacase.case_description import PluginTableContainer
from alfasim_sdk._internal.alfacase.case_description import PluginTracerReference
from alfasim_sdk._internal.alfacase.case_description_attributes import (
Expand Down Expand Up @@ -311,8 +317,11 @@ def _convert_reference(
if is_dict_with_keys(value, "tracer_id"):
return PluginTracerReference(tracer_id=int(value["tracer_id"]))
else:
if is_dict_with_keys(value, "plugin_item_id"):
return PluginInternalReference(plugin_item_id=int(value["plugin_item_id"]))
if is_dict_with_keys(value, "plugin_item_id", strict=False):
return PluginInternalReference(
container_key=value.get("container_key"),
plugin_item_id=int(value["plugin_item_id"]),
)
raise InvalidPluginDataError(f"Can not convert to a reference: {value!r}")


Expand Down Expand Up @@ -341,6 +350,37 @@ def _convert_string(
raise InvalidPluginDataError(f"Can not convert to a string: {value!r}")


def _convert_table_column_contents(
raw_col: object,
) -> Union[
Array, PluginTableColumnForInternalReference, PluginTableColumnForTracerReference
]:
"""
Try to convert an object into a valid table column content.

This is a helper function to `_convert_table` and is not registered
in `_PLUGIN_FIELD_TO_CASEDESCRIPTION`.
"""
if is_dict_with_keys(raw_col, "values", "unit"):
# Quantity.
return Array([float(v) for v in raw_col["values"]], raw_col["unit"])
elif is_dict_with_keys(raw_col, "tracer_ids"):
# Reference to tracer.
return PluginTableColumnForTracerReference(
[(int(v) if v != "None" else None) for v in raw_col["tracer_ids"]]
)
elif is_dict_with_keys(raw_col, "plugin_item_ids", "container_key"):
# Reference to plugin model.
return PluginTableColumnForInternalReference(
container_key=raw_col["container_key"],
plugin_item_ids=[
(int(v) if v != "None" else None) for v in raw_col["plugin_item_ids"]
],
)
else:
raise InvalidPluginDataError(f"Can not convert table column: {raw_col!r}")


def _convert_table(
value: object, type_from_plugin: BaseField, alfacase_path: Path
) -> PluginTableContainer:
Expand All @@ -352,17 +392,9 @@ def _convert_table(
raw_columns = value["columns"]
col_ids = [col.id for col in type_from_plugin.rows]
if is_dict_with_keys(raw_columns, *col_ids):
columns = {}
for col in col_ids:
raw_col = raw_columns[col]
if is_dict_with_keys(raw_col, "values", "unit"):
columns[col] = Array(
[float(v) for v in raw_col["values"]], raw_col["unit"]
)
else:
raise InvalidPluginDataError(
f"Can not convert table column: {raw_col!r}"
)
columns = {
col: _convert_table_column_contents(raw_columns[col]) for col in col_ids
}
return PluginTableContainer(columns=columns)
raise InvalidPluginDataError(f"Can not convert to a table: {value!r}")

Expand Down
73 changes: 52 additions & 21 deletions src/alfasim_sdk/_internal/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import numbers
from typing import Callable
from typing import FrozenSet
from typing import List
from typing import Optional
from typing import Sequence
from typing import Union

import attr
Expand All @@ -16,38 +16,38 @@
from alfasim_sdk._internal.validators import valid_unit


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class ALFAsimType:
name: str = attrib(default="ALFAsim")


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class TracerType(ALFAsimType):
_CONTAINER_TYPE = "TracerModelContainer"


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Tab:
"""
Base class for tab attributes available at ALFAsim.
"""


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Tabs:
"""
Base class for tabs attributes available at ALFAsim.
"""


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Group:
"""
Base class for Group attribute available at ALFAsim.
"""


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class BaseField:
"""
A base field for all types available at ALFAsim.
Expand Down Expand Up @@ -205,7 +205,7 @@ def alfasim_get_data_model_type():
)


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class String(BaseField):
"""
The String field represents an input that allows the user to enter and edit a single line of plain text.
Expand Down Expand Up @@ -249,7 +249,7 @@ class MyModel:
value: str = attrib(validator=non_empty_str)


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Enum(BaseField):
"""
The Enum field provides list of options to the user, showing only the selected item but providing a way to display
Expand Down Expand Up @@ -331,7 +331,7 @@ def check( # pylint: disable=arguments-differ
)


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class BaseReference(BaseField):
ref_type: type = attrib()
container_type: Optional[str] = attrib(
Expand All @@ -340,7 +340,11 @@ class BaseReference(BaseField):

def __attrs_post_init__(self):
if issubclass(self.ref_type, ALFAsimType):
self.container_type = self.ref_type._CONTAINER_TYPE
if self.container_type is not None:
raise TypeError(
"When using a ALFAsimType the container_type field must be None"
)
object.__setattr__(self, "container_type", self.ref_type._CONTAINER_TYPE)
else:
if self.container_type is None:
raise TypeError(
Expand All @@ -364,7 +368,7 @@ def check(self, attr: Attribute, value) -> None:
)


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Reference(BaseReference):
"""
The Reference field provides a list of options to the user and displays the current item selected.
Expand Down Expand Up @@ -464,7 +468,7 @@ class MyContainer:
"""


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class MultipleReference(BaseReference):
"""
The MultipleReference field works similar to :class:`Reference`, providing a list of options
Expand Down Expand Up @@ -638,24 +642,33 @@ class MyModel:
class TableColumn(BaseField):
"""
The TableColumn component provides columns for a :class:`Table` field.
Currently only columns with a :class:`Quantity` fields are available.
Currently only columns with :class:`Quantity` and :class:`Reference` fields are available.

Check out the documentation from :class:`Table` to see more details about the usage and how to retrieve values.
"""

id: str = attrib(validator=non_empty_str)
value: Quantity = attrib()
value: Union[Quantity, Reference] = attrib()
caption: str = attrib(init=False, default="")

def __attrs_post_init__(self) -> None:
object.__setattr__(self, "caption", self.value.caption)

@value.validator
def check( # pylint: disable=arguments-differ
self, attr: Attribute, values: Quantity
self, attr: Attribute, values: Union[Quantity, Reference]
) -> None:
if not isinstance(values, Quantity):
raise TypeError(f"{attr.name} must be a Quantity, got a {type(values)}.")
if isinstance(values, Quantity):
return # Always OK.
elif isinstance(values, Reference):
if values.container_type is None:
raise ValueError(
"When placed on table columns a Reference requires a non None container_type."
)
else:
raise TypeError(
f"{attr.name} must be a Quantity or a Reference, got a {type(values)}."
)


@attr.s(kw_only=True, frozen=True)
Expand All @@ -667,6 +680,14 @@ class Table(BaseField):

.. code-block:: python

@data_model(caption="Wolf")
class Wolf:
name = String(value="Wolf", caption="Name")

@container_model(caption="The Pack", model=Wolf, icon="")
class Pack:
pass

@data_model(icon="", caption="My Model")
class MyModel:
table_field=Table(
Expand All @@ -687,10 +708,20 @@ class MyModel:
caption="Pressure Column Caption",
),
),
TableColumn(
id="tracer",
value=Refence(
ref_type=Wolf,
container_type="Pack",
caption="Wolf Column Caption",
),
),
],
caption="Table Field",
)

TODO: ASIM-5862: update docs table_field_example_1.png image!

The image above illustrates the output from the example above.

.. image:: /_static/images/api/table_field_example_1.png
Expand Down Expand Up @@ -750,7 +781,7 @@ class MyModel:

"""

rows: FrozenSet[TableColumn] = attrib(converter=tuple)
rows: Sequence[TableColumn] = attrib(converter=tuple)

@rows.validator
def check( # pylint: disable=arguments-differ
Expand All @@ -763,7 +794,7 @@ def check( # pylint: disable=arguments-differ
raise TypeError(f"{attr.name} must be a list of TableColumn.")


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class Boolean(BaseField):
"""
The Boolean field provides a checkbox to select/deselect a property.
Expand Down Expand Up @@ -809,7 +840,7 @@ class MyModel:
value: bool = attrib(validator=instance_of(bool))


@attr.s(kw_only=True)
@attr.s(kw_only=True, frozen=True)
class FileContent(BaseField):
"""
The FileContent component provides a platform-native file dialog to the user to be able to select a file.
Expand Down
Loading
Loading