easyscience 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easyscience-2.0.0 → easyscience-2.2.0}/.gitignore +3 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/PKG-INFO +12 -2
- {easyscience-2.0.0 → easyscience-2.2.0}/README.md +6 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/pyproject.toml +8 -1
- easyscience-2.2.0/src/easyscience/__version__.py +1 -0
- easyscience-2.2.0/src/easyscience/base_classes/__init__.py +8 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/based_base.py +19 -4
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/collection_base.py +15 -11
- easyscience-2.2.0/src/easyscience/base_classes/easy_list.py +281 -0
- easyscience-2.2.0/src/easyscience/base_classes/model_base.py +119 -0
- easyscience-2.2.0/src/easyscience/base_classes/new_base.py +145 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/obj_base.py +7 -15
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/fitter.py +1 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/__init__.py +4 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_base.py +1 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_bumps.py +10 -3
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_dfo.py +14 -7
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_lmfit.py +14 -4
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/utils.py +1 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/__init__.py +1 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/hugger.py +18 -22
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/property.py +66 -90
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/logger.py +1 -3
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/map.py +83 -36
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/undo_redo.py +7 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_base.py +74 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_component.py +2 -3
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_dict.py +3 -3
- easyscience-2.2.0/src/easyscience/job/__init__.py +8 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/analysis.py +6 -13
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/experiment.py +2 -3
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/job.py +9 -8
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/theoreticalmodel.py +3 -3
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/dict.py +9 -14
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/models/__init__.py +3 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/models/polynomial.py +1 -2
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/__init__.py +1 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/classUtils.py +1 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/decorators.py +4 -6
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/__init__.py +2 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_any_type.py +5 -6
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_array.py +106 -102
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_bool.py +1 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_number.py +23 -11
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/parameter.py +280 -60
- easyscience-2.2.0/src/easyscience/variable/parameter_dependency_resolver.py +147 -0
- easyscience-2.0.0/src/easyscience/__version__.py +0 -1
- easyscience-2.0.0/src/easyscience/base_classes/__init__.py +0 -9
- easyscience-2.0.0/src/easyscience/job/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/LICENSE +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/available_minimizers.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/interface_factory.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/factory.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/multi_fitter.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/global_object.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/__init__.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/json.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/legacy_core.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/xml.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/classTools.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/string.py +0 -0
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_base.py +1 -1
- {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_str.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: easyscience
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Generic logic for easyScience libraries
|
|
5
5
|
Project-URL: homepage, https://github.com/EasyScience/EasyScience
|
|
6
6
|
Project-URL: documentation, https://easyscience.github.io/EasyScience/
|
|
@@ -52,10 +52,15 @@ Requires-Dist: lmfit
|
|
|
52
52
|
Requires-Dist: numpy
|
|
53
53
|
Requires-Dist: scipp
|
|
54
54
|
Requires-Dist: uncertainties
|
|
55
|
+
Provides-Extra: build
|
|
56
|
+
Requires-Dist: build; extra == 'build'
|
|
57
|
+
Requires-Dist: hatchling<=1.21.0; extra == 'build'
|
|
58
|
+
Requires-Dist: setuptools-git-versioning; extra == 'build'
|
|
55
59
|
Provides-Extra: dev
|
|
56
60
|
Requires-Dist: build; extra == 'dev'
|
|
57
61
|
Requires-Dist: codecov; extra == 'dev'
|
|
58
62
|
Requires-Dist: flake8; extra == 'dev'
|
|
63
|
+
Requires-Dist: jupyterlab; extra == 'dev'
|
|
59
64
|
Requires-Dist: matplotlib; extra == 'dev'
|
|
60
65
|
Requires-Dist: pytest; extra == 'dev'
|
|
61
66
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
@@ -73,6 +78,7 @@ Description-Content-Type: text/markdown
|
|
|
73
78
|
[](CODE_OF_CONDUCT.md)
|
|
74
79
|
[](https://pypi.python.org/pypi/EasyScience)
|
|
75
80
|
[](LICENSE)
|
|
81
|
+
[](https://codecov.io/github/EasyScience/corelib)
|
|
76
82
|
|
|
77
83
|
# Easyscience
|
|
78
84
|
|
|
@@ -91,6 +97,10 @@ Or direct from the repository:
|
|
|
91
97
|
|
|
92
98
|
```pip install https://github.com/easyScience/EasyScience```
|
|
93
99
|
|
|
100
|
+
### Development
|
|
101
|
+
|
|
102
|
+
For development setup and workflow instructions, please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
103
|
+
|
|
94
104
|
## Test
|
|
95
105
|
|
|
96
106
|
After installation, launch the test suite:
|
|
@@ -101,7 +111,7 @@ After installation, launch the test suite:
|
|
|
101
111
|
|
|
102
112
|
Documentation can be found at:
|
|
103
113
|
|
|
104
|
-
[https://easyScience.github.io/
|
|
114
|
+
[https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
|
|
105
115
|
|
|
106
116
|
## Contributing
|
|
107
117
|
We absolutely welcome contributions. **EasyScience** is maintained by the ESS and on a volunteer basis and thus we need to foster a community that can support user questions and develop new features to make this software a useful tool for all users while encouraging every member of the community to share their ideas.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[](CODE_OF_CONDUCT.md)
|
|
2
2
|
[](https://pypi.python.org/pypi/EasyScience)
|
|
3
3
|
[](LICENSE)
|
|
4
|
+
[](https://codecov.io/github/EasyScience/corelib)
|
|
4
5
|
|
|
5
6
|
# Easyscience
|
|
6
7
|
|
|
@@ -19,6 +20,10 @@ Or direct from the repository:
|
|
|
19
20
|
|
|
20
21
|
```pip install https://github.com/easyScience/EasyScience```
|
|
21
22
|
|
|
23
|
+
### Development
|
|
24
|
+
|
|
25
|
+
For development setup and workflow instructions, please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
26
|
+
|
|
22
27
|
## Test
|
|
23
28
|
|
|
24
29
|
After installation, launch the test suite:
|
|
@@ -29,7 +34,7 @@ After installation, launch the test suite:
|
|
|
29
34
|
|
|
30
35
|
Documentation can be found at:
|
|
31
36
|
|
|
32
|
-
[https://easyScience.github.io/
|
|
37
|
+
[https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
|
|
33
38
|
|
|
34
39
|
## Contributing
|
|
35
40
|
We absolutely welcome contributions. **EasyScience** is maintained by the ESS and on a volunteer basis and thus we need to foster a community that can support user questions and develop new features to make this software a useful tool for all users while encouraging every member of the community to share their ideas.
|
|
@@ -38,6 +38,11 @@ dependencies = [
|
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
|
+
build = [
|
|
42
|
+
"hatchling <= 1.21.0",
|
|
43
|
+
"setuptools-git-versioning",
|
|
44
|
+
"build"
|
|
45
|
+
]
|
|
41
46
|
dev = [
|
|
42
47
|
"build",
|
|
43
48
|
"codecov",
|
|
@@ -46,7 +51,8 @@ dev = [
|
|
|
46
51
|
"pytest",
|
|
47
52
|
"pytest-cov",
|
|
48
53
|
"ruff",
|
|
49
|
-
"tox-gh-actions"
|
|
54
|
+
"tox-gh-actions",
|
|
55
|
+
"jupyterlab"
|
|
50
56
|
]
|
|
51
57
|
docs = [
|
|
52
58
|
"doc8",
|
|
@@ -136,6 +142,7 @@ python =
|
|
|
136
142
|
PLATFORM =
|
|
137
143
|
ubuntu-latest: linux
|
|
138
144
|
macos-latest: macos
|
|
145
|
+
macos-15-intel: macos-intel
|
|
139
146
|
windows-latest: windows
|
|
140
147
|
[testenv]
|
|
141
148
|
passenv =
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.2.0'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .based_base import BasedBase
|
|
2
|
+
from .collection_base import CollectionBase
|
|
3
|
+
from .easy_list import EasyList
|
|
4
|
+
from .model_base import ModelBase
|
|
5
|
+
from .new_base import NewBase
|
|
6
|
+
from .obj_base import ObjBase
|
|
7
|
+
|
|
8
|
+
__all__ = [BasedBase, CollectionBase, ObjBase, ModelBase, NewBase, EasyList]
|
|
@@ -3,8 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
# SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
|
|
4
4
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
5
|
# © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
|
|
6
|
-
from inspect import
|
|
6
|
+
from inspect import signature
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import Any
|
|
9
|
+
from typing import Dict
|
|
8
10
|
from typing import Iterable
|
|
9
11
|
from typing import List
|
|
10
12
|
from typing import Optional
|
|
@@ -38,9 +40,9 @@ class BasedBase(SerializerComponent):
|
|
|
38
40
|
@property
|
|
39
41
|
def _arg_spec(self) -> Set[str]:
|
|
40
42
|
base_cls = getattr(self, '__old_class__', self.__class__)
|
|
41
|
-
|
|
42
|
-
names =
|
|
43
|
-
return names
|
|
43
|
+
sign = signature(base_cls.__init__)
|
|
44
|
+
names = [param.name for param in sign.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD]
|
|
45
|
+
return set(names[1:])
|
|
44
46
|
|
|
45
47
|
def __reduce__(self):
|
|
46
48
|
"""
|
|
@@ -195,4 +197,17 @@ class BasedBase(SerializerComponent):
|
|
|
195
197
|
new_obj = self.__class__.from_dict(temp)
|
|
196
198
|
return new_obj
|
|
197
199
|
|
|
200
|
+
def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Convert an object into a full dictionary using `SerializerDict`.
|
|
203
|
+
This is a shortcut for ```obj.encode(encoder=SerializerDict)```
|
|
198
204
|
|
|
205
|
+
:param skip: List of field names as strings to skip when forming the dictionary
|
|
206
|
+
:return: encoded object containing all information to reform an EasyScience object.
|
|
207
|
+
"""
|
|
208
|
+
# extend skip to include unique_name by default
|
|
209
|
+
if skip is None:
|
|
210
|
+
skip = []
|
|
211
|
+
if 'unique_name' not in skip:
|
|
212
|
+
skip.append('unique_name')
|
|
213
|
+
return super().as_dict(skip=skip)
|
|
@@ -14,6 +14,7 @@ from typing import Optional
|
|
|
14
14
|
from typing import Tuple
|
|
15
15
|
from typing import Union
|
|
16
16
|
|
|
17
|
+
from easyscience.base_classes.new_base import NewBase
|
|
17
18
|
from easyscience.global_object.undo_redo import NotarizedDict
|
|
18
19
|
|
|
19
20
|
from ..variable.descriptor_base import DescriptorBase
|
|
@@ -23,7 +24,6 @@ if TYPE_CHECKING:
|
|
|
23
24
|
from ..fitting.calculators import InterfaceFactoryTemplate
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
|
|
27
27
|
class CollectionBase(BasedBase, MutableSequence):
|
|
28
28
|
"""
|
|
29
29
|
This is the base class for which all higher level classes are built off of.
|
|
@@ -35,7 +35,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
35
35
|
def __init__(
|
|
36
36
|
self,
|
|
37
37
|
name: str,
|
|
38
|
-
*args: Union[BasedBase, DescriptorBase],
|
|
38
|
+
*args: Union[BasedBase, DescriptorBase, NewBase],
|
|
39
39
|
interface: Optional[InterfaceFactoryTemplate] = None,
|
|
40
40
|
unique_name: Optional[str] = None,
|
|
41
41
|
**kwargs,
|
|
@@ -65,7 +65,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
65
65
|
_kwargs[key] = item
|
|
66
66
|
kwargs = _kwargs
|
|
67
67
|
for item in list(kwargs.values()) + _args:
|
|
68
|
-
if not issubclass(type(item), (DescriptorBase, BasedBase)):
|
|
68
|
+
if not issubclass(type(item), (DescriptorBase, BasedBase, NewBase)):
|
|
69
69
|
raise AttributeError('A collection can only be formed from easyscience objects.')
|
|
70
70
|
args = _args
|
|
71
71
|
_kwargs = {}
|
|
@@ -91,19 +91,19 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
91
91
|
self.interface = interface
|
|
92
92
|
self._kwargs._stack_enabled = True
|
|
93
93
|
|
|
94
|
-
def insert(self, index: int, value: Union[DescriptorBase, BasedBase]) -> None:
|
|
94
|
+
def insert(self, index: int, value: Union[DescriptorBase, BasedBase, NewBase]) -> None:
|
|
95
95
|
"""
|
|
96
96
|
Insert an object into the collection at an index.
|
|
97
97
|
|
|
98
98
|
:param index: Index for EasyScience object to be inserted.
|
|
99
99
|
:type index: int
|
|
100
100
|
:param value: Object to be inserted.
|
|
101
|
-
:type value: Union[BasedBase, DescriptorBase]
|
|
101
|
+
:type value: Union[BasedBase, DescriptorBase, NewBase]
|
|
102
102
|
:return: None
|
|
103
103
|
:rtype: None
|
|
104
104
|
"""
|
|
105
105
|
t_ = type(value)
|
|
106
|
-
if issubclass(t_, (BasedBase, DescriptorBase)):
|
|
106
|
+
if issubclass(t_, (BasedBase, DescriptorBase, NewBase)):
|
|
107
107
|
update_key = list(self._kwargs.keys())
|
|
108
108
|
values = list(self._kwargs.values())
|
|
109
109
|
# Update the internal dict
|
|
@@ -118,7 +118,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
118
118
|
else:
|
|
119
119
|
raise AttributeError('Only EasyScience objects can be put into an EasyScience group')
|
|
120
120
|
|
|
121
|
-
def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase]:
|
|
121
|
+
def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase, NewBase]:
|
|
122
122
|
"""
|
|
123
123
|
Get an item in the collection based on its index.
|
|
124
124
|
|
|
@@ -152,7 +152,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
152
152
|
keys = list(self._kwargs.keys())
|
|
153
153
|
return self._kwargs[keys[idx]]
|
|
154
154
|
|
|
155
|
-
def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase]) -> None:
|
|
155
|
+
def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase, NewBase]) -> None:
|
|
156
156
|
"""
|
|
157
157
|
Set an item via it's index.
|
|
158
158
|
|
|
@@ -164,7 +164,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
164
164
|
if isinstance(value, Number): # noqa: S3827
|
|
165
165
|
item = self.__getitem__(key)
|
|
166
166
|
item.value = value
|
|
167
|
-
elif issubclass(type(value), (BasedBase, DescriptorBase)):
|
|
167
|
+
elif issubclass(type(value), (BasedBase, DescriptorBase, NewBase)):
|
|
168
168
|
update_key = list(self._kwargs.keys())
|
|
169
169
|
values = list(self._kwargs.values())
|
|
170
170
|
old_item = values[key]
|
|
@@ -232,9 +232,13 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
232
232
|
return tuple(self._kwargs.values())
|
|
233
233
|
|
|
234
234
|
def __repr__(self) -> str:
|
|
235
|
-
return f
|
|
235
|
+
return f'{self.__class__.__name__} `{getattr(self, "name")}` of length {len(self)}'
|
|
236
236
|
|
|
237
|
-
def sort(
|
|
237
|
+
def sort(
|
|
238
|
+
self,
|
|
239
|
+
mapping: Callable[[Union[BasedBase, DescriptorBase, NewBase]], Any],
|
|
240
|
+
reverse: bool = False,
|
|
241
|
+
) -> None:
|
|
238
242
|
"""
|
|
239
243
|
Sort the collection according to the given mapping.
|
|
240
244
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import MutableSequence
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import Callable
|
|
12
|
+
from typing import Dict
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
from typing import List
|
|
15
|
+
from typing import Optional
|
|
16
|
+
from typing import Type
|
|
17
|
+
from typing import TypeVar
|
|
18
|
+
from typing import overload
|
|
19
|
+
|
|
20
|
+
from easyscience.io.serializer_base import SerializerBase
|
|
21
|
+
|
|
22
|
+
from .new_base import NewBase
|
|
23
|
+
|
|
24
|
+
ProtectedType_ = TypeVar('ProtectedType', bound=NewBase)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EasyList(NewBase, MutableSequence[ProtectedType_]):
|
|
28
|
+
# If we were to inherit from List instead of MutableSequence,
|
|
29
|
+
# we would have to overwrite "extend", "remove", "__iadd__", "count", "append", "__iter__" and "clear"
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*args: ProtectedType_ | list[ProtectedType_],
|
|
33
|
+
protected_types: list[Type[NewBase]] | Type[NewBase] | None = None,
|
|
34
|
+
unique_name: Optional[str] = None,
|
|
35
|
+
display_name: Optional[str] = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the EasyList.
|
|
40
|
+
:param args: Initial items to add to the list
|
|
41
|
+
:param protected_types: Types that are allowed in the list. Can be a single NewBase subclass or a list of them.
|
|
42
|
+
If None, defaults to [NewBase].
|
|
43
|
+
:param unique_name: Optional unique name for the list
|
|
44
|
+
:param display_name: Optional display name for the list
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(unique_name=unique_name, display_name=display_name)
|
|
47
|
+
if protected_types is None:
|
|
48
|
+
self._protected_types = [NewBase]
|
|
49
|
+
elif isinstance(protected_types, type) and issubclass(protected_types, NewBase):
|
|
50
|
+
self._protected_types = [protected_types]
|
|
51
|
+
elif isinstance(protected_types, Iterable) and all(issubclass(t, NewBase) for t in protected_types):
|
|
52
|
+
self._protected_types = list(protected_types)
|
|
53
|
+
else:
|
|
54
|
+
raise TypeError('protected_types must be a NewBase subclass or an iterable of NewBase subclasses')
|
|
55
|
+
self._data: List[ProtectedType_] = []
|
|
56
|
+
|
|
57
|
+
# Add initial items
|
|
58
|
+
for item in args:
|
|
59
|
+
if isinstance(item, list):
|
|
60
|
+
for sub_item in item:
|
|
61
|
+
self.append(sub_item)
|
|
62
|
+
else:
|
|
63
|
+
self.append(item)
|
|
64
|
+
|
|
65
|
+
# For deserialization, the dict can't contain an *args, so we check for 'data' in kwargs
|
|
66
|
+
if 'data' in kwargs:
|
|
67
|
+
data = kwargs.pop('data')
|
|
68
|
+
for item in data:
|
|
69
|
+
self.append(item)
|
|
70
|
+
|
|
71
|
+
# MutableSequence abstract methods
|
|
72
|
+
|
|
73
|
+
# Use @overload to provide precise type hints for different __getitem__ argument types
|
|
74
|
+
@overload
|
|
75
|
+
def __getitem__(self, idx: int) -> ProtectedType_: ...
|
|
76
|
+
@overload
|
|
77
|
+
def __getitem__(self, idx: slice) -> 'EasyList[ProtectedType_]': ...
|
|
78
|
+
@overload
|
|
79
|
+
def __getitem__(self, idx: str) -> ProtectedType_: ...
|
|
80
|
+
def __getitem__(self, idx: int | slice | str) -> ProtectedType_ | 'EasyList[ProtectedType_]':
|
|
81
|
+
"""
|
|
82
|
+
Get an item by index, slice, or unique_name.
|
|
83
|
+
|
|
84
|
+
:param idx: Index, slice, or unique_name of the item
|
|
85
|
+
:return: The item or a new EasyList for slices
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(idx, int):
|
|
88
|
+
return self._data[idx]
|
|
89
|
+
elif isinstance(idx, slice):
|
|
90
|
+
return self.__class__(self._data[idx], protected_types=self._protected_types)
|
|
91
|
+
elif isinstance(idx, str):
|
|
92
|
+
element = next((r for r in self._data if self._get_key(r) == idx), None)
|
|
93
|
+
if element is not None:
|
|
94
|
+
return element
|
|
95
|
+
raise KeyError(f'No item with unique name "{idx}" found')
|
|
96
|
+
else:
|
|
97
|
+
raise TypeError('Index must be an int, slice, or str')
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def __setitem__(self, idx: int, value: ProtectedType_) -> None: ...
|
|
101
|
+
@overload
|
|
102
|
+
def __setitem__(self, idx: slice, value: Iterable[ProtectedType_]) -> None: ...
|
|
103
|
+
|
|
104
|
+
def __setitem__(self, idx: int | slice, value: ProtectedType_ | Iterable[ProtectedType_]) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Set an item at an index.
|
|
107
|
+
|
|
108
|
+
:param idx: Index to set
|
|
109
|
+
:param value: New value
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(idx, int):
|
|
112
|
+
if not isinstance(value, tuple(self._protected_types)):
|
|
113
|
+
raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
|
|
114
|
+
if value is not self._data[idx] and value in self:
|
|
115
|
+
warnings.warn(f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored')
|
|
116
|
+
return
|
|
117
|
+
self._data[idx] = value
|
|
118
|
+
elif isinstance(idx, slice):
|
|
119
|
+
if not isinstance(value, Iterable):
|
|
120
|
+
raise TypeError('Value must be an iterable for slice assignment')
|
|
121
|
+
replaced = self._data[idx]
|
|
122
|
+
new_values = list(value)
|
|
123
|
+
if len(new_values) != len(replaced):
|
|
124
|
+
raise ValueError('Length of new values must match the length of the slice being replaced')
|
|
125
|
+
for i, v in enumerate(new_values):
|
|
126
|
+
if not isinstance(v, tuple(self._protected_types)):
|
|
127
|
+
raise TypeError(f'Items must be one of {self._protected_types}, got {type(v)}')
|
|
128
|
+
if v in self and replaced[i] is not v:
|
|
129
|
+
warnings.warn(f'Item with unique name "{v.unique_name}" already in EasyList, it will be ignored')
|
|
130
|
+
new_values[i] = replaced[i] # Keep the original value if the new one is a duplicate
|
|
131
|
+
self._data[idx] = new_values
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError('Index must be an int or slice')
|
|
134
|
+
|
|
135
|
+
def __delitem__(self, idx: int | slice | str) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Delete an item by index, slice, or name.
|
|
138
|
+
|
|
139
|
+
:param idx: Index, slice, or name of item to delete
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(idx, (int, slice)):
|
|
142
|
+
del self._data[idx]
|
|
143
|
+
elif isinstance(idx, str):
|
|
144
|
+
for i, item in enumerate(self._data):
|
|
145
|
+
if self._get_key(item) == idx:
|
|
146
|
+
del self._data[i]
|
|
147
|
+
return
|
|
148
|
+
raise KeyError(f'No item with unique name "{idx}" found')
|
|
149
|
+
else:
|
|
150
|
+
raise TypeError('Index must be an int, slice, or str')
|
|
151
|
+
|
|
152
|
+
def __len__(self) -> int:
|
|
153
|
+
"""Return the number of items in the collection."""
|
|
154
|
+
return len(self._data)
|
|
155
|
+
|
|
156
|
+
def insert(self, index: int, value: ProtectedType_) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Insert an item at an index.
|
|
159
|
+
|
|
160
|
+
:param index: Index to insert at
|
|
161
|
+
:param value: Item to insert
|
|
162
|
+
"""
|
|
163
|
+
if not isinstance(index, int):
|
|
164
|
+
raise TypeError('Index must be an integer')
|
|
165
|
+
elif not isinstance(value, tuple(self._protected_types)):
|
|
166
|
+
raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
|
|
167
|
+
if value in self:
|
|
168
|
+
warnings.warn(f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored')
|
|
169
|
+
return
|
|
170
|
+
self._data.insert(index, value)
|
|
171
|
+
|
|
172
|
+
def _get_key(self, obj) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Get the unique name of an object.
|
|
175
|
+
Can be overridden to use a different attribute as the key.
|
|
176
|
+
:param object: Object to get the key for
|
|
177
|
+
:return: The key of the object
|
|
178
|
+
:rtype: str
|
|
179
|
+
"""
|
|
180
|
+
return obj.unique_name
|
|
181
|
+
|
|
182
|
+
# Overwriting methods
|
|
183
|
+
|
|
184
|
+
def __repr__(self) -> str:
|
|
185
|
+
return f'{self.__class__.__name__} of length {len(self)} of type(s) {self._protected_types}'
|
|
186
|
+
|
|
187
|
+
def __contains__(self, item: ProtectedType_ | str) -> bool:
|
|
188
|
+
if isinstance(item, str):
|
|
189
|
+
return any(self._get_key(r) == item for r in self._data)
|
|
190
|
+
return item in self._data
|
|
191
|
+
|
|
192
|
+
def __reversed__(self):
|
|
193
|
+
return self._data.__reversed__()
|
|
194
|
+
|
|
195
|
+
def sort(self, key: Callable[[ProtectedType_], Any] = None, reverse: bool = False) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Sort the collection according to the given key function.
|
|
198
|
+
|
|
199
|
+
:param key: Mapping function to sort by
|
|
200
|
+
:param reverse: Whether to reverse the sort
|
|
201
|
+
"""
|
|
202
|
+
self._data.sort(reverse=reverse, key=key)
|
|
203
|
+
|
|
204
|
+
def index(self, value: ProtectedType_ | str, start: int = 0, stop: int = None) -> int:
|
|
205
|
+
if stop is None:
|
|
206
|
+
stop = len(self._data)
|
|
207
|
+
if isinstance(value, str):
|
|
208
|
+
for i in range(start, min(stop, len(self._data))):
|
|
209
|
+
if self._get_key(self._data[i]) == value:
|
|
210
|
+
return i
|
|
211
|
+
raise ValueError(f'{value} is not in EasyList')
|
|
212
|
+
return self._data.index(value, start, stop)
|
|
213
|
+
|
|
214
|
+
def pop(self, index: int | str = -1) -> ProtectedType_:
|
|
215
|
+
"""
|
|
216
|
+
Remove and return an item at the given index or unique_name.
|
|
217
|
+
|
|
218
|
+
:param index: Index or unique_name of the item to remove
|
|
219
|
+
:return: The removed item
|
|
220
|
+
"""
|
|
221
|
+
if isinstance(index, int):
|
|
222
|
+
return self._data.pop(index)
|
|
223
|
+
elif isinstance(index, str):
|
|
224
|
+
for i, item in enumerate(self._data):
|
|
225
|
+
if self._get_key(item) == index:
|
|
226
|
+
return self._data.pop(i)
|
|
227
|
+
raise KeyError(f'No item with unique name "{index}" found')
|
|
228
|
+
else:
|
|
229
|
+
raise TypeError('Index must be an int or str')
|
|
230
|
+
|
|
231
|
+
# Serialization support
|
|
232
|
+
|
|
233
|
+
def to_dict(self) -> dict:
|
|
234
|
+
"""
|
|
235
|
+
Convert the EasyList to a dictionary for serialization.
|
|
236
|
+
|
|
237
|
+
:return: Dictionary representation of the EasyList
|
|
238
|
+
"""
|
|
239
|
+
dict_repr = super().to_dict()
|
|
240
|
+
if self._protected_types != [NewBase]:
|
|
241
|
+
dict_repr['protected_types'] = [
|
|
242
|
+
{'@module': cls_.__module__, '@class': cls_.__name__} for cls_ in self._protected_types
|
|
243
|
+
] # noqa: E501
|
|
244
|
+
dict_repr['data'] = [item.to_dict() for item in self._data]
|
|
245
|
+
return dict_repr
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase:
|
|
249
|
+
"""
|
|
250
|
+
Re-create an EasyScience object from a full encoded dictionary.
|
|
251
|
+
|
|
252
|
+
:param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object
|
|
253
|
+
:return: Reformed EasyScience object
|
|
254
|
+
"""
|
|
255
|
+
if not SerializerBase._is_serialized_easyscience_object(obj_dict):
|
|
256
|
+
raise ValueError('Input must be a dictionary representing an EasyScience EasyList object.')
|
|
257
|
+
temp_dict = copy.deepcopy(obj_dict) # Make a copy to avoid mutating the input
|
|
258
|
+
if temp_dict['@class'] == cls.__name__:
|
|
259
|
+
if 'protected_types' in temp_dict:
|
|
260
|
+
protected_types = temp_dict.pop('protected_types')
|
|
261
|
+
for i, type_dict in enumerate(protected_types):
|
|
262
|
+
if '@module' in type_dict and '@class' in type_dict:
|
|
263
|
+
modname = type_dict['@module']
|
|
264
|
+
classname = type_dict['@class']
|
|
265
|
+
mod = __import__(modname, globals(), locals(), [classname], 0)
|
|
266
|
+
if hasattr(mod, classname):
|
|
267
|
+
cls_ = getattr(mod, classname)
|
|
268
|
+
protected_types[i] = cls_
|
|
269
|
+
else:
|
|
270
|
+
raise ImportError(f'Could not import class {classname} from module {modname}')
|
|
271
|
+
else:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
'Each protected type must be a serialized EasyScience class with @module and @class keys'
|
|
274
|
+
) # noqa: E501
|
|
275
|
+
else:
|
|
276
|
+
protected_types = None
|
|
277
|
+
kwargs = SerializerBase.deserialize_dict(temp_dict)
|
|
278
|
+
data = kwargs.pop('data', [])
|
|
279
|
+
return cls(data, protected_types=protected_types, **kwargs)
|
|
280
|
+
else:
|
|
281
|
+
raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
|
|
4
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
# © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from easyscience.variable.descriptor_number import DescriptorNumber
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Dict
|
|
13
|
+
from typing import List
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from ..io import SerializerBase
|
|
17
|
+
from ..variable import Parameter
|
|
18
|
+
from ..variable.descriptor_base import DescriptorBase
|
|
19
|
+
from .new_base import NewBase
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModelBase(NewBase):
|
|
23
|
+
"""
|
|
24
|
+
This is the base class for all model classes in EasyScience.
|
|
25
|
+
It provides methods to get parameters for fitting and analysis as well as proper serialization/deserialization for
|
|
26
|
+
DescriptorNumber/Parameter attributes.
|
|
27
|
+
|
|
28
|
+
It assumes that Parameters/DescriptorNumbers are assigned as properties with the getters returning the parameter
|
|
29
|
+
but the setter only setting the value of the parameter.
|
|
30
|
+
e.g.
|
|
31
|
+
```python
|
|
32
|
+
@property
|
|
33
|
+
def my_param(self) -> Parameter:
|
|
34
|
+
return self._my_param
|
|
35
|
+
|
|
36
|
+
@my_param.setter
|
|
37
|
+
def my_param(self, new_value: float) -> None:
|
|
38
|
+
self._my_param.value = new_value
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, unique_name: Optional[str] = None, display_name: Optional[str] = None):
|
|
43
|
+
super().__init__(unique_name=unique_name, display_name=display_name)
|
|
44
|
+
|
|
45
|
+
def get_all_variables(self) -> List[DescriptorBase]:
|
|
46
|
+
"""
|
|
47
|
+
Get all `Descriptor` and `Parameter` objects as a list.
|
|
48
|
+
|
|
49
|
+
:return: List of `Descriptor` and `Parameter` objects.
|
|
50
|
+
"""
|
|
51
|
+
vars = []
|
|
52
|
+
for attr_name in dir(self):
|
|
53
|
+
attr = getattr(self, attr_name)
|
|
54
|
+
if isinstance(attr, DescriptorBase):
|
|
55
|
+
vars.append(attr)
|
|
56
|
+
elif hasattr(attr, 'get_all_variables'):
|
|
57
|
+
vars += attr.get_all_variables()
|
|
58
|
+
return vars
|
|
59
|
+
|
|
60
|
+
def get_all_parameters(self) -> List[Parameter]:
|
|
61
|
+
"""
|
|
62
|
+
Get all `Parameter` objects as a list.
|
|
63
|
+
|
|
64
|
+
:return: List of `Parameter` objects.
|
|
65
|
+
"""
|
|
66
|
+
return [param for param in self.get_all_variables() if isinstance(param, Parameter)]
|
|
67
|
+
|
|
68
|
+
def get_fittable_parameters(self) -> List[Parameter]:
|
|
69
|
+
"""
|
|
70
|
+
Get all parameters which can be fitted as a list.
|
|
71
|
+
|
|
72
|
+
:return: List of `Parameter` objects.
|
|
73
|
+
"""
|
|
74
|
+
return [param for param in self.get_all_parameters() if param.independent]
|
|
75
|
+
|
|
76
|
+
def get_free_parameters(self) -> List[Parameter]:
|
|
77
|
+
"""
|
|
78
|
+
Get all parameters which are currently free to be fitted as a list.
|
|
79
|
+
|
|
80
|
+
:return: List of `Parameter` objects.
|
|
81
|
+
"""
|
|
82
|
+
return [param for param in self.get_fittable_parameters() if not param.fixed]
|
|
83
|
+
|
|
84
|
+
def get_fit_parameters(self) -> List[Parameter]:
|
|
85
|
+
"""
|
|
86
|
+
This is an alias for `get_free_parameters`.
|
|
87
|
+
To be removed when fully moved to new base classes and minimizer can be changed.
|
|
88
|
+
"""
|
|
89
|
+
return self.get_free_parameters()
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, obj_dict: Dict[str, Any]) -> ModelBase:
|
|
93
|
+
"""
|
|
94
|
+
Re-create an EasyScience object with DescriptorNumber attributes from a full encoded dictionary.
|
|
95
|
+
|
|
96
|
+
:param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object
|
|
97
|
+
:return: Reformed EasyScience object
|
|
98
|
+
"""
|
|
99
|
+
if not SerializerBase._is_serialized_easyscience_object(obj_dict):
|
|
100
|
+
raise ValueError('Input must be a dictionary representing an EasyScience object.')
|
|
101
|
+
if obj_dict['@class'] == cls.__name__:
|
|
102
|
+
kwargs = SerializerBase.deserialize_dict(obj_dict)
|
|
103
|
+
parameter_placeholder = {}
|
|
104
|
+
for key, value in kwargs.items():
|
|
105
|
+
if isinstance(value, DescriptorNumber):
|
|
106
|
+
parameter_placeholder[key] = value
|
|
107
|
+
kwargs[key] = value.value
|
|
108
|
+
cls_instance = cls(**kwargs)
|
|
109
|
+
for key, value in parameter_placeholder.items():
|
|
110
|
+
try:
|
|
111
|
+
temp_param = getattr(cls_instance, key)
|
|
112
|
+
setattr(cls_instance, '_' + key, value)
|
|
113
|
+
cls_instance._global_object.map.prune(temp_param.unique_name)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise SyntaxError(f"""Could not set parameter {key} during `from_dict` with full deserialized variable. \n'
|
|
116
|
+
This should be fixed in the class definition. Error: {e}""") from e
|
|
117
|
+
return cls_instance
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')
|