easyscience 2.1.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.1.0 → easyscience-2.2.0}/PKG-INFO +3 -2
- {easyscience-2.1.0 → easyscience-2.2.0}/README.md +2 -1
- easyscience-2.2.0/src/easyscience/__version__.py +1 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/__init__.py +2 -7
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/collection_base.py +14 -9
- easyscience-2.2.0/src/easyscience/base_classes/easy_list.py +281 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_bumps.py +8 -1
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_dfo.py +11 -3
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_lmfit.py +11 -1
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/map.py +79 -32
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_number.py +1 -4
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/parameter.py +81 -8
- easyscience-2.1.0/src/easyscience/__version__.py +0 -1
- {easyscience-2.1.0 → easyscience-2.2.0}/.gitignore +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/LICENSE +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/pyproject.toml +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/based_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/model_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/new_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/base_classes/obj_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/available_minimizers.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/interface_factory.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/fitter.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/factory.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/utils.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/multi_fitter.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/global_object.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/hugger.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/property.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/logger.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/global_object/undo_redo.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/io/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/io/serializer_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/io/serializer_component.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/io/serializer_dict.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/job/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/job/analysis.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/job/experiment.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/job/job.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/job/theoreticalmodel.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/legacy/dict.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/legacy/json.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/legacy/legacy_core.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/legacy/xml.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/models/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/models/polynomial.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/utils/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/utils/classTools.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/utils/classUtils.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/utils/decorators.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/utils/string.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/__init__.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_any_type.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_array.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_base.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_bool.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_str.py +0 -0
- {easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/parameter_dependency_resolver.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/
|
|
@@ -78,6 +78,7 @@ Description-Content-Type: text/markdown
|
|
|
78
78
|
[](CODE_OF_CONDUCT.md)
|
|
79
79
|
[](https://pypi.python.org/pypi/EasyScience)
|
|
80
80
|
[](LICENSE)
|
|
81
|
+
[](https://codecov.io/github/EasyScience/corelib)
|
|
81
82
|
|
|
82
83
|
# Easyscience
|
|
83
84
|
|
|
@@ -110,7 +111,7 @@ After installation, launch the test suite:
|
|
|
110
111
|
|
|
111
112
|
Documentation can be found at:
|
|
112
113
|
|
|
113
|
-
[https://easyScience.github.io/
|
|
114
|
+
[https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
|
|
114
115
|
|
|
115
116
|
## Contributing
|
|
116
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
|
|
|
@@ -33,7 +34,7 @@ After installation, launch the test suite:
|
|
|
33
34
|
|
|
34
35
|
Documentation can be found at:
|
|
35
36
|
|
|
36
|
-
[https://easyScience.github.io/
|
|
37
|
+
[https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
|
|
37
38
|
|
|
38
39
|
## Contributing
|
|
39
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.2.0'
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
from .based_base import BasedBase
|
|
2
2
|
from .collection_base import CollectionBase
|
|
3
|
+
from .easy_list import EasyList
|
|
3
4
|
from .model_base import ModelBase
|
|
4
5
|
from .new_base import NewBase
|
|
5
6
|
from .obj_base import ObjBase
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
-
BasedBase,
|
|
9
|
-
CollectionBase,
|
|
10
|
-
ObjBase,
|
|
11
|
-
ModelBase,
|
|
12
|
-
NewBase,
|
|
13
|
-
]
|
|
8
|
+
__all__ = [BasedBase, CollectionBase, ObjBase, ModelBase, NewBase, EasyList]
|
|
@@ -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
|
|
@@ -34,7 +35,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
34
35
|
def __init__(
|
|
35
36
|
self,
|
|
36
37
|
name: str,
|
|
37
|
-
*args: Union[BasedBase, DescriptorBase],
|
|
38
|
+
*args: Union[BasedBase, DescriptorBase, NewBase],
|
|
38
39
|
interface: Optional[InterfaceFactoryTemplate] = None,
|
|
39
40
|
unique_name: Optional[str] = None,
|
|
40
41
|
**kwargs,
|
|
@@ -64,7 +65,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
64
65
|
_kwargs[key] = item
|
|
65
66
|
kwargs = _kwargs
|
|
66
67
|
for item in list(kwargs.values()) + _args:
|
|
67
|
-
if not issubclass(type(item), (DescriptorBase, BasedBase)):
|
|
68
|
+
if not issubclass(type(item), (DescriptorBase, BasedBase, NewBase)):
|
|
68
69
|
raise AttributeError('A collection can only be formed from easyscience objects.')
|
|
69
70
|
args = _args
|
|
70
71
|
_kwargs = {}
|
|
@@ -90,19 +91,19 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
90
91
|
self.interface = interface
|
|
91
92
|
self._kwargs._stack_enabled = True
|
|
92
93
|
|
|
93
|
-
def insert(self, index: int, value: Union[DescriptorBase, BasedBase]) -> None:
|
|
94
|
+
def insert(self, index: int, value: Union[DescriptorBase, BasedBase, NewBase]) -> None:
|
|
94
95
|
"""
|
|
95
96
|
Insert an object into the collection at an index.
|
|
96
97
|
|
|
97
98
|
:param index: Index for EasyScience object to be inserted.
|
|
98
99
|
:type index: int
|
|
99
100
|
:param value: Object to be inserted.
|
|
100
|
-
:type value: Union[BasedBase, DescriptorBase]
|
|
101
|
+
:type value: Union[BasedBase, DescriptorBase, NewBase]
|
|
101
102
|
:return: None
|
|
102
103
|
:rtype: None
|
|
103
104
|
"""
|
|
104
105
|
t_ = type(value)
|
|
105
|
-
if issubclass(t_, (BasedBase, DescriptorBase)):
|
|
106
|
+
if issubclass(t_, (BasedBase, DescriptorBase, NewBase)):
|
|
106
107
|
update_key = list(self._kwargs.keys())
|
|
107
108
|
values = list(self._kwargs.values())
|
|
108
109
|
# Update the internal dict
|
|
@@ -117,7 +118,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
117
118
|
else:
|
|
118
119
|
raise AttributeError('Only EasyScience objects can be put into an EasyScience group')
|
|
119
120
|
|
|
120
|
-
def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase]:
|
|
121
|
+
def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase, NewBase]:
|
|
121
122
|
"""
|
|
122
123
|
Get an item in the collection based on its index.
|
|
123
124
|
|
|
@@ -151,7 +152,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
151
152
|
keys = list(self._kwargs.keys())
|
|
152
153
|
return self._kwargs[keys[idx]]
|
|
153
154
|
|
|
154
|
-
def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase]) -> None:
|
|
155
|
+
def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase, NewBase]) -> None:
|
|
155
156
|
"""
|
|
156
157
|
Set an item via it's index.
|
|
157
158
|
|
|
@@ -163,7 +164,7 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
163
164
|
if isinstance(value, Number): # noqa: S3827
|
|
164
165
|
item = self.__getitem__(key)
|
|
165
166
|
item.value = value
|
|
166
|
-
elif issubclass(type(value), (BasedBase, DescriptorBase)):
|
|
167
|
+
elif issubclass(type(value), (BasedBase, DescriptorBase, NewBase)):
|
|
167
168
|
update_key = list(self._kwargs.keys())
|
|
168
169
|
values = list(self._kwargs.values())
|
|
169
170
|
old_item = values[key]
|
|
@@ -233,7 +234,11 @@ class CollectionBase(BasedBase, MutableSequence):
|
|
|
233
234
|
def __repr__(self) -> str:
|
|
234
235
|
return f'{self.__class__.__name__} `{getattr(self, "name")}` of length {len(self)}'
|
|
235
236
|
|
|
236
|
-
def sort(
|
|
237
|
+
def sort(
|
|
238
|
+
self,
|
|
239
|
+
mapping: Callable[[Union[BasedBase, DescriptorBase, NewBase]], Any],
|
|
240
|
+
reverse: bool = False,
|
|
241
|
+
) -> None:
|
|
237
242
|
"""
|
|
238
243
|
Sort the collection according to the given mapping.
|
|
239
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__}.')
|
{easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_bumps.py
RENAMED
|
@@ -98,6 +98,11 @@ class Bumps(MinimizerBase):
|
|
|
98
98
|
:type method: str
|
|
99
99
|
:return: Fit results
|
|
100
100
|
:rtype: ModelResult
|
|
101
|
+
|
|
102
|
+
For standard least squares, the weights should be 1/sigma, where
|
|
103
|
+
sigma is the standard deviation of the measurement. For
|
|
104
|
+
unweighted least squares, these should be 1.
|
|
105
|
+
|
|
101
106
|
"""
|
|
102
107
|
method_dict = self._get_method_kwargs(method)
|
|
103
108
|
|
|
@@ -191,6 +196,8 @@ class Bumps(MinimizerBase):
|
|
|
191
196
|
Generate a bumps model from the supplied `fit_function` and parameters in the base object.
|
|
192
197
|
Note that this makes a callable as it needs to be initialized with *x*, *y*, *weights*
|
|
193
198
|
|
|
199
|
+
Weights are converted to dy (standard deviation of y).
|
|
200
|
+
|
|
194
201
|
:return: Callable to make a bumps Curve model
|
|
195
202
|
:rtype: Callable
|
|
196
203
|
"""
|
|
@@ -205,7 +212,7 @@ class Bumps(MinimizerBase):
|
|
|
205
212
|
else:
|
|
206
213
|
for par in parameters:
|
|
207
214
|
bumps_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = obj.convert_to_par_object(par)
|
|
208
|
-
return Curve(fit_func, x, y, dy=weights, **bumps_pars)
|
|
215
|
+
return Curve(fit_func, x, y, dy=1 / weights, **bumps_pars)
|
|
209
216
|
|
|
210
217
|
return _make_func
|
|
211
218
|
|
|
@@ -74,7 +74,7 @@ class DFO(MinimizerBase):
|
|
|
74
74
|
:type x: np.ndarray
|
|
75
75
|
:param y: measured points
|
|
76
76
|
:type y: np.ndarray
|
|
77
|
-
:param weights: Weights for supplied measured points
|
|
77
|
+
:param weights: Weights for supplied measured points.
|
|
78
78
|
:type weights: np.ndarray
|
|
79
79
|
:param model: Optional Model which is being fitted to
|
|
80
80
|
:type model: lmModel
|
|
@@ -85,6 +85,10 @@ class DFO(MinimizerBase):
|
|
|
85
85
|
:type method: str
|
|
86
86
|
:return: Fit results
|
|
87
87
|
:rtype: ModelResult
|
|
88
|
+
|
|
89
|
+
For standard least squares, the weights should be 1/sigma, where
|
|
90
|
+
sigma is the standard deviation of the measurement. For
|
|
91
|
+
unweighted least squares, these should be 1.
|
|
88
92
|
"""
|
|
89
93
|
x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights)
|
|
90
94
|
|
|
@@ -163,7 +167,7 @@ class DFO(MinimizerBase):
|
|
|
163
167
|
def _residuals(pars_values: List[float]) -> np.ndarray:
|
|
164
168
|
for idx, par_name in enumerate(dfo_pars.keys()):
|
|
165
169
|
dfo_pars[par_name] = pars_values[idx]
|
|
166
|
-
return (y - fit_func(x, **dfo_pars))
|
|
170
|
+
return (y - fit_func(x, **dfo_pars)) * weights
|
|
167
171
|
|
|
168
172
|
return _residuals
|
|
169
173
|
|
|
@@ -267,7 +271,11 @@ class DFO(MinimizerBase):
|
|
|
267
271
|
return results
|
|
268
272
|
|
|
269
273
|
@staticmethod
|
|
270
|
-
def _prepare_kwargs(
|
|
274
|
+
def _prepare_kwargs(
|
|
275
|
+
tolerance: Optional[float] = None,
|
|
276
|
+
max_evaluations: Optional[int] = None,
|
|
277
|
+
**kwargs,
|
|
278
|
+
) -> dict[str:str]:
|
|
271
279
|
if max_evaluations is not None:
|
|
272
280
|
kwargs['maxfun'] = max_evaluations # max number of function evaluations
|
|
273
281
|
if tolerance is not None:
|
{easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_lmfit.py
RENAMED
|
@@ -111,6 +111,11 @@ class LMFit(MinimizerBase): # noqa: S101
|
|
|
111
111
|
:param kwargs: Additional arguments for the fitting function.
|
|
112
112
|
:return: Fit results
|
|
113
113
|
:rtype: ModelResult
|
|
114
|
+
|
|
115
|
+
For standard least squares, the weights should be 1/sigma, where
|
|
116
|
+
sigma is the standard deviation of the measurement. For
|
|
117
|
+
unweighted least squares, these should be 1.
|
|
118
|
+
|
|
114
119
|
"""
|
|
115
120
|
x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights)
|
|
116
121
|
|
|
@@ -230,7 +235,12 @@ class LMFit(MinimizerBase): # noqa: S101
|
|
|
230
235
|
else:
|
|
231
236
|
value = item.value
|
|
232
237
|
|
|
233
|
-
model.set_param_hint(
|
|
238
|
+
model.set_param_hint(
|
|
239
|
+
MINIMIZER_PARAMETER_PREFIX + str(name),
|
|
240
|
+
value=value,
|
|
241
|
+
min=item.min,
|
|
242
|
+
max=item.max,
|
|
243
|
+
)
|
|
234
244
|
|
|
235
245
|
# Cache the model for later reference
|
|
236
246
|
self._cached_model = model
|
|
@@ -74,9 +74,32 @@ class Map:
|
|
|
74
74
|
# A dict with object names as keys and a list of their object types as values, with weak references
|
|
75
75
|
self.__type_dict = {}
|
|
76
76
|
|
|
77
|
+
def _snapshot_items(self):
|
|
78
|
+
"""Return a stable snapshot of __type_dict items.
|
|
79
|
+
|
|
80
|
+
Some callers iterate over __type_dict while other threads or
|
|
81
|
+
weakref finalizers may modify it. Creating a list snapshot (with
|
|
82
|
+
a retry loop) prevents RuntimeError: dictionary changed size during iteration.
|
|
83
|
+
"""
|
|
84
|
+
while True:
|
|
85
|
+
try:
|
|
86
|
+
return list(self.__type_dict.items())
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
# Dict changed during snapshot creation, retry
|
|
89
|
+
continue
|
|
90
|
+
|
|
77
91
|
def vertices(self) -> List[str]:
|
|
78
|
-
"""
|
|
79
|
-
|
|
92
|
+
"""Returns the vertices of a map.
|
|
93
|
+
|
|
94
|
+
Uses a retry loop to handle RuntimeError that can occur when the
|
|
95
|
+
WeakValueDictionary is modified during iteration (e.g., by garbage collection).
|
|
96
|
+
"""
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
return list(self._store)
|
|
100
|
+
except RuntimeError:
|
|
101
|
+
# Dictionary changed size during iteration, retry
|
|
102
|
+
continue
|
|
80
103
|
|
|
81
104
|
def edges(self):
|
|
82
105
|
"""returns the edges of a map"""
|
|
@@ -100,46 +123,64 @@ class Map:
|
|
|
100
123
|
|
|
101
124
|
def _nested_get(self, obj_type: str) -> List[str]:
|
|
102
125
|
"""Access a nested object in root by key sequence."""
|
|
103
|
-
|
|
126
|
+
# Create a stable snapshot of the dict items to avoid RuntimeError
|
|
127
|
+
# when the dict is modified during iteration (e.g., by finalizers).
|
|
128
|
+
while True:
|
|
129
|
+
try:
|
|
130
|
+
items = self._snapshot_items()
|
|
131
|
+
return [key for key, item in items if obj_type in item.type]
|
|
132
|
+
except RuntimeError:
|
|
133
|
+
# In case the snapshot itself raises (very rare), retry
|
|
134
|
+
continue
|
|
104
135
|
|
|
105
136
|
def get_item_by_key(self, item_id: str) -> object:
|
|
106
|
-
if item_id in self._store
|
|
137
|
+
if item_id in self._store:
|
|
107
138
|
return self._store[item_id]
|
|
108
139
|
raise ValueError('Item not in map.')
|
|
109
140
|
|
|
110
141
|
def is_known(self, vertex: object) -> bool:
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
"""Check if a vertex is known in the map.
|
|
143
|
+
|
|
144
|
+
All objects should have a 'unique_name' attribute.
|
|
145
|
+
"""
|
|
146
|
+
return vertex.unique_name in self._store
|
|
113
147
|
|
|
114
148
|
def find_type(self, vertex: object) -> List[str]:
|
|
115
149
|
if self.is_known(vertex):
|
|
116
150
|
return self.__type_dict[vertex.unique_name].type
|
|
117
151
|
|
|
118
152
|
def reset_type(self, obj, default_type: str):
|
|
119
|
-
if obj.unique_name in self.__type_dict
|
|
153
|
+
if obj.unique_name in self.__type_dict:
|
|
120
154
|
self.__type_dict[obj.unique_name].reset_type(default_type)
|
|
121
155
|
|
|
122
156
|
def change_type(self, obj, new_type: str):
|
|
123
|
-
if obj.unique_name in self.__type_dict
|
|
157
|
+
if obj.unique_name in self.__type_dict:
|
|
124
158
|
self.__type_dict[obj.unique_name].type = new_type
|
|
125
159
|
|
|
126
160
|
def add_vertex(self, obj: object, obj_type: str = None):
|
|
127
161
|
name = obj.unique_name
|
|
128
|
-
if name in self._store
|
|
162
|
+
if name in self._store:
|
|
129
163
|
raise ValueError(f'Object name {name} already exists in the graph.')
|
|
164
|
+
# Clean up stale entry in __type_dict if the weak reference was collected
|
|
165
|
+
# but the finalizer hasn't run yet
|
|
166
|
+
if name in self.__type_dict:
|
|
167
|
+
del self.__type_dict[name]
|
|
168
|
+
|
|
130
169
|
self._store[name] = obj
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
170
|
+
|
|
171
|
+
entry_list = _EntryList()
|
|
172
|
+
entry_list.finalizer = weakref.finalize(obj, self.prune_type_dict, name)
|
|
173
|
+
entry_list.type = obj_type
|
|
174
|
+
self.__type_dict[name] = entry_list # Add objects type to the list of types
|
|
134
175
|
|
|
135
176
|
def add_edge(self, start_obj: object, end_obj: object):
|
|
136
|
-
if start_obj.unique_name in self.__type_dict
|
|
177
|
+
if start_obj.unique_name in self.__type_dict:
|
|
137
178
|
self.__type_dict[start_obj.unique_name].append(end_obj.unique_name)
|
|
138
179
|
else:
|
|
139
180
|
raise AttributeError('Start object not in map.')
|
|
140
181
|
|
|
141
182
|
def get_edges(self, start_obj) -> List[str]:
|
|
142
|
-
if start_obj.unique_name in self.__type_dict
|
|
183
|
+
if start_obj.unique_name in self.__type_dict:
|
|
143
184
|
return list(self.__type_dict[start_obj.unique_name])
|
|
144
185
|
else:
|
|
145
186
|
raise AttributeError
|
|
@@ -151,8 +192,11 @@ class Map:
|
|
|
151
192
|
vertices
|
|
152
193
|
"""
|
|
153
194
|
edges = []
|
|
154
|
-
|
|
155
|
-
|
|
195
|
+
# Iterate over a snapshot of items and snapshot neighbour lists to
|
|
196
|
+
# avoid concurrent modification issues.
|
|
197
|
+
for vertex, neighbours in self._snapshot_items():
|
|
198
|
+
neighbours_snapshot = list(neighbours)
|
|
199
|
+
for neighbour in neighbours_snapshot:
|
|
156
200
|
if {neighbour, vertex} not in edges:
|
|
157
201
|
edges.append({vertex, neighbour})
|
|
158
202
|
return edges
|
|
@@ -163,22 +207,25 @@ class Map:
|
|
|
163
207
|
return
|
|
164
208
|
vertex2 = child_obj.unique_name
|
|
165
209
|
|
|
166
|
-
if vertex1 in self.__type_dict
|
|
210
|
+
if vertex1 in self.__type_dict and vertex2 in self.__type_dict[vertex1]:
|
|
167
211
|
del self.__type_dict[vertex1][self.__type_dict[vertex1].index(vertex2)]
|
|
168
212
|
|
|
213
|
+
def prune_type_dict(self, key: str):
|
|
214
|
+
if key in self.__type_dict:
|
|
215
|
+
del self.__type_dict[key]
|
|
216
|
+
|
|
169
217
|
def prune(self, key: str):
|
|
170
|
-
if key in self.__type_dict
|
|
218
|
+
if key in self.__type_dict:
|
|
171
219
|
del self.__type_dict[key]
|
|
172
|
-
|
|
220
|
+
if key in self._store:
|
|
221
|
+
del self._store[key]
|
|
173
222
|
|
|
174
223
|
def find_isolated_vertices(self) -> list:
|
|
175
224
|
"""returns a list of isolated vertices."""
|
|
176
|
-
graph = self.__type_dict
|
|
177
225
|
isolated = []
|
|
178
|
-
for vertex in
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
isolated += [vertex]
|
|
226
|
+
for vertex, neighbours in self._snapshot_items():
|
|
227
|
+
if not list(neighbours):
|
|
228
|
+
isolated.append(vertex)
|
|
182
229
|
return isolated
|
|
183
230
|
|
|
184
231
|
def find_path(self, start_vertex: str, end_vertex: str, path=[]) -> list:
|
|
@@ -230,9 +277,10 @@ class Map:
|
|
|
230
277
|
path_length = sys.maxsize
|
|
231
278
|
optimum_path = []
|
|
232
279
|
if start_vertex is None:
|
|
233
|
-
# We now have to find where to begin.....
|
|
234
|
-
for possible_start, vertices in self.
|
|
235
|
-
|
|
280
|
+
# We now have to find where to begin..... Iterate over a snapshot
|
|
281
|
+
for possible_start, vertices in self._snapshot_items():
|
|
282
|
+
vertices_snapshot = list(vertices)
|
|
283
|
+
if end_vertex in vertices_snapshot:
|
|
236
284
|
temp_path = self.find_path(possible_start, end_vertex)
|
|
237
285
|
if len(temp_path) < path_length:
|
|
238
286
|
path_length = len(temp_path)
|
|
@@ -247,13 +295,13 @@ class Map:
|
|
|
247
295
|
if vertices_encountered is None:
|
|
248
296
|
vertices_encountered = set()
|
|
249
297
|
graph = self.__type_dict
|
|
250
|
-
vertices = list(graph
|
|
298
|
+
vertices = list(graph)
|
|
251
299
|
if not start_vertex:
|
|
252
300
|
# chose a vertex from graph as a starting point
|
|
253
301
|
start_vertex = vertices[0]
|
|
254
302
|
vertices_encountered.add(start_vertex)
|
|
255
303
|
if len(vertices_encountered) != len(vertices):
|
|
256
|
-
for vertex in graph[start_vertex]:
|
|
304
|
+
for vertex in list(graph[start_vertex]):
|
|
257
305
|
if vertex not in vertices_encountered and self.is_connected(vertices_encountered, vertex):
|
|
258
306
|
return True
|
|
259
307
|
else:
|
|
@@ -262,10 +310,9 @@ class Map:
|
|
|
262
310
|
|
|
263
311
|
def _clear(self):
|
|
264
312
|
"""Reset the map to an empty state. Only to be used for testing"""
|
|
265
|
-
|
|
266
|
-
|
|
313
|
+
self._store.clear()
|
|
314
|
+
self.__type_dict.clear()
|
|
267
315
|
gc.collect()
|
|
268
|
-
self.__type_dict = {}
|
|
269
316
|
|
|
270
317
|
def __repr__(self) -> str:
|
|
271
318
|
return f'Map object of {len(self._store)} vertices.'
|
|
@@ -423,16 +423,13 @@ class DescriptorNumber(DescriptorBase):
|
|
|
423
423
|
|
|
424
424
|
def __truediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber:
|
|
425
425
|
if isinstance(other, numbers.Number):
|
|
426
|
-
original_other = other
|
|
427
426
|
if other == 0:
|
|
428
427
|
raise ZeroDivisionError('Cannot divide by zero')
|
|
429
428
|
new_value = self.full_value / other
|
|
430
429
|
elif type(other) is DescriptorNumber:
|
|
431
|
-
|
|
432
|
-
if original_other == 0:
|
|
430
|
+
if other.value == 0:
|
|
433
431
|
raise ZeroDivisionError('Cannot divide by zero')
|
|
434
432
|
new_value = self.full_value / other.full_value
|
|
435
|
-
other.value = original_other
|
|
436
433
|
else:
|
|
437
434
|
return NotImplemented
|
|
438
435
|
descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value)
|
|
@@ -121,7 +121,12 @@ class Parameter(DescriptorNumber):
|
|
|
121
121
|
|
|
122
122
|
@classmethod
|
|
123
123
|
def from_dependency(
|
|
124
|
-
cls,
|
|
124
|
+
cls,
|
|
125
|
+
name: str,
|
|
126
|
+
dependency_expression: str,
|
|
127
|
+
dependency_map: Optional[dict] = None,
|
|
128
|
+
desired_unit: str | sc.Unit | None = None,
|
|
129
|
+
**kwargs,
|
|
125
130
|
) -> Parameter: # noqa: E501
|
|
126
131
|
"""
|
|
127
132
|
Create a dependent Parameter directly from a dependency expression.
|
|
@@ -129,15 +134,20 @@ class Parameter(DescriptorNumber):
|
|
|
129
134
|
:param name: The name of the parameter
|
|
130
135
|
:param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter.
|
|
131
136
|
:param dependency_map: A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies.
|
|
137
|
+
:param desired_unit: The desired unit of the dependent parameter.
|
|
132
138
|
:param kwargs: Additional keyword arguments to pass to the Parameter constructor.
|
|
133
139
|
:return: A new dependent Parameter object.
|
|
134
140
|
""" # noqa: E501
|
|
135
141
|
# Set default values for required parameters for the constructor, they get overwritten by the dependency anyways
|
|
136
|
-
default_kwargs = {'value': 0.0, '
|
|
142
|
+
default_kwargs = {'value': 0.0, 'variance': 0.0, 'min': -np.inf, 'max': np.inf}
|
|
137
143
|
# Update with user-provided kwargs, to avoid errors.
|
|
138
144
|
default_kwargs.update(kwargs)
|
|
139
145
|
parameter = cls(name=name, **default_kwargs)
|
|
140
|
-
parameter.make_dependent_on(
|
|
146
|
+
parameter.make_dependent_on(
|
|
147
|
+
dependency_expression=dependency_expression,
|
|
148
|
+
dependency_map=dependency_map,
|
|
149
|
+
desired_unit=desired_unit,
|
|
150
|
+
)
|
|
141
151
|
return parameter
|
|
142
152
|
|
|
143
153
|
def _update(self) -> None:
|
|
@@ -158,11 +168,20 @@ class Parameter(DescriptorNumber):
|
|
|
158
168
|
) # noqa: E501
|
|
159
169
|
self._min.unit = temporary_parameter.unit
|
|
160
170
|
self._max.unit = temporary_parameter.unit
|
|
171
|
+
|
|
172
|
+
if self._desired_unit is not None:
|
|
173
|
+
self._convert_unit(self._desired_unit)
|
|
174
|
+
|
|
161
175
|
self._notify_observers()
|
|
162
176
|
else:
|
|
163
177
|
warnings.warn('This parameter is not dependent. It cannot be updated.')
|
|
164
178
|
|
|
165
|
-
def make_dependent_on(
|
|
179
|
+
def make_dependent_on(
|
|
180
|
+
self,
|
|
181
|
+
dependency_expression: str,
|
|
182
|
+
dependency_map: Optional[dict] = None,
|
|
183
|
+
desired_unit: str | sc.Unit | None = None,
|
|
184
|
+
) -> None:
|
|
166
185
|
"""
|
|
167
186
|
Make this parameter dependent on another parameter. This will overwrite the current value, unit, variance, min and max.
|
|
168
187
|
|
|
@@ -183,6 +202,9 @@ class Parameter(DescriptorNumber):
|
|
|
183
202
|
A dictionary of dependency expression symbol name and dependency object pairs.
|
|
184
203
|
This is inserted into the asteval interpreter to resolve dependencies.
|
|
185
204
|
|
|
205
|
+
:param desired_unit:
|
|
206
|
+
The desired unit of the dependent parameter. If None, the default unit of the dependency expression result is used.
|
|
207
|
+
|
|
186
208
|
""" # noqa: E501
|
|
187
209
|
if not isinstance(dependency_expression, str):
|
|
188
210
|
raise TypeError('`dependency_expression` must be a string representing a valid dependency expression.')
|
|
@@ -212,6 +234,7 @@ class Parameter(DescriptorNumber):
|
|
|
212
234
|
'_dependency_map': self._dependency_map,
|
|
213
235
|
'_dependency_interpreter': self._dependency_interpreter,
|
|
214
236
|
'_clean_dependency_string': self._clean_dependency_string,
|
|
237
|
+
'_desired_unit': self._desired_unit,
|
|
215
238
|
}
|
|
216
239
|
for dependency in self._dependency_map.values():
|
|
217
240
|
dependency._detach_observer(self)
|
|
@@ -219,6 +242,9 @@ class Parameter(DescriptorNumber):
|
|
|
219
242
|
self._independent = False
|
|
220
243
|
self._dependency_string = dependency_expression
|
|
221
244
|
self._dependency_map = dependency_map if dependency_map is not None else {}
|
|
245
|
+
if desired_unit is not None and not (isinstance(desired_unit, str) or isinstance(desired_unit, sc.Unit)):
|
|
246
|
+
raise TypeError('`desired_unit` must be a string representing a valid unit.')
|
|
247
|
+
self._desired_unit = desired_unit
|
|
222
248
|
# List of allowed python constructs for the asteval interpreter
|
|
223
249
|
asteval_config = {
|
|
224
250
|
'import': False,
|
|
@@ -289,6 +315,17 @@ class Parameter(DescriptorNumber):
|
|
|
289
315
|
raise error
|
|
290
316
|
# Update the parameter with the dependency result
|
|
291
317
|
self._fixed = False
|
|
318
|
+
|
|
319
|
+
if self._desired_unit is not None:
|
|
320
|
+
try:
|
|
321
|
+
dependency_result._convert_unit(self._desired_unit)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
desired_unit_for_error_message = self._desired_unit
|
|
324
|
+
self._revert_dependency() # also deletes self._desired_unit
|
|
325
|
+
raise UnitError(
|
|
326
|
+
f'Failed to convert unit from {dependency_result.unit} to {desired_unit_for_error_message}: {e}'
|
|
327
|
+
)
|
|
328
|
+
|
|
292
329
|
self._update()
|
|
293
330
|
|
|
294
331
|
def make_independent(self) -> None:
|
|
@@ -306,6 +343,7 @@ class Parameter(DescriptorNumber):
|
|
|
306
343
|
del self._dependency_interpreter
|
|
307
344
|
del self._dependency_string
|
|
308
345
|
del self._clean_dependency_string
|
|
346
|
+
del self._desired_unit
|
|
309
347
|
else:
|
|
310
348
|
raise AttributeError('This parameter is already independent.')
|
|
311
349
|
|
|
@@ -470,6 +508,28 @@ class Parameter(DescriptorNumber):
|
|
|
470
508
|
"""
|
|
471
509
|
self._convert_unit(unit_str)
|
|
472
510
|
|
|
511
|
+
def set_desired_unit(self, unit_str: str | sc.Unit | None) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Set the desired unit for a dependent Parameter. This will convert the parameter to the desired unit.
|
|
514
|
+
|
|
515
|
+
:param unit_str: The desired unit as a string.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
if self._independent:
|
|
519
|
+
raise AttributeError('This is an independent parameter, desired unit can only be set for dependent parameters.')
|
|
520
|
+
if not (isinstance(unit_str, str) or isinstance(unit_str, sc.Unit) or unit_str is None):
|
|
521
|
+
raise TypeError('`unit_str` must be a string representing a valid unit.')
|
|
522
|
+
|
|
523
|
+
if unit_str is not None:
|
|
524
|
+
try:
|
|
525
|
+
old_unit_for_message = self.unit
|
|
526
|
+
self._convert_unit(unit_str)
|
|
527
|
+
except Exception as e:
|
|
528
|
+
raise UnitError(f'Failed to convert unit from {old_unit_for_message} to {unit_str}: {e}')
|
|
529
|
+
|
|
530
|
+
self._desired_unit = unit_str
|
|
531
|
+
self._update()
|
|
532
|
+
|
|
473
533
|
@property
|
|
474
534
|
def min(self) -> numbers.Number:
|
|
475
535
|
"""
|
|
@@ -580,6 +640,9 @@ class Parameter(DescriptorNumber):
|
|
|
580
640
|
# Save the dependency expression
|
|
581
641
|
raw_dict['_dependency_string'] = self._clean_dependency_string
|
|
582
642
|
|
|
643
|
+
if self._desired_unit is not None:
|
|
644
|
+
raw_dict['_desired_unit'] = self._desired_unit
|
|
645
|
+
|
|
583
646
|
# Mark that this parameter is dependent
|
|
584
647
|
raw_dict['_independent'] = self._independent
|
|
585
648
|
|
|
@@ -648,6 +711,7 @@ class Parameter(DescriptorNumber):
|
|
|
648
711
|
dependency_string = raw_dict.pop('_dependency_string', None)
|
|
649
712
|
dependency_map_serializer_ids = raw_dict.pop('_dependency_map_serializer_ids', None)
|
|
650
713
|
is_independent = raw_dict.pop('_independent', True)
|
|
714
|
+
desired_unit = raw_dict.pop('_desired_unit', None)
|
|
651
715
|
# Note: Keep _serializer_id in the dict so it gets passed to __init__
|
|
652
716
|
|
|
653
717
|
# Create the parameter using the base class method (serializer_id is now handled in __init__)
|
|
@@ -659,6 +723,7 @@ class Parameter(DescriptorNumber):
|
|
|
659
723
|
param._pending_dependency_map_serializer_ids = dependency_map_serializer_ids
|
|
660
724
|
# Keep parameter as independent initially - will be made dependent after all objects are loaded
|
|
661
725
|
param._independent = True
|
|
726
|
+
param._pending_desired_unit = desired_unit
|
|
662
727
|
|
|
663
728
|
return param
|
|
664
729
|
|
|
@@ -874,10 +939,14 @@ class Parameter(DescriptorNumber):
|
|
|
874
939
|
elif self.max <= 0:
|
|
875
940
|
combinations = [self.max / other.min, np.inf]
|
|
876
941
|
else:
|
|
877
|
-
combinations = [
|
|
942
|
+
combinations = [
|
|
943
|
+
self.min / other.min,
|
|
944
|
+
self.max / other.max,
|
|
945
|
+
self.min / other.max,
|
|
946
|
+
self.max / other.min,
|
|
947
|
+
]
|
|
878
948
|
else:
|
|
879
949
|
combinations = [self.min / other.value, self.max / other.value]
|
|
880
|
-
other.value = other_value
|
|
881
950
|
else:
|
|
882
951
|
return NotImplemented
|
|
883
952
|
min_value = min(combinations)
|
|
@@ -926,7 +995,6 @@ class Parameter(DescriptorNumber):
|
|
|
926
995
|
parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value)
|
|
927
996
|
parameter._convert_unit(parameter._base_unit())
|
|
928
997
|
parameter.name = parameter.unique_name
|
|
929
|
-
self.value = original_self
|
|
930
998
|
return parameter
|
|
931
999
|
|
|
932
1000
|
def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter:
|
|
@@ -1019,13 +1087,18 @@ class Parameter(DescriptorNumber):
|
|
|
1019
1087
|
|
|
1020
1088
|
# Establish the dependency relationship
|
|
1021
1089
|
try:
|
|
1022
|
-
self.make_dependent_on(
|
|
1090
|
+
self.make_dependent_on(
|
|
1091
|
+
dependency_expression=dependency_string,
|
|
1092
|
+
dependency_map=dependency_map,
|
|
1093
|
+
desired_unit=self._pending_desired_unit,
|
|
1094
|
+
)
|
|
1023
1095
|
except Exception as e:
|
|
1024
1096
|
raise ValueError(f"Error establishing dependency '{dependency_string}': {e}")
|
|
1025
1097
|
|
|
1026
1098
|
# Clean up temporary attributes
|
|
1027
1099
|
delattr(self, '_pending_dependency_string')
|
|
1028
1100
|
delattr(self, '_pending_dependency_map_serializer_ids')
|
|
1101
|
+
delattr(self, '_pending_desired_unit')
|
|
1029
1102
|
|
|
1030
1103
|
def _find_parameter_by_serializer_id(self, serializer_id: str) -> Optional['DescriptorNumber']:
|
|
1031
1104
|
"""Find a parameter by its serializer_id from all parameters in the global map."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.1.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/interface_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{easyscience-2.1.0 → easyscience-2.2.0}/src/easyscience/variable/parameter_dependency_resolver.py
RENAMED
|
File without changes
|