-
Notifications
You must be signed in to change notification settings - Fork 2
New base class #163
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
New base class #163
Changes from all commits
a0aadea
66a199e
690ce16
6687c33
5def3e7
884df73
0f8b932
8e3ab34
51100ad
4189a27
64f09c1
4e26dfc
f396c70
ef6ee5c
d19f256
cecc7b0
fef80c0
390ad80
113ec7c
e10a4fa
99c83e3
5e67822
f8b0817
a57f922
667ca97
7ddaea6
2fb951c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| from .based_base import BasedBase | ||
| from .collection_base import CollectionBase | ||
| from .model_base import ModelBase | ||
| from .new_base import NewBase | ||
| from .obj_base import ObjBase | ||
|
|
||
| __all__ = [ | ||
| BasedBase, | ||
| CollectionBase, | ||
| ObjBase, | ||
| ModelBase, | ||
| NewBase, | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| from __future__ import annotations | ||
|
|
||
| # SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software> | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from easyscience.variable.descriptor_number import DescriptorNumber | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Any | ||
| from typing import Dict | ||
| from typing import List | ||
| from typing import Optional | ||
|
|
||
| from ..io import SerializerBase | ||
| from ..variable import Parameter | ||
| from ..variable.descriptor_base import DescriptorBase | ||
| from .new_base import NewBase | ||
|
|
||
|
|
||
| class ModelBase(NewBase): | ||
| """ | ||
| This is the base class for all model classes in EasyScience. | ||
| It provides methods to get parameters for fitting and analysis as well as proper serialization/deserialization for | ||
| DescriptorNumber/Parameter attributes. | ||
|
|
||
| It assumes that Parameters/DescriptorNumbers are assigned as properties with the getters returning the parameter | ||
| but the setter only setting the value of the parameter. | ||
| e.g. | ||
| ```python | ||
| @property | ||
| def my_param(self) -> Parameter: | ||
| return self._my_param | ||
|
|
||
| @my_param.setter | ||
| def my_param(self, new_value: float) -> None: | ||
| self._my_param.value = new_value | ||
| ``` | ||
| """ | ||
|
|
||
| def __init__(self, unique_name: Optional[str] = None, display_name: Optional[str] = None): | ||
| super().__init__(unique_name=unique_name, display_name=display_name) | ||
|
|
||
| def get_all_variables(self) -> List[DescriptorBase]: | ||
| """ | ||
| Get all `Descriptor` and `Parameter` objects as a list. | ||
|
|
||
| :return: List of `Descriptor` and `Parameter` objects. | ||
| """ | ||
| vars = [] | ||
| for attr_name in dir(self): | ||
| attr = getattr(self, attr_name) | ||
| if isinstance(attr, DescriptorBase): | ||
| vars.append(attr) | ||
| elif hasattr(attr, 'get_all_variables'): | ||
| vars += attr.get_all_variables() | ||
| return vars | ||
|
|
||
| def get_all_parameters(self) -> List[Parameter]: | ||
| """ | ||
| Get all `Parameter` objects as a list. | ||
|
|
||
| :return: List of `Parameter` objects. | ||
| """ | ||
| return [param for param in self.get_all_variables() if isinstance(param, Parameter)] | ||
|
|
||
| def get_fittable_parameters(self) -> List[Parameter]: | ||
| """ | ||
| Get all parameters which can be fitted as a list. | ||
|
|
||
| :return: List of `Parameter` objects. | ||
| """ | ||
| return [param for param in self.get_all_parameters() if param.independent] | ||
|
|
||
| def get_free_parameters(self) -> List[Parameter]: | ||
| """ | ||
| Get all parameters which are currently free to be fitted as a list. | ||
|
|
||
| :return: List of `Parameter` objects. | ||
| """ | ||
| return [param for param in self.get_fittable_parameters() if not param.fixed] | ||
|
|
||
| def get_fit_parameters(self) -> List[Parameter]: | ||
| """ | ||
| This is an alias for `get_free_parameters`. | ||
| To be removed when fully moved to new base classes and minimizer can be changed. | ||
| """ | ||
| return self.get_free_parameters() | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, obj_dict: Dict[str, Any]) -> ModelBase: | ||
| """ | ||
| Re-create an EasyScience object with DescriptorNumber attributes from a full encoded dictionary. | ||
|
|
||
| :param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object | ||
| :return: Reformed EasyScience object | ||
| """ | ||
| if not SerializerBase._is_serialized_easyscience_object(obj_dict): | ||
| raise ValueError('Input must be a dictionary representing an EasyScience object.') | ||
| if obj_dict['@class'] == cls.__name__: | ||
| kwargs = SerializerBase.deserialize_dict(obj_dict) | ||
| parameter_placeholder = {} | ||
| for key, value in kwargs.items(): | ||
| if isinstance(value, DescriptorNumber): | ||
| parameter_placeholder[key] = value | ||
| kwargs[key] = value.value | ||
| cls_instance = cls(**kwargs) | ||
| for key, value in parameter_placeholder.items(): | ||
| try: | ||
| temp_param = getattr(cls_instance, key) | ||
| setattr(cls_instance, '_' + key, value) | ||
| cls_instance._global_object.map.prune(temp_param.unique_name) | ||
| except Exception as e: | ||
| raise SyntaxError(f"""Could not set parameter {key} during `from_dict` with full deserialized variable. \n' | ||
| This should be fixed in the class definition. Error: {e}""") from e | ||
| return cls_instance | ||
| else: | ||
| raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| from __future__ import annotations | ||
|
|
||
| # SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software> | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience | ||
| from inspect import signature | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Any | ||
| from typing import Dict | ||
| from typing import Iterable | ||
| from typing import List | ||
| from typing import Optional | ||
| from typing import Set | ||
|
|
||
| from easyscience import global_object | ||
|
|
||
| from ..global_object.undo_redo import property_stack | ||
| from ..io.serializer_base import SerializerBase | ||
|
|
||
|
|
||
| class NewBase: | ||
| """ | ||
| This is the new base class for easyscience objects. | ||
| It provides serialization capabilities as well as unique naming and display naming. | ||
| """ | ||
rozyczko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def __init__(self, unique_name: Optional[str] = None, display_name: Optional[str] = None): | ||
| self._global_object = global_object | ||
| if unique_name is None: | ||
rozyczko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| unique_name = self._global_object.generate_unique_name(self.__class__.__name__) | ||
| self._default_unique_name = True | ||
| else: | ||
| self._default_unique_name = False | ||
| if not isinstance(unique_name, str): | ||
| raise TypeError('Unique name has to be a string.') | ||
| self._unique_name = unique_name | ||
| self._global_object.map.add_vertex(self, obj_type='created') | ||
| if display_name is not None and not isinstance(display_name, str): | ||
| raise TypeError('Display name must be a string or None') | ||
| self._display_name = display_name | ||
|
|
||
| @property | ||
| def _arg_spec(self) -> Set[str]: | ||
| """ | ||
| This method is used by the serializer to determine which arguments are needed | ||
| by the constructor to deserialize the object. | ||
| """ | ||
| sign = signature(self.__class__.__init__) | ||
| names = [param.name for param in sign.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD] | ||
| return set(names[1:]) | ||
|
|
||
| @property | ||
| def unique_name(self) -> str: | ||
| """Get the unique name of the object.""" | ||
| return self._unique_name | ||
|
|
||
| @unique_name.setter | ||
| def unique_name(self, new_unique_name: str): | ||
| """Set a new unique name for the object. The old name is still kept in the map. | ||
|
|
||
| :param new_unique_name: New unique name for the object""" | ||
| if not isinstance(new_unique_name, str): | ||
| raise TypeError('Unique name has to be a string.') | ||
| self._unique_name = new_unique_name | ||
| self._global_object.map.add_vertex(self) | ||
| self._default_unique_name = False | ||
|
|
||
| @property | ||
| def display_name(self) -> str: | ||
| """ | ||
| Get a pretty display name. | ||
|
|
||
| :return: The pretty display name. | ||
| """ | ||
| display_name = self._display_name | ||
| if display_name is None: | ||
| display_name = self.unique_name | ||
| return display_name | ||
|
|
||
| @display_name.setter | ||
| @property_stack | ||
| def display_name(self, name: str | None) -> None: | ||
| """ | ||
| Set the pretty display name. | ||
|
|
||
| :param name: Pretty display name of the object. | ||
| """ | ||
| if name is not None and not isinstance(name, str): | ||
| raise TypeError('Display name must be a string or None') | ||
| self._display_name = name | ||
|
|
||
| def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: | ||
| """ | ||
| Convert an EasyScience object into a full dictionary using `SerializerBase`s generic `convert_to_dict` method. | ||
|
|
||
| :param skip: List of field names as strings to skip when forming the dictionary | ||
| :return: encoded object containing all information to reform an EasyScience object. | ||
| """ | ||
| serializer = SerializerBase() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we instantiate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's only
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _convert_to_dict only uses self to later refer where to pull |
||
| if skip is None: | ||
| skip = [] | ||
| if self._default_unique_name and 'unique_name' not in skip: | ||
| skip.append('unique_name') | ||
| if self._display_name is None: | ||
| skip.append('display_name') | ||
| return serializer._convert_to_dict(self, skip=skip, full_encode=False) | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase: | ||
| """ | ||
| Re-create an EasyScience object from a full encoded dictionary. | ||
|
|
||
| :param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object | ||
| :return: Reformed EasyScience object | ||
| """ | ||
| if not SerializerBase._is_serialized_easyscience_object(obj_dict): | ||
| raise ValueError('Input must be a dictionary representing an EasyScience object.') | ||
| if obj_dict['@class'] == cls.__name__: | ||
| kwargs = SerializerBase.deserialize_dict(obj_dict) | ||
| return cls(**kwargs) | ||
| else: | ||
| raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.') | ||
|
|
||
| def __dir__(self) -> Iterable[str]: | ||
| """ | ||
| This creates auto-completion and helps out in iPython notebooks. | ||
|
|
||
| :return: list of function and parameter names for auto-completion | ||
| """ | ||
| new_class_objs = list(k for k in dir(self.__class__) if not k.startswith('_')) | ||
| return sorted(new_class_objs) | ||
|
|
||
| def __copy__(self) -> NewBase: | ||
| """Return a copy of the object.""" | ||
| temp = self.to_dict(skip=['unique_name']) | ||
| new_obj = self.__class__.from_dict(temp) | ||
| return new_obj | ||
|
|
||
| def __deepcopy__(self, memo): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my linter complains for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But in this context, memo is not used. I understand that it's necessary to have as a parameter, but the fact that it's not used in the method body leads to a linter problem. tbh i don't think that's an issue, just a reminder in case |
||
| return self.__copy__() | ||
|
|
||
| def __repr__(self) -> str: | ||
| return f'{self.__class__.__name__} `{self.unique_name}`' | ||
rozyczko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.