From 5d62a99630ae86ead2e9b586e6b20e8c6bdf2344 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 5 Mar 2025 13:25:22 +0100 Subject: [PATCH 01/58] first changes --- src/easyscience/Objects/ObjectClasses.py | 2 +- .../Objects/new_variable/parameter.py | 28 +++---- src/easyscience/Objects/virtual.py | 6 +- src/easyscience/fitting/Constraints.py | 18 ++-- tests/unit_tests/Fitting/test_constraints.py | 16 ++-- .../Objects/new_variable/test_parameter.py | 36 ++++---- tests/unit_tests/Objects/test_BaseObj.py | 83 ++++++++++--------- .../global_object/test_undo_redo.py | 40 ++++----- 8 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/Objects/ObjectClasses.py index 037f0a49..8318d2a8 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/Objects/ObjectClasses.py @@ -202,7 +202,7 @@ def get_fit_parameters(self) -> Union[List[Parameter], List[NewParameter]]: if hasattr(item, 'get_fit_parameters'): fit_list = [*fit_list, *item.get_fit_parameters()] elif isinstance(item, Parameter) or isinstance(item, NewParameter): - if item.enabled and not item.fixed: + if item.independent and not item.fixed: fit_list.append(item) return fit_list diff --git a/src/easyscience/Objects/new_variable/parameter.py b/src/easyscience/Objects/new_variable/parameter.py index 66626cdc..10ebc8e5 100644 --- a/src/easyscience/Objects/new_variable/parameter.py +++ b/src/easyscience/Objects/new_variable/parameter.py @@ -54,7 +54,7 @@ def __init__( url: Optional[str] = None, display_name: Optional[str] = None, callback: property = property(), - enabled: Optional[bool] = True, + independent: Optional[bool] = True, parent: Optional[Any] = None, ): """ @@ -72,7 +72,7 @@ def __init__( :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed - :param enabled: Can the objects value be set + :param independent: Can the objects value be set :param parent: The object which is the parent to this one .. note:: @@ -113,7 +113,7 @@ def __init__( # Create additional fitting elements self._fixed = fixed - self._enabled = enabled + self._independent = independent self._initial_scalar = copy.deepcopy(self._scalar) builtin_constraint = { # Last argument in constructor is the name of the property holding the value of the constraint @@ -171,9 +171,9 @@ def value(self, value: numbers.Number) -> None: :param value: New value of self """ - if not self.enabled: + if not self.independent: if global_object.debug: - raise CoreSetException(f'{str(self)} is not enabled.') + raise CoreSetException(f'{str(self)} is not independent.') return if not isinstance(value, numbers.Number) or isinstance(value, bool): @@ -289,12 +289,12 @@ def fixed(self, fixed: bool) -> None: :param fixed: True = fixed, False = can vary """ - if not self.enabled: + if not self.independent: if self._global_object.stack.enabled: # Remove the recorded change from the stack self._global_object.stack.pop() if global_object.debug: - raise CoreSetException(f'{str(self)} is not enabled.') + raise CoreSetException(f'{str(self)} is not independent.') return if not isinstance(fixed, bool): raise ValueError(f'{fixed=} must be a boolean. Got {type(fixed)}') @@ -329,8 +329,8 @@ def bounds(self, new_bound: Tuple[numbers.Number, numbers.Number]) -> None: raise ValueError(f'Current paramter value: {self._scalar.value} must be within {new_bound=}') # Enable the parameter if needed - if not self.enabled: - self.enabled = True + if not self.independent: + self.independent = True # Free parameter if needed if self.fixed: self.fixed = False @@ -376,23 +376,23 @@ def _constraint_runner( return value @property - def enabled(self) -> bool: + def independent(self) -> bool: """ Logical property to see if the objects value can be directly set. :return: Can the objects value be set """ - return self._enabled + return self._independent - @enabled.setter + @independent.setter @property_stack_deco - def enabled(self, value: bool) -> None: + def independent(self, value: bool) -> None: """ Enable and disable the direct setting of an objects value field. :param value: True - objects value can be set, False - the opposite """ - self._enabled = value + self._independent = value def __copy__(self) -> Parameter: new_obj = super().__copy__() diff --git a/src/easyscience/Objects/virtual.py b/src/easyscience/Objects/virtual.py index 12562f60..3fcf8e64 100644 --- a/src/easyscience/Objects/virtual.py +++ b/src/easyscience/Objects/virtual.py @@ -79,8 +79,8 @@ def component_realizer(obj: BV, component: str, recursive: bool = True): if not isinstance(obj, Iterable) or not issubclass(obj.__class__, MutableSequence): old_component = obj._kwargs[component] new_components = realizer(obj._kwargs[component]) - if hasattr(new_components, 'enabled'): - new_components.enabled = True + if hasattr(new_components, 'independent'): + new_components.independent = True else: old_component = obj[component] new_components = realizer(obj[component]) @@ -169,7 +169,7 @@ def virtualizer(obj: BV) -> BV: d['fixed'] = True d['unique_name'] = None v_p = cls(**d) - v_p._enabled = False + v_p.independent = False constraint = ObjConstraint(v_p, '', obj) constraint.external = True obj._constraints['virtual'][v_p.unique_name] = constraint diff --git a/src/easyscience/fitting/Constraints.py b/src/easyscience/fitting/Constraints.py index b123d124..f9980639 100644 --- a/src/easyscience/fitting/Constraints.py +++ b/src/easyscience/fitting/Constraints.py @@ -60,11 +60,11 @@ def __init__( # Test if dependent is a parameter or a descriptor. # We can not import `Parameter`, so...... if dependent_obj.__class__.__name__ == 'Parameter': - if not dependent_obj.enabled: + if not dependent_obj.independent: raise AssertionError('A dependent object needs to be initially enabled.') if global_object.debug: print(f'Dependent variable {dependent_obj}. It should be a `Descriptor`.' f'Setting to fixed') - dependent_obj.enabled = False + dependent_obj.independent = False self._finalizer = weakref.finalize(self, cleanup_constraint, self.dependent_obj_ids, True) self.operator = operator @@ -94,10 +94,10 @@ def enabled(self, enabled_value: bool): if self._enabled == enabled_value: return elif enabled_value: - self.get_obj(self.dependent_obj_ids).enabled = False + self.get_obj(self.dependent_obj_ids).independent = False self() else: - self.get_obj(self.dependent_obj_ids).enabled = True + self.get_obj(self.dependent_obj_ids).independent = True self._enabled = enabled_value def __call__(self, *args, no_set: bool = False, **kwargs): @@ -126,12 +126,12 @@ def __call__(self, *args, no_set: bool = False, **kwargs): if not no_set: toggle = False - if not dependent_obj.enabled: - dependent_obj.enabled = True + if not dependent_obj.independent: + dependent_obj.independent = True toggle = True dependent_obj.value = value if toggle: - dependent_obj.enabled = False + dependent_obj.independent = False return value @abstractmethod @@ -514,10 +514,10 @@ def __repr__(self) -> str: return f'{self.__class__.__name__}' -def cleanup_constraint(obj_id: str, enabled: bool): +def cleanup_constraint(obj_id: str, independent: bool): try: obj = global_object.map.get_item_by_key(obj_id) - obj.enabled = enabled + obj.independent = independent except ValueError: if global_object.debug: print(f'Object with ID {obj_id} has already been deleted') diff --git a/tests/unit_tests/Fitting/test_constraints.py b/tests/unit_tests/Fitting/test_constraints.py index b20247e5..17d35f25 100644 --- a/tests/unit_tests/Fitting/test_constraints.py +++ b/tests/unit_tests/Fitting/test_constraints.py @@ -113,22 +113,22 @@ def test_ObjConstraint_Multiple(threePars): def test_ConstraintEnable_Disable(twoPars): - assert twoPars[0][0].enabled - assert twoPars[0][1].enabled + assert twoPars[0][0].independent + assert twoPars[0][1].independent c = ObjConstraint(twoPars[0][0], "", twoPars[0][1]) twoPars[0][0].user_constraints["num_1"] = c assert c.enabled - assert twoPars[0][1].enabled - assert not twoPars[0][0].enabled + assert twoPars[0][1].independent + assert not twoPars[0][0].independent c.enabled = False assert not c.enabled - assert twoPars[0][1].enabled - assert twoPars[0][0].enabled + assert twoPars[0][1].independent + assert twoPars[0][0].independent c.enabled = True assert c.enabled - assert twoPars[0][1].enabled - assert not twoPars[0][0].enabled + assert twoPars[0][1].independent + assert not twoPars[0][0].independent diff --git a/tests/unit_tests/Objects/new_variable/test_parameter.py b/tests/unit_tests/Objects/new_variable/test_parameter.py index 5269d81b..72fcc7d5 100644 --- a/tests/unit_tests/Objects/new_variable/test_parameter.py +++ b/tests/unit_tests/Objects/new_variable/test_parameter.py @@ -24,7 +24,7 @@ def parameter(self) -> Parameter: url="url", display_name="display_name", callback=self.mock_callback, - enabled="enabled", + independent="independent", parent=None, ) return parameter @@ -40,7 +40,7 @@ def test_init(self, parameter: Parameter): assert parameter._max.value == 10 assert parameter._max.unit == "m" assert parameter._callback == self.mock_callback - assert parameter._enabled == "enabled" + assert parameter._independent == "independent" # From super assert parameter._scalar.value == 1 @@ -69,7 +69,7 @@ def test_init_value_min_exception(self): url="url", display_name="display_name", callback=mock_callback, - enabled="enabled", + independent="independent", parent=None, ) @@ -91,7 +91,7 @@ def test_init_value_max_exception(self): url="url", display_name="display_name", callback=mock_callback, - enabled="enabled", + independent="independent", parent=None, ) @@ -189,7 +189,7 @@ def test_bounds(self, parameter: Parameter): def test_set_bounds(self, parameter: Parameter): # When - parameter._enabled = False + parameter._independent = False parameter._fixed = True # Then @@ -198,12 +198,12 @@ def test_set_bounds(self, parameter: Parameter): # Expect assert parameter.min == -10 assert parameter.max == 5 - assert parameter._enabled == True + assert parameter._independent == True assert parameter._fixed == False def test_set_bounds_exception_min(self, parameter: Parameter): # When - parameter._enabled = False + parameter._independent = False parameter._fixed = True # Then @@ -213,12 +213,12 @@ def test_set_bounds_exception_min(self, parameter: Parameter): # Expect assert parameter.min == 0 assert parameter.max == 10 - assert parameter._enabled == False + assert parameter._independent == False assert parameter._fixed == True def test_set_bounds_exception_max(self, parameter: Parameter): # When - parameter._enabled = False + parameter._independent = False parameter._fixed = True # Then @@ -228,22 +228,22 @@ def test_set_bounds_exception_max(self, parameter: Parameter): # Expect assert parameter.min == 0 assert parameter.max == 10 - assert parameter._enabled == False + assert parameter._independent == False assert parameter._fixed == True - def test_enabled(self, parameter: Parameter): + def test_independent(self, parameter: Parameter): # When - parameter._enabled = True + parameter._independent = True # Then Expect - assert parameter.enabled is True + assert parameter.independent is True - def test_set_enabled(self, parameter: Parameter): + def test_set_independent(self, parameter: Parameter): # When - parameter.enabled = False + parameter.independent = False # Then Expect - assert parameter._enabled is False + assert parameter._independent is False def test_value_match_callback(self, parameter: Parameter): # When @@ -313,7 +313,7 @@ def test_copy(self, parameter: Parameter): assert parameter_copy._description == parameter._description assert parameter_copy._url == parameter._url assert parameter_copy._display_name == parameter._display_name - assert parameter_copy._enabled == parameter._enabled + assert parameter_copy._independent == parameter._independent def test_as_data_dict(self, clear, parameter: Parameter): # When Then @@ -331,7 +331,7 @@ def test_as_data_dict(self, clear, parameter: Parameter): "description": "description", "url": "url", "display_name": "display_name", - "enabled": "enabled", + "independent": "independent", "unique_name": "Parameter_0", } diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/Objects/test_BaseObj.py index 5fdb087e..742c3b59 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/Objects/test_BaseObj.py @@ -17,8 +17,8 @@ import easyscience from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Descriptor -from easyscience.Objects.ObjectClasses import Parameter +from easyscience.Objects.new_variable import DescriptorNumber +from easyscience.Objects.new_variable import Parameter from easyscience.Utils.io.dict import DictSerializer from easyscience import global_object @@ -31,9 +31,9 @@ def setup_pars(): d = { "name": "test", "par1": Parameter("p1", 0.1, fixed=True), - "des1": Descriptor("d1", 0.1), + "des1": DescriptorNumber("d1", 0.1), "par2": Parameter("p2", 0.1), - "des2": Descriptor("d2", 0.1), + "des2": DescriptorNumber("d2", 0.1), "par3": Parameter("p3", 0.1), } return d @@ -103,8 +103,8 @@ def test_baseobj_set(setup_pars: dict): obj = BaseObj(name, **kwargs) new_value = 5.0 with not_raises([AttributeError, ValueError]): - obj.p1 = new_value - assert obj.p1.raw_value == new_value + obj.p1.value = new_value + assert obj.p1.value == new_value def test_baseobj_get_parameters(setup_pars: dict): @@ -134,25 +134,29 @@ def test_baseobj_as_dict(setup_pars: dict): "@class": "BaseObj", "@version": easyscience.__version__, "name": "test", + "unique_name": "BaseObj_0", "par1": { "@module": Parameter.__module__, "@class": Parameter.__name__, "@version": easyscience.__version__, "name": "p1", "value": 0.1, - "error": 0.0, + "unit": "dimensionless", + "variance": 0.0, "min": -np.inf, "max": np.inf, "fixed": True, - "units": "dimensionless", + "unique_name": "Parameter_0" }, "des1": { - "@module": Descriptor.__module__, - "@class": Descriptor.__name__, + "@module": DescriptorNumber.__module__, + "@class": DescriptorNumber.__name__, "@version": easyscience.__version__, "name": "d1", "value": 0.1, - "units": "dimensionless", + "unit": "dimensionless", + "variable": "d1", + "unique_name": "DescriptorNumber_0", "description": "", "url": "", "display_name": "d1", @@ -163,19 +167,22 @@ def test_baseobj_as_dict(setup_pars: dict): "@version": easyscience.__version__, "name": "p2", "value": 0.1, - "error": 0.0, + "unit": "dimensionless", + "variance": 0.0, "min": -np.inf, "max": np.inf, "fixed": False, - "units": "dimensionless", + "unique_name": "Parameter_1" }, "des2": { - "@module": Descriptor.__module__, - "@class": Descriptor.__name__, + "@module": DescriptorNumber.__module__, + "@class": DescriptorNumber.__name__, "@version": easyscience.__version__, "name": "d2", "value": 0.1, - "units": "dimensionless", + "unit": "dimensionless", + "variance": None, + "unique_name": "DescriptorNumber_1", "description": "", "url": "", "display_name": "d2", @@ -186,11 +193,11 @@ def test_baseobj_as_dict(setup_pars: dict): "@version": easyscience.__version__, "name": "p3", "value": 0.1, - "error": 0.0, + "unit": "dimensionless", + "variance": 0.0, "min": -np.inf, "max": np.inf, "fixed": False, - "units": "dimensionless", }, } @@ -333,23 +340,23 @@ def from_pars(cls, m, c, diff): return cls(m, c, diff) def __call__(self, *args, **kwargs): - return super(L2, self).__call__(*args, **kwargs) + self.diff.raw_value + return super(L2, self).__call__(*args, **kwargs) + self.diff.value l2 = L2.from_pars(1, 2, 3) - assert l2.m.raw_value == 1 - assert l2.c.raw_value == 2 - assert l2.diff.raw_value == 3 + assert l2.m.value == 1 + assert l2.c.value == 2 + assert l2.diff.value == 3 l2.diff = 4 assert isinstance(l2.diff, Parameter) - assert l2.diff.raw_value == 4 + assert l2.diff.value == 4 l2.foo = "foo" assert l2.foo == "foo" x = np.linspace(0, 10, 100) - y = l2.m.raw_value * x + l2.c.raw_value + l2.diff.raw_value + y = l2.m.value * x + l2.c.value + l2.diff.value assert np.allclose(l2(x), y) @@ -368,11 +375,11 @@ def from_pars(cls, a: float): a = A.from_pars(a_start) graph = a._global_object.map - assert a.a.raw_value == a_start + assert a.a.value == a_start assert len(graph.get_edges(a)) == 1 setattr(a, "a", a_end) - assert a.a.raw_value == a_end + assert a.a.value == a_end assert len(graph.get_edges(a)) == 1 @@ -409,11 +416,11 @@ def from_pars(cls, a: float): a = A.from_pars(a_start) graph = a._global_object.map - assert a.a.raw_value == a_start + assert a.a.value == a_start assert len(graph.get_edges(a)) == 1 setattr(a, "a", a_end) - assert a.a.raw_value == a_end + assert a.a.value == a_end assert len(graph.get_edges(a)) == 1 @@ -434,14 +441,14 @@ def from_pars(cls, a: float): a = A.from_pars(a_start) graph = a._global_object.map - assert a.a.raw_value == a_start + assert a.a.value == a_start assert len(graph.get_edges(a)) == 1 a_ = Parameter("a", a_end) assert a.a.unique_name in graph.get_edges(a) a__ = a.a setattr(a, "a", a_) - assert a.a.raw_value == a_end + assert a.a.value == a_end assert len(graph.get_edges(a)) == 1 assert a_.unique_name in graph.get_edges(a) assert a__.unique_name not in graph.get_edges(a) @@ -455,13 +462,13 @@ def __init__(self, a: Optional[Union[Parameter, float]] = None): self.a = a a = A() - assert a.a.raw_value == 1.0 + assert a.a.value == 1.0 a = A(2.0) - assert a.a.raw_value == 2.0 + assert a.a.value == 2.0 a = A(Parameter("a", 3.0)) - assert a.a.raw_value == 3.0 + assert a.a.value == 3.0 a.a = 4.0 - assert a.a.raw_value == 4.0 + assert a.a.value == 4.0 class B(BaseObj): def __init__(self, b: Optional[Union[A, Parameter, float]] = None): @@ -472,13 +479,13 @@ def __init__(self, b: Optional[Union[A, Parameter, float]] = None): self.b = b b = B() - assert b.b.a.raw_value == 1.0 + assert b.b.a.value == 1.0 b = B(2.0) - assert b.b.a.raw_value == 2.0 + assert b.b.a.value == 2.0 b = B(A(3.0)) - assert b.b.a.raw_value == 3.0 + assert b.b.a.value == 3.0 b.b.a = 4.0 - assert b.b.a.raw_value == 4.0 + assert b.b.a.value == 4.0 def test_unique_name_generator(clear): # When Then diff --git a/tests/unit_tests/global_object/test_undo_redo.py b/tests/unit_tests/global_object/test_undo_redo.py index fb067d10..d1b02c63 100644 --- a/tests/unit_tests/global_object/test_undo_redo.py +++ b/tests/unit_tests/global_object/test_undo_redo.py @@ -12,8 +12,8 @@ from easyscience.Objects.Groups import BaseCollection from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.Variable import Descriptor -from easyscience.Objects.Variable import Parameter +from easyscience.Objects.new_variable import DescriptorNumber +from easyscience.Objects.new_variable import Parameter from easyscience.fitting import Fitter @@ -24,7 +24,7 @@ def createSingleObjs(idx): if idx % 2: return Parameter(name, idx) else: - return Descriptor(name, idx) + return DescriptorNumber(name, idx) def createParam(option): @@ -67,7 +67,7 @@ def getter(_obj, _attr): for option in [ ("value", 500), ("error", 5), - ("enabled", False), + ("independent", False), ("unit", "meter / second"), ("display_name", "boom"), ("fixed", False), @@ -77,7 +77,7 @@ def getter(_obj, _attr): ], ) @pytest.mark.parametrize( - "idx", [pytest.param(0, id="Descriptor"), pytest.param(1, id="Parameter")] + "idx", [pytest.param(0, id="DescriptorNumber"), pytest.param(1, id="Parameter")] ) def test_SinglesUndoRedo(idx, test): obj = createSingleObjs(idx) @@ -96,7 +96,7 @@ def test_Parameter_Bounds_UndoRedo(value): from easyscience import global_object global_object.stack.enabled = True - p = Parameter("test", 1, enabled=value) + p = Parameter("test", 1, independent=value) assert p.min == -np.inf assert p.max == np.inf assert p.bounds == (-np.inf, np.inf) @@ -105,13 +105,13 @@ def test_Parameter_Bounds_UndoRedo(value): assert p.min == 0 assert p.max == 2 assert p.bounds == (0, 2) - assert p.enabled is True + assert p.independent is True global_object.stack.undo() assert p.min == -np.inf assert p.max == np.inf assert p.bounds == (-np.inf, np.inf) - assert p.enabled is value + assert p.independent is value def test_BaseObjUndoRedo(): @@ -125,7 +125,7 @@ def test_BaseObjUndoRedo(): # Test setting value for b_obj in objs.values(): - e = doUndoRedo(obj, b_obj.name, b_obj.raw_value + 1, "raw_value") + e = doUndoRedo(obj, b_obj.name, b_obj.value + 1, "value") if e: raise e @@ -209,25 +209,25 @@ def test_UndoRedoMacros(): global_object.stack.enabled = True global_object.stack.beginMacro(undo_text) - values = [item.raw_value for item in items] + values = [item.value for item in items] for item, value in zip(items, values): item.value = value + offset global_object.stack.endMacro() for item, old_value in zip(items, values): - assert item.raw_value == old_value + offset + assert item.value == old_value + offset assert global_object.stack.undoText() == undo_text global_object.stack.undo() for item, old_value in zip(items, values): - assert item.raw_value == old_value + assert item.value == old_value assert global_object.stack.redoText() == undo_text global_object.stack.redo() for item, old_value in zip(items, values): - assert item.raw_value == old_value + offset + assert item.value == old_value + offset @pytest.mark.parametrize("fit_engine", ["LMFit", "Bumps", "DFO"]) @@ -254,7 +254,7 @@ def from_pars(cls, m_value: float, c_value: float): return cls(m=m, c=c) def __call__(self, x: np.ndarray) -> np.ndarray: - return self.m.raw_value * x + self.c.raw_value + return self.m.value * x + self.c.value l1 = Line.default() m_sp = 4 @@ -277,18 +277,18 @@ def __call__(self, x: np.ndarray) -> np.ndarray: global_object.stack.enabled = True res = f.fit(x, y) - # assert l1.c.raw_value == pytest.approx(l2.c.raw_value, rel=l2.c.error * 3) - # assert l1.m.raw_value == pytest.approx(l2.m.raw_value, rel=l2.m.error * 3) + # assert l1.c.value == pytest.approx(l2.c.value, rel=l2.c.error * 3) + # assert l1.m.value == pytest.approx(l2.m.value, rel=l2.m.error * 3) assert global_object.stack.undoText() == "Fitting routine" global_object.stack.undo() - assert l2.m.raw_value == m_sp - assert l2.c.raw_value == c_sp + assert l2.m.value == m_sp + assert l2.c.value == c_sp assert global_object.stack.redoText() == "Fitting routine" global_object.stack.redo() - assert l2.m.raw_value == res.p[f"p{l2.m.unique_name}"] - assert l2.c.raw_value == res.p[f"p{l2.c.unique_name}"] + assert l2.m.value == res.p[f"p{l2.m.unique_name}"] + assert l2.c.value == res.p[f"p{l2.c.unique_name}"] # @pytest.mark.parametrize('math_funcs', [pytest.param([Parameter.__iadd__, float.__add__], id='Addition'), From e27d5955d3b0d7a23769884b1284f31e997472f4 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 1 Apr 2025 15:15:43 +0200 Subject: [PATCH 02/58] Add Observer pattern to DescriptorNumber --- .../Objects/variable/descriptor_number.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index cfba4a44..b162a22a 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -74,6 +74,8 @@ def __init__( if self.unit is not None: self.convert_unit(self._base_unit()) + self._observers: List[DescriptorNumber] = [] + @classmethod def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumber: """ @@ -90,6 +92,19 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumb raise TypeError(f'{full_value=} must be a scipp scalar') return cls(name=name, value=full_value.value, unit=full_value.unit, variance=full_value.variance, **kwargs) + def attach_observer(self, observer: DescriptorNumber) -> None: + """Attach an observer to the descriptor.""" + self._observers.append(observer) + + def detach_observer(self, observer: DescriptorNumber) -> None: + """Detach an observer from the descriptor.""" + self._observers.remove(observer) + + def notify_observers(self) -> None: + """Notify all observers of a change.""" + for observer in self._observers: + observer.update(self) + @property def full_value(self) -> Variable: """ @@ -125,6 +140,8 @@ def value(self, value: numbers.Number) -> None: if not isinstance(value, numbers.Number) or isinstance(value, bool): raise TypeError(f'{value=} must be a number') self._scalar.value = float(value) + # Notify observers of the change + self.notify_observers() @property def unit(self) -> str: @@ -168,6 +185,8 @@ def variance(self, variance_float: float) -> None: raise ValueError(f'{variance_float=} must be positive') variance_float = float(variance_float) self._scalar.variance = variance_float + # Notify observers of the change + self.notify_observers() @property def error(self) -> float: @@ -197,6 +216,8 @@ def error(self, value: float) -> None: self._scalar.variance = value**2 else: self._scalar.variance = None + # Notify observers of the change + self.notify_observers() def convert_unit(self, unit_str: str) -> None: """ @@ -228,7 +249,8 @@ def set_scalar(obj, scalar): # Update the scalar self._scalar = new_scalar - + # Notify observers of the change + self.notify_observers() # Just to get return type right def __copy__(self) -> DescriptorNumber: From df1d138f410df125eaf6809bbe5d2aa5d43d723b Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 2 Apr 2025 14:30:43 +0200 Subject: [PATCH 03/58] rename and move observer methods --- .../Objects/variable/descriptor_number.py | 19 ++++++++++--------- src/easyscience/Objects/variable/parameter.py | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index b162a22a..088b6d08 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -47,6 +47,8 @@ def __init__( param parent: Parent of the descriptor .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ + self._observers: List[DescriptorNumber] = [] + if not isinstance(value, numbers.Number) or isinstance(value, bool): raise TypeError(f'{value=} must be a number') if variance is not None: @@ -74,7 +76,6 @@ def __init__( if self.unit is not None: self.convert_unit(self._base_unit()) - self._observers: List[DescriptorNumber] = [] @classmethod def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumber: @@ -92,18 +93,18 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumb raise TypeError(f'{full_value=} must be a scipp scalar') return cls(name=name, value=full_value.value, unit=full_value.unit, variance=full_value.variance, **kwargs) - def attach_observer(self, observer: DescriptorNumber) -> None: + def _attach_observer(self, observer: DescriptorNumber) -> None: """Attach an observer to the descriptor.""" self._observers.append(observer) - def detach_observer(self, observer: DescriptorNumber) -> None: + def _detach_observer(self, observer: DescriptorNumber) -> None: """Detach an observer from the descriptor.""" self._observers.remove(observer) - def notify_observers(self) -> None: + def _notify_observers(self) -> None: """Notify all observers of a change.""" for observer in self._observers: - observer.update(self) + observer._update(self) @property def full_value(self) -> Variable: @@ -141,7 +142,7 @@ def value(self, value: numbers.Number) -> None: raise TypeError(f'{value=} must be a number') self._scalar.value = float(value) # Notify observers of the change - self.notify_observers() + self._notify_observers() @property def unit(self) -> str: @@ -186,7 +187,7 @@ def variance(self, variance_float: float) -> None: variance_float = float(variance_float) self._scalar.variance = variance_float # Notify observers of the change - self.notify_observers() + self._notify_observers() @property def error(self) -> float: @@ -217,7 +218,7 @@ def error(self, value: float) -> None: else: self._scalar.variance = None # Notify observers of the change - self.notify_observers() + self._notify_observers() def convert_unit(self, unit_str: str) -> None: """ @@ -250,7 +251,7 @@ def set_scalar(obj, scalar): # Update the scalar self._scalar = new_scalar # Notify observers of the change - self.notify_observers() + self._notify_observers() # Just to get return type right def __copy__(self) -> DescriptorNumber: diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 4a4edc5a..d031b95f 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -11,6 +11,7 @@ from types import MappingProxyType from typing import Any from typing import Dict +from typing import List from typing import Optional from typing import Tuple from typing import Union @@ -78,12 +79,12 @@ def __init__( .. note:: Undo/Redo functionality is implemented for the attributes `value`, `variance`, `error`, `min`, `max`, `bounds`, `fixed`, `unit` """ # noqa: E501 + if not isinstance(value, numbers.Number): + raise TypeError('`value` must be a number') if not isinstance(min, numbers.Number): raise TypeError('`min` must be a number') if not isinstance(max, numbers.Number): raise TypeError('`max` must be a number') - if not isinstance(value, numbers.Number): - raise TypeError('`value` must be a number') if value < min: raise ValueError(f'{value=} can not be less than {min=}') if value > max: @@ -125,6 +126,13 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) + self._observers: List[DescriptorNumber] = [] + + def _update(self) -> None: + """ + Update the parameter. This is called by the interface when the parameter is changed. + """ + @property def value_no_call_back(self) -> numbers.Number: """ @@ -207,6 +215,9 @@ def value(self, value: numbers.Number) -> None: if self._callback.fset is not None: self._callback.fset(self._scalar.value) + # Notify observers of the change + self._notify_observers() + def convert_unit(self, unit_str: str) -> None: """ Perform unit conversion. The value, max and min can change on unit change. @@ -218,6 +229,7 @@ def convert_unit(self, unit_str: str) -> None: new_unit = sc.Unit(unit_str) # unit_str is tested in super method self._min = self._min.to(unit=new_unit) self._max = self._max.to(unit=new_unit) + self._notify_observers() @property def min(self) -> numbers.Number: @@ -246,6 +258,7 @@ def min(self, min_value: numbers.Number) -> None: self._min.value = min_value else: raise ValueError(f'The current value ({self.value}) is smaller than the desired min value ({min_value}).') + self._notify_observers() @property def max(self) -> numbers.Number: @@ -274,6 +287,7 @@ def max(self, max_value: numbers.Number) -> None: self._max.value = max_value else: raise ValueError(f'The current value ({self.value}) is greater than the desired max value ({max_value}).') + self._notify_observers() @property def fixed(self) -> bool: From 7b61c5cbc4ede8582432d9632e1f61b3643659ed Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 2 Apr 2025 14:43:35 +0200 Subject: [PATCH 04/58] Add the dependency_interpreter to the global_object --- src/easyscience/global_object/global_object.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index c78db4ef..fde25951 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -5,6 +5,8 @@ __author__ = 'github.com/wardsimon' __version__ = '0.1.0' +from asteval import Interpreter + from easyscience.Utils.classUtils import singleton from .hugger.hugger import ScriptManager @@ -19,6 +21,8 @@ class GlobalObject: into the collective. """ + __dependency_interpreter = Interpreter(minimal=True) + __dependency_interpreter.config['if'] = True __log = Logger() __map = Map() __stack = None @@ -35,6 +39,8 @@ def __init__(self): self.script: ScriptManager = ScriptManager() # Map. This is the conduit database between all global object species self.map: Map = self.__map + # Dependency interpreter. This is used to evaluate dependencies in dependent Parameters + self.dependency_interpreter: Interpreter = self.__dependency_interpreter def instantiate_stack(self): """ From 6fd5d7a546f551d7f0d4b9e57811aefa7586f2d8 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 2 Apr 2025 16:06:07 +0200 Subject: [PATCH 05/58] implement the _update method --- src/easyscience/Objects/variable/parameter.py | 11 +++++++++-- src/easyscience/global_object/global_object.py | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index d031b95f..aa47ee79 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -130,8 +130,15 @@ def __init__( def _update(self) -> None: """ - Update the parameter. This is called by the interface when the parameter is changed. - """ + Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. + """ + temporary_parameter = self._global_object.dependency_interpreter(self._dependency_string) + self._scalar.value = temporary_parameter.value + self._scalar.unit = temporary_parameter.unit + self._scalar.variance = temporary_parameter.variance + self._min.value = temporary_parameter.min + self._max.value = temporary_parameter.max + self._notify_observers() @property def value_no_call_back(self) -> numbers.Number: diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index fde25951..738c4bf9 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -23,6 +23,7 @@ class GlobalObject: __dependency_interpreter = Interpreter(minimal=True) __dependency_interpreter.config['if'] = True + __dependency_interpreter.readonly_symbols = [] __log = Logger() __map = Map() __stack = None From 2d41091362ca38631887389426a85313a93aaa55 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 3 Apr 2025 14:27:34 +0200 Subject: [PATCH 06/58] Update constructor to accept strings as values --- src/easyscience/Objects/variable/parameter.py | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index aa47ee79..e3b55574 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -6,6 +6,7 @@ import copy import numbers +import warnings import weakref from collections import namedtuple from types import MappingProxyType @@ -55,7 +56,6 @@ def __init__( url: Optional[str] = None, display_name: Optional[str] = None, callback: property = property(), - independent: Optional[bool] = True, parent: Optional[Any] = None, ): """ @@ -69,32 +69,53 @@ def __init__( :param variance: The variance of the value :param min: The minimum value for fitting :param max: The maximum value for fitting - :param fixed: Can the parameter vary while fitting? + :param fixed: Can the parameter vary? :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed - :param independent: Can the objects value be set + :param independent: Is the object dependent on another object? :param parent: The object which is the parent to this one .. note:: Undo/Redo functionality is implemented for the attributes `value`, `variance`, `error`, `min`, `max`, `bounds`, `fixed`, `unit` """ # noqa: E501 - if not isinstance(value, numbers.Number): - raise TypeError('`value` must be a number') - if not isinstance(min, numbers.Number): - raise TypeError('`min` must be a number') - if not isinstance(max, numbers.Number): - raise TypeError('`max` must be a number') - if value < min: - raise ValueError(f'{value=} can not be less than {min=}') - if value > max: - raise ValueError(f'{value=} can not be greater than {max=}') - - if np.isclose(min, max, rtol=1e-9, atol=0.0): - raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') - if not isinstance(fixed, bool): - raise TypeError('`fixed` must be either True or False') - + self._observers: List[DescriptorNumber] = [] + # If value is a string, the Parameter is a dependent parameter. + # The string is then run through the dependency interpreter to overwrite the value, unit, variance, min and max. # noqa: E501 + if isinstance(value, str): + if unit !='': + warnings.warn('Dependent parameters infer their unit from their dependency. The set unit will be ignored.') + if variance != 0.0: + warnings.warn('Dependent parameters compute their variance from their dependency. The set variance will be ignored.') # noqa: E501 + if min != -np.inf: + warnings.warn('Dependent parameters compute their minimum value from their dependency. The set min will be ignored.') # noqa: E501 + if max != np.inf: + warnings.warn('Dependent parameters compute their maximum value from their dependency. The set max will be ignored.') # noqa: E501 + self._dependency_string = value + self._independent = False + try: + dependency_result = self._global_object.dependency_interpreter(self._dependency_string) + except Exception as message: + raise ValueError(f'Invalid dependency expression: {self._dependency_string}') from message + value = dependency_result.value + unit = dependency_result.unit + variance = dependency_result.variance + min = dependency_result.min if isinstance(dependency_result, Parameter) else -np.inf + max = dependency_result.max if isinstance(dependency_result, Parameter) else np.inf + elif isinstance(value, numbers.Number): + if not isinstance(max, numbers.Number): + raise TypeError('`max` must be a number') + if value < min: + raise ValueError(f'{value=} can not be less than {min=}') + if value > max: + raise ValueError(f'{value=} can not be greater than {max=}') + if np.isclose(min, max, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') + if not isinstance(fixed, bool): + raise TypeError('`fixed` must be either True or False') + self._independent = True + else: + raise TypeError('`value` must be a number or a string representing a valid dependency expression.') self._fixed = fixed # For fitting, but must be initialized before super().__init__ self._min = sc.scalar(float(min), unit=unit) self._max = sc.scalar(float(max), unit=unit) @@ -116,8 +137,6 @@ def __init__( weakref.finalize(self, self._callback.fdel) # Create additional fitting elements - self._fixed = fixed - self._independent = independent self._initial_scalar = copy.deepcopy(self._scalar) builtin_constraint = { # Last argument in constructor is the name of the property holding the value of the constraint @@ -126,19 +145,21 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) - self._observers: List[DescriptorNumber] = [] def _update(self) -> None: """ Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. """ - temporary_parameter = self._global_object.dependency_interpreter(self._dependency_string) - self._scalar.value = temporary_parameter.value - self._scalar.unit = temporary_parameter.unit - self._scalar.variance = temporary_parameter.variance - self._min.value = temporary_parameter.min - self._max.value = temporary_parameter.max - self._notify_observers() + if not self._independent: + temporary_parameter = self._global_object.dependency_interpreter(self._dependency_string) + self._scalar.value = temporary_parameter.value + self._scalar.unit = temporary_parameter.unit + self._scalar.variance = temporary_parameter.variance + self._min.value = temporary_parameter.min + self._max.value = temporary_parameter.max + self._notify_observers() + else: + warnings.warn('This parameter is not dependent. It cannot be updated.') @property def value_no_call_back(self) -> numbers.Number: From 78532cd223f07a2a2e3b361ef3bff5a2f707b7ba Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 7 Apr 2025 12:52:17 +0200 Subject: [PATCH 07/58] Disable setters when parameter is dependent --- src/easyscience/Objects/variable/parameter.py | 114 +++++++++++------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index e3b55574..67f82da3 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -211,40 +211,62 @@ def value(self, value: numbers.Number) -> None: :param value: New value of self """ - if not self.independent: - if global_object.debug: - raise CoreSetException(f'{str(self)} is not independent.') - return + if self._independent: + if not isinstance(value, numbers.Number) or isinstance(value, bool): + raise TypeError(f'{value=} must be a number') + + # Need to set the value for constraints to be functional + self._scalar.value = float(value) + # if self._callback.fset is not None: + # self._callback.fset(self._scalar.value) + + # Deals with min/max + value = self._constraint_runner(self.builtin_constraints, self._scalar.value) + + # Deals with user constraints + # Changes should not be registrered in the undo/redo stack + stack_state = global_object.stack.enabled + if stack_state: + global_object.stack.force_state(False) + try: + value = self._constraint_runner(self.user_constraints, value) + finally: + global_object.stack.force_state(stack_state) - if not isinstance(value, numbers.Number) or isinstance(value, bool): - raise TypeError(f'{value=} must be a number') + value = self._constraint_runner(self._constraints.virtual, value) - # Need to set the value for constraints to be functional - self._scalar.value = float(value) - # if self._callback.fset is not None: - # self._callback.fset(self._scalar.value) + self._scalar.value = float(value) + if self._callback.fset is not None: + self._callback.fset(self._scalar.value) - # Deals with min/max - value = self._constraint_runner(self.builtin_constraints, self._scalar.value) + # Notify observers of the change + self._notify_observers() + else: + raise AttributeError("This parameter is not independent, its value cannot be set directly. Please make it independent first.") # noqa: E501 - # Deals with user constraints - # Changes should not be registrered in the undo/redo stack - stack_state = global_object.stack.enabled - if stack_state: - global_object.stack.force_state(False) - try: - value = self._constraint_runner(self.user_constraints, value) - finally: - global_object.stack.force_state(stack_state) + @DescriptorNumber.variance.setter + def variance(self, variance_float: float) -> None: + """ + Set the variance. - value = self._constraint_runner(self._constraints.virtual, value) + :param variance_float: Variance as a float + """ + if self._independent: + DescriptorNumber.variance.fset(self, variance_float) + else: + raise AttributeError("This parameter is not independent, its variance cannot be set directly. Please make it independent first.") # noqa: E501 - self._scalar.value = float(value) - if self._callback.fset is not None: - self._callback.fset(self._scalar.value) + @DescriptorNumber.error.setter + def error(self, value: float) -> None: + """ + Set the standard deviation for the parameter. - # Notify observers of the change - self._notify_observers() + :param value: New error value + """ + if self._independent: + DescriptorNumber.error.fset(self, value) + else: + raise AttributeError("This parameter is not independent, its error cannot be set directly. Please make it independent first.") # noqa: E501 def convert_unit(self, unit_str: str) -> None: """ @@ -278,15 +300,18 @@ def min(self, min_value: numbers.Number) -> None: :param min_value: new minimum value :return: None """ - if not isinstance(min_value, numbers.Number): - raise TypeError('`min` must be a number') - if np.isclose(min_value, self._max.value, rtol=1e-9, atol=0.0): - raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') - if min_value <= self.value: - self._min.value = min_value + if self._independent: + if not isinstance(min_value, numbers.Number): + raise TypeError('`min` must be a number') + if np.isclose(min_value, self._max.value, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') + if min_value <= self.value: + self._min.value = min_value + else: + raise ValueError(f'The current value ({self.value}) is smaller than the desired min value ({min_value}).') + self._notify_observers() else: - raise ValueError(f'The current value ({self.value}) is smaller than the desired min value ({min_value}).') - self._notify_observers() + raise AttributeError("This parameter is not independent, its min cannot be set directly. Please make it independent first.") # noqa: E501 @property def max(self) -> numbers.Number: @@ -307,15 +332,18 @@ def max(self, max_value: numbers.Number) -> None: :param max_value: new maximum value :return: None """ - if not isinstance(max_value, numbers.Number): - raise TypeError('`max` must be a number') - if np.isclose(max_value, self._min.value, rtol=1e-9, atol=0.0): - raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') - if max_value >= self.value: - self._max.value = max_value + if self._independent: + if not isinstance(max_value, numbers.Number): + raise TypeError('`max` must be a number') + if np.isclose(max_value, self._min.value, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') + if max_value >= self.value: + self._max.value = max_value + else: + raise ValueError(f'The current value ({self.value}) is greater than the desired max value ({max_value}).') + self._notify_observers() else: - raise ValueError(f'The current value ({self.value}) is greater than the desired max value ({max_value}).') - self._notify_observers() + raise AttributeError("This parameter is not independent, its max cannot be set directly. Please make it independent first.") # noqa: E501 @property def fixed(self) -> bool: From 5ec44a58599d1ab4bfa90172f8f7abd84534b92a Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 8 Apr 2025 15:04:09 +0200 Subject: [PATCH 08/58] Allow unique_names in dependency expression --- .../Objects/variable/descriptor_number.py | 2 +- src/easyscience/Objects/variable/parameter.py | 58 ++++++++++++++----- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index 088b6d08..f5aec194 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -104,7 +104,7 @@ def _detach_observer(self, observer: DescriptorNumber) -> None: def _notify_observers(self) -> None: """Notify all observers of a change.""" for observer in self._observers: - observer._update(self) + observer._update() @property def full_value(self) -> Variable: diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 67f82da3..14b65cf1 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -6,6 +6,7 @@ import copy import numbers +import re import warnings import weakref from collections import namedtuple @@ -93,8 +94,9 @@ def __init__( warnings.warn('Dependent parameters compute their maximum value from their dependency. The set max will be ignored.') # noqa: E501 self._dependency_string = value self._independent = False + self._process_dependency_unique_names(self._dependency_string) try: - dependency_result = self._global_object.dependency_interpreter(self._dependency_string) + dependency_result = self._global_object.dependency_interpreter(self._clean_dependency_string) except Exception as message: raise ValueError(f'Invalid dependency expression: {self._dependency_string}') from message value = dependency_result.value @@ -151,7 +153,7 @@ def _update(self) -> None: Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. """ if not self._independent: - temporary_parameter = self._global_object.dependency_interpreter(self._dependency_string) + temporary_parameter = self._global_object.dependency_interpreter(self._clean_dependency_string) self._scalar.value = temporary_parameter.value self._scalar.unit = temporary_parameter.unit self._scalar.variance = temporary_parameter.variance @@ -390,6 +392,7 @@ def bounds(self) -> Tuple[numbers.Number, numbers.Number]: :return: Tuple of the parameters minimum and maximum values """ return self.min, self.max + @bounds.setter def bounds(self, new_bound: Tuple[numbers.Number, numbers.Number]) -> None: """ @@ -439,6 +442,25 @@ def builtin_constraints(self) -> Dict[str, SelfConstraint]: """ return MappingProxyType(self._constraints.builtin) + @property + def independent(self) -> bool: + """ + Logical property to see if the objects value can be directly set. + + :return: Can the objects value be set + """ + return self._independent + + @independent.setter + @property_stack_deco + def independent(self, value: bool) -> None: + """ + Enable and disable the direct setting of an objects value field. + + :param value: True - objects value can be set, False - the opposite + """ + self._independent = value + @property def user_constraints(self) -> Dict[str, ConstraintBase]: """ @@ -470,24 +492,28 @@ def _constraint_runner( value = constained_value return value - @property - def independent(self) -> bool: - """ - Logical property to see if the objects value can be directly set. - - :return: Can the objects value be set + def _process_dependency_unique_names(self, dependency_expression: str): """ - return self._independent + Add the unique names of the parameters to the ASTEval interpreter. This is used to evaluate the dependency expression. - @independent.setter - @property_stack_deco - def independent(self, value: bool) -> None: + :param dependency_expression: The dependency expression to be evaluated """ - Enable and disable the direct setting of an objects value field. + # Get the unique_names from the expression string regardless of the quotes used + inputted_unique_names = re.findall("(\'.+?\')", dependency_expression) + inputted_unique_names += re.findall('(\".+?\")', dependency_expression) - :param value: True - objects value can be set, False - the opposite - """ - self._independent = value + clean_dependency_string = dependency_expression + existing_unique_names = self._global_object.map.vertices() + # Add the unique names of the parameters to the ASTEVAL interpreter + for name in inputted_unique_names: + stripped_name = name.strip("'\"") + if stripped_name not in existing_unique_names: + raise ValueError(f'A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression.') # noqa: E501 + dependent_parameter = self._global_object.map.get_item_by_key(stripped_name) + self._global_object.dependency_interpreter.symtable['__'+stripped_name+'__'] = dependent_parameter + dependent_parameter._attach_observer(self) + clean_dependency_string = clean_dependency_string.replace(name, '__'+stripped_name+'__') + self._clean_dependency_string = clean_dependency_string def __copy__(self) -> Parameter: new_obj = super().__copy__() From cb4d2c3a5313125c954b68cb0aac1a12bb14c9fc Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 10 Apr 2025 10:29:16 +0200 Subject: [PATCH 09/58] Allow symbols from the global scope in the dependency expression --- src/easyscience/Objects/variable/parameter.py | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 14b65cf1..cddbccb8 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -4,6 +4,7 @@ from __future__ import annotations +import ast import copy import numbers import re @@ -94,6 +95,7 @@ def __init__( warnings.warn('Dependent parameters compute their maximum value from their dependency. The set max will be ignored.') # noqa: E501 self._dependency_string = value self._independent = False + self._process_dependency_symbol_names(self._dependency_string) self._process_dependency_unique_names(self._dependency_string) try: dependency_result = self._global_object.dependency_interpreter(self._clean_dependency_string) @@ -510,11 +512,58 @@ def _process_dependency_unique_names(self, dependency_expression: str): if stripped_name not in existing_unique_names: raise ValueError(f'A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression.') # noqa: E501 dependent_parameter = self._global_object.map.get_item_by_key(stripped_name) - self._global_object.dependency_interpreter.symtable['__'+stripped_name+'__'] = dependent_parameter - dependent_parameter._attach_observer(self) - clean_dependency_string = clean_dependency_string.replace(name, '__'+stripped_name+'__') + if isinstance(dependent_parameter, DescriptorNumber): + self._global_object.dependency_interpreter.symtable['__'+stripped_name+'__'] = dependent_parameter + dependent_parameter._attach_observer(self) + clean_dependency_string = clean_dependency_string.replace(name, '__'+stripped_name+'__') + else: + raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter/DescriptorNumber. Please check your dependency expression.') # noqa: E501 self._clean_dependency_string = clean_dependency_string + def _process_dependency_symbol_names(self, dependency_expression: str): + """ + Add the symbol names of the parameters to the ASTEval interpreter. This is used to evaluate the dependency expression. + + :param dependency_expression: The dependency expression to be evaluated + """ + # Get the symbol names in the dependency expression by walking the abstract syntax tree with ast. + + abstract_syntax_tree = ast.parse(dependency_expression) + abstract_syntax_tree_nodes = ast.walk(abstract_syntax_tree) + for node in abstract_syntax_tree_nodes: + # If the node is a Name, check if it is in globals and add it to the interpreter + if isinstance(node, ast.Name): + name = node.id + if name in globals(): + object = globals()[name] + if isinstance(object, DescriptorNumber): + self._global_object.dependency_interpreter.symtable[name] = object + object._attach_observer(self) + else: + raise ValueError(f'Object {name} not found in globals. Please check your dependency expression.') + # If the node is an attribute, get the attribute tree and check if the last element ie. the Name node is in globals + elif isinstance(node, ast.Attribute): + attribute_list = self._get_attribute_tree(node) + object_name = attribute_list[-1] + if object_name in globals(): + object = globals()[object_name] + else: + raise ValueError(f'Object {object_name} not found in globals. Please check your dependency expression.') # noqa: E501 + attribute_object = eval('.'.join(attribute_list.reverse())) # noqa: S307 + if isinstance(attribute_object, DescriptorNumber): + self._global_object.dependency_interpreter.symtable[object_name] = object + attribute_object._attach_observer(self) + + def _get_attribute_tree(self, node: ast.Attribute, attribute_list: list = []) -> list: + if isinstance(node, ast.Attribute): + attribute_list.append(node.attr) + return self._get_attribute_tree(node.value, attribute_list) + elif isinstance(node, ast.Name): + attribute_list.append(node.id) + return attribute_list + else: + raise ValueError(f'Invalid node type: {type(node)}') + def __copy__(self) -> Parameter: new_obj = super().__copy__() new_obj._callback = property() From addac0c94190f4cd93282a314a6bab0b6ddfa50f Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 15 Apr 2025 11:48:30 +0200 Subject: [PATCH 10/58] Change symbol names to come from a dependency_map dict input rather than from the global scope --- .../Objects/variable/descriptor_number.py | 54 ++++++--- src/easyscience/Objects/variable/parameter.py | 113 +++++++----------- .../global_object/global_object.py | 7 -- 3 files changed, 81 insertions(+), 93 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index f5aec194..beddcf2b 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -18,6 +18,22 @@ from .descriptor_base import DescriptorBase +# Why is this a decorator? Because otherwise we would need a flag on the convert_unit method to avoid +# infinite recursion. This is a bit cleaner as it avoids the need for a internal only flag on a user method. +def notify_observers(func): + """ + Decorator to notify observers of a change in the descriptor. + + :param func: Function to be decorated + :return: Decorated function + """ + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + self._notify_observers() + return result + + return wrapper + class DescriptorNumber(DescriptorBase): """ A `Descriptor` for Number values with units. The internal representation is a scipp scalar. @@ -74,7 +90,7 @@ def __init__( # Call convert_unit during initialization to ensure that the unit has no numbers in it, and to ensure unit consistency. if self.unit is not None: - self.convert_unit(self._base_unit()) + self._convert_unit(self._base_unit()) @classmethod @@ -131,6 +147,7 @@ def value(self) -> numbers.Number: return self._scalar.value @value.setter + @notify_observers @property_stack_deco def value(self, value: numbers.Number) -> None: """ @@ -141,8 +158,6 @@ def value(self, value: numbers.Number) -> None: if not isinstance(value, numbers.Number) or isinstance(value, bool): raise TypeError(f'{value=} must be a number') self._scalar.value = float(value) - # Notify observers of the change - self._notify_observers() @property def unit(self) -> str: @@ -172,6 +187,7 @@ def variance(self) -> float: return self._scalar.variance @variance.setter + @notify_observers @property_stack_deco def variance(self, variance_float: float) -> None: """ @@ -186,8 +202,6 @@ def variance(self, variance_float: float) -> None: raise ValueError(f'{variance_float=} must be positive') variance_float = float(variance_float) self._scalar.variance = variance_float - # Notify observers of the change - self._notify_observers() @property def error(self) -> float: @@ -201,6 +215,7 @@ def error(self) -> float: return float(np.sqrt(self._scalar.variance)) @error.setter + @notify_observers @property_stack_deco def error(self, value: float) -> None: """ @@ -217,10 +232,8 @@ def error(self, value: float) -> None: self._scalar.variance = value**2 else: self._scalar.variance = None - # Notify observers of the change - self._notify_observers() - def convert_unit(self, unit_str: str) -> None: + def _convert_unit(self, unit_str: str) -> None: """ Convert the value from one unit system to another. @@ -250,8 +263,15 @@ def set_scalar(obj, scalar): # Update the scalar self._scalar = new_scalar - # Notify observers of the change - self._notify_observers() + + @notify_observers + def convert_unit(self, unit_str: str) -> None: + """ + Convert the value from one unit system to another. + + :param unit_str: New unit in string form + """ + self._convert_unit(unit_str) # Just to get return type right def __copy__(self) -> DescriptorNumber: @@ -290,11 +310,11 @@ def __add__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN elif type(other) is DescriptorNumber: original_unit = other.unit try: - other.convert_unit(self.unit) + other._convert_unit(self.unit) except UnitError: raise UnitError(f'Values with units {self.unit} and {other.unit} cannot be added') from None new_value = self.full_value + other.full_value - other.convert_unit(original_unit) + other._convert_unit(original_unit) else: return NotImplemented descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) @@ -320,11 +340,11 @@ def __sub__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN elif type(other) is DescriptorNumber: original_unit = other.unit try: - other.convert_unit(self.unit) + other._convert_unit(self.unit) except UnitError: raise UnitError(f'Values with units {self.unit} and {other.unit} cannot be subtracted') from None new_value = self.full_value - other.full_value - other.convert_unit(original_unit) + other._convert_unit(original_unit) else: return NotImplemented descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) @@ -350,7 +370,7 @@ def __mul__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorN else: return NotImplemented descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number._convert_unit(descriptor_number._base_unit()) descriptor_number.name = descriptor_number.unique_name return descriptor_number @@ -378,7 +398,7 @@ def __truediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Descrip else: return NotImplemented descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) - descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number._convert_unit(descriptor_number._base_unit()) descriptor_number.name = descriptor_number.unique_name return descriptor_number @@ -445,4 +465,4 @@ def _base_unit(self) -> str: return string[i:] elif letter not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-']: return string[i:] - return '' + return '' \ No newline at end of file diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index cddbccb8..ba9b50f2 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -4,7 +4,6 @@ from __future__ import annotations -import ast import copy import numbers import re @@ -21,6 +20,7 @@ import numpy as np import scipp as sc +from asteval import Interpreter from scipp import UnitError from scipp import Variable @@ -31,6 +31,7 @@ from easyscience.Utils.Exceptions import CoreSetException from .descriptor_number import DescriptorNumber +from .descriptor_number import notify_observers Constraints = namedtuple('Constraints', ['user', 'builtin', 'virtual']) @@ -59,6 +60,7 @@ def __init__( display_name: Optional[str] = None, callback: property = property(), parent: Optional[Any] = None, + dependency_map: Optional[dict] = {}, ): """ This class is an extension of a `DescriptorNumber`. Where the descriptor was for static @@ -75,8 +77,8 @@ def __init__( :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed - :param independent: Is the object dependent on another object? :param parent: The object which is the parent to this one + :param dependency_map: A dictionary of dependencies. This is inserted into the asteval interpreter to resolve dependencies. .. note:: Undo/Redo functionality is implemented for the attributes `value`, `variance`, `error`, `min`, `max`, `bounds`, `fixed`, `unit` @@ -93,14 +95,24 @@ def __init__( warnings.warn('Dependent parameters compute their minimum value from their dependency. The set min will be ignored.') # noqa: E501 if max != np.inf: warnings.warn('Dependent parameters compute their maximum value from their dependency. The set max will be ignored.') # noqa: E501 + self._dependency_interpreter = Interpreter(minimal=True) + self._dependency_interpreter.config['if'] = True self._dependency_string = value + self._dependency_map = dependency_map self._independent = False - self._process_dependency_symbol_names(self._dependency_string) self._process_dependency_unique_names(self._dependency_string) + for key, value in self._dependency_map.items(): + if isinstance(value, DescriptorNumber): + self._dependency_interpreter.symtable[key] = value + value._attach_observer(self) + else: + raise TypeError(f'{key=} must be a DescriptorNumber or Parameter. Got {type(value)}') try: - dependency_result = self._global_object.dependency_interpreter(self._clean_dependency_string) - except Exception as message: - raise ValueError(f'Invalid dependency expression: {self._dependency_string}') from message + dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) + except NameError as message: + raise NameError('\nUnknown name encountered in dependecy expression:'+ + '\n'+'\n'.join(str(message).split("\n")[1:])+ + '\nPlease check your expression or add the name to the dependency_map') from None value = dependency_result.value unit = dependency_result.unit variance = dependency_result.variance @@ -149,13 +161,12 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) - def _update(self) -> None: """ Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. """ if not self._independent: - temporary_parameter = self._global_object.dependency_interpreter(self._clean_dependency_string) + temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) self._scalar.value = temporary_parameter.value self._scalar.unit = temporary_parameter.unit self._scalar.variance = temporary_parameter.variance @@ -272,18 +283,27 @@ def error(self, value: float) -> None: else: raise AttributeError("This parameter is not independent, its error cannot be set directly. Please make it independent first.") # noqa: E501 - def convert_unit(self, unit_str: str) -> None: + def _convert_unit(self, unit_str: str) -> None: """ Perform unit conversion. The value, max and min can change on unit change. :param new_unit: new unit :return: None """ - super().convert_unit(unit_str) + super()._convert_unit(unit_str=unit_str) new_unit = sc.Unit(unit_str) # unit_str is tested in super method self._min = self._min.to(unit=new_unit) self._max = self._max.to(unit=new_unit) - self._notify_observers() + + @notify_observers + def convert_unit(self, unit_str: str) -> None: + """ + Perform unit conversion. The value, max and min can change on unit change. + + :param new_unit: new unit + :return: None + """ + self._convert_unit(unit_str) @property def min(self) -> numbers.Number: @@ -513,57 +533,12 @@ def _process_dependency_unique_names(self, dependency_expression: str): raise ValueError(f'A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression.') # noqa: E501 dependent_parameter = self._global_object.map.get_item_by_key(stripped_name) if isinstance(dependent_parameter, DescriptorNumber): - self._global_object.dependency_interpreter.symtable['__'+stripped_name+'__'] = dependent_parameter - dependent_parameter._attach_observer(self) + self._dependency_map['__'+stripped_name+'__'] = dependent_parameter clean_dependency_string = clean_dependency_string.replace(name, '__'+stripped_name+'__') else: raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter/DescriptorNumber. Please check your dependency expression.') # noqa: E501 self._clean_dependency_string = clean_dependency_string - def _process_dependency_symbol_names(self, dependency_expression: str): - """ - Add the symbol names of the parameters to the ASTEval interpreter. This is used to evaluate the dependency expression. - - :param dependency_expression: The dependency expression to be evaluated - """ - # Get the symbol names in the dependency expression by walking the abstract syntax tree with ast. - - abstract_syntax_tree = ast.parse(dependency_expression) - abstract_syntax_tree_nodes = ast.walk(abstract_syntax_tree) - for node in abstract_syntax_tree_nodes: - # If the node is a Name, check if it is in globals and add it to the interpreter - if isinstance(node, ast.Name): - name = node.id - if name in globals(): - object = globals()[name] - if isinstance(object, DescriptorNumber): - self._global_object.dependency_interpreter.symtable[name] = object - object._attach_observer(self) - else: - raise ValueError(f'Object {name} not found in globals. Please check your dependency expression.') - # If the node is an attribute, get the attribute tree and check if the last element ie. the Name node is in globals - elif isinstance(node, ast.Attribute): - attribute_list = self._get_attribute_tree(node) - object_name = attribute_list[-1] - if object_name in globals(): - object = globals()[object_name] - else: - raise ValueError(f'Object {object_name} not found in globals. Please check your dependency expression.') # noqa: E501 - attribute_object = eval('.'.join(attribute_list.reverse())) # noqa: S307 - if isinstance(attribute_object, DescriptorNumber): - self._global_object.dependency_interpreter.symtable[object_name] = object - attribute_object._attach_observer(self) - - def _get_attribute_tree(self, node: ast.Attribute, attribute_list: list = []) -> list: - if isinstance(node, ast.Attribute): - attribute_list.append(node.attr) - return self._get_attribute_tree(node.value, attribute_list) - elif isinstance(node, ast.Name): - attribute_list.append(node.id) - return attribute_list - else: - raise ValueError(f'Invalid node type: {type(node)}') - def __copy__(self) -> Parameter: new_obj = super().__copy__() new_obj._callback = property() @@ -596,13 +571,13 @@ def __add__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here other_unit = other.unit try: - other.convert_unit(self.unit) + other._convert_unit(self.unit) except UnitError: raise UnitError(f'Values with units {self.unit} and {other.unit} cannot be added') from None new_full_value = self.full_value + other.full_value min_value = self.min + other.min if isinstance(other, Parameter) else self.min + other.value max_value = self.max + other.max if isinstance(other, Parameter) else self.max + other.value - other.convert_unit(other_unit) + other._convert_unit(other_unit) else: return NotImplemented parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) @@ -619,13 +594,13 @@ def __radd__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here original_unit = self.unit try: - self.convert_unit(other.unit) + self._convert_unit(other.unit) except UnitError: raise UnitError(f'Values with units {other.unit} and {self.unit} cannot be added') from None new_full_value = self.full_value + other.full_value min_value = self.min + other.value max_value = self.max + other.value - self.convert_unit(original_unit) + self._convert_unit(original_unit) else: return NotImplemented parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) @@ -642,7 +617,7 @@ def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here other_unit = other.unit try: - other.convert_unit(self.unit) + other._convert_unit(self.unit) except UnitError: raise UnitError(f'Values with units {self.unit} and {other.unit} cannot be subtracted') from None new_full_value = self.full_value - other.full_value @@ -652,7 +627,7 @@ def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> else: min_value = self.min - other.value max_value = self.max - other.value - other.convert_unit(other_unit) + other._convert_unit(other_unit) else: return NotImplemented parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) @@ -669,13 +644,13 @@ def __rsub__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here original_unit = self.unit try: - self.convert_unit(other.unit) + self._convert_unit(other.unit) except UnitError: raise UnitError(f'Values with units {other.unit} and {self.unit} cannot be subtracted') from None new_full_value = other.full_value - self.full_value min_value = other.value - self.max max_value = other.value - self.min - self.convert_unit(original_unit) + self._convert_unit(original_unit) else: return NotImplemented parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) @@ -719,7 +694,7 @@ def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> min_value = min(combinations) max_value = max(combinations) parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) - parameter.convert_unit(parameter._base_unit()) + parameter._convert_unit(parameter._base_unit()) parameter.name = parameter.unique_name return parameter @@ -743,7 +718,7 @@ def __rmul__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: min_value = min(combinations) max_value = max(combinations) parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) - parameter.convert_unit(parameter._base_unit()) + parameter._convert_unit(parameter._base_unit()) parameter.name = parameter.unique_name return parameter @@ -785,7 +760,7 @@ def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) min_value = min(combinations) max_value = max(combinations) parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) - parameter.convert_unit(parameter._base_unit()) + parameter._convert_unit(parameter._base_unit()) parameter.name = parameter.unique_name return parameter @@ -826,7 +801,7 @@ def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parame min_value = min(combinations) max_value = max(combinations) parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) - parameter.convert_unit(parameter._base_unit()) + parameter._convert_unit(parameter._base_unit()) parameter.name = parameter.unique_name self.value = original_self return parameter diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index 738c4bf9..c78db4ef 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -5,8 +5,6 @@ __author__ = 'github.com/wardsimon' __version__ = '0.1.0' -from asteval import Interpreter - from easyscience.Utils.classUtils import singleton from .hugger.hugger import ScriptManager @@ -21,9 +19,6 @@ class GlobalObject: into the collective. """ - __dependency_interpreter = Interpreter(minimal=True) - __dependency_interpreter.config['if'] = True - __dependency_interpreter.readonly_symbols = [] __log = Logger() __map = Map() __stack = None @@ -40,8 +35,6 @@ def __init__(self): self.script: ScriptManager = ScriptManager() # Map. This is the conduit database between all global object species self.map: Map = self.__map - # Dependency interpreter. This is used to evaluate dependencies in dependent Parameters - self.dependency_interpreter: Interpreter = self.__dependency_interpreter def instantiate_stack(self): """ From 047568c379c10e49ac6809c3c33f3bc59c5266f7 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 16 Apr 2025 15:30:20 +0200 Subject: [PATCH 11/58] Allow dependencies of dependent parameters. Check for cyclic dependencies --- .../Objects/variable/descriptor_number.py | 13 +++++++-- src/easyscience/Objects/variable/parameter.py | 29 ++++++++++++++----- .../global_object/global_object.py | 2 ++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index beddcf2b..f98e9c09 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -117,10 +117,17 @@ def _detach_observer(self, observer: DescriptorNumber) -> None: """Detach an observer from the descriptor.""" self._observers.remove(observer) - def _notify_observers(self) -> None: - """Notify all observers of a change.""" + def _notify_observers(self, update_id=None) -> None: + """Notify all observers of a change. + + :param update_id: Optional update ID to pass to observers. Used to avoid cyclic depenencies. + + """ + if update_id is None: + self._global_object.update_id_iterator += 1 + update_id = self._global_object.update_id_iterator for observer in self._observers: - observer._update() + observer._update(update_id=update_id, updating_object=self.unique_name) @property def full_value(self) -> Variable: diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index ba9b50f2..14faa183 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -99,11 +99,13 @@ def __init__( self._dependency_interpreter.config['if'] = True self._dependency_string = value self._dependency_map = dependency_map + self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies self._independent = False self._process_dependency_unique_names(self._dependency_string) for key, value in self._dependency_map.items(): if isinstance(value, DescriptorNumber): self._dependency_interpreter.symtable[key] = value + self._dependency_updates[value.unique_name] = 0 value._attach_observer(self) else: raise TypeError(f'{key=} must be a DescriptorNumber or Parameter. Got {type(value)}') @@ -161,18 +163,29 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) - def _update(self) -> None: + def _update(self, update_id: int, updating_object: str) -> None: """ Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. + + :param update_id: The id of the update. This is used to avoid cyclic dependencies. + :param updating_object: The unique_name of the object which is updating this parameter. + """ if not self._independent: - temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) - self._scalar.value = temporary_parameter.value - self._scalar.unit = temporary_parameter.unit - self._scalar.variance = temporary_parameter.variance - self._min.value = temporary_parameter.min - self._max.value = temporary_parameter.max - self._notify_observers() + # Check if this parameter has already been updated by the updating object with this update id + if self._dependency_updates[updating_object] == update_id: + warnings.warn('Warning: Cyclic dependency detected!\n' + + f'This parameter, {self.unique_name}, has already been updated by {updating_object} during this update.\n' + # noqa: E501 + 'This update will be ignored. Please check your dependencies.') + else: + # Update the value of the parameter using the dependency interpreter + temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) + self._scalar.value = temporary_parameter.value + self._scalar.unit = temporary_parameter.unit + self._scalar.variance = temporary_parameter.variance + self._min.value = temporary_parameter.min + self._max.value = temporary_parameter.max + self._notify_observers(update_id=update_id) else: warnings.warn('This parameter is not dependent. It cannot be updated.') diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index c78db4ef..dd188db2 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -36,6 +36,8 @@ def __init__(self): # Map. This is the conduit database between all global object species self.map: Map = self.__map + self.update_id_iterator = 0 + def instantiate_stack(self): """ The undo/redo stack references the collective. Hence it has to be imported From 878293ca60940f04754d344f7871f4b5012021a9 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 23 Apr 2025 13:46:03 +0200 Subject: [PATCH 12/58] Make method for making a parameter dependent after initialization. Move dependent parameter constructor to class method --- src/easyscience/Objects/variable/parameter.py | 139 +++++++++++------- 1 file changed, 86 insertions(+), 53 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 14faa183..2f6226df 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -60,7 +60,6 @@ def __init__( display_name: Optional[str] = None, callback: property = property(), parent: Optional[Any] = None, - dependency_map: Optional[dict] = {}, ): """ This class is an extension of a `DescriptorNumber`. Where the descriptor was for static @@ -78,62 +77,26 @@ def __init__( :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed :param parent: The object which is the parent to this one - :param dependency_map: A dictionary of dependencies. This is inserted into the asteval interpreter to resolve dependencies. .. note:: Undo/Redo functionality is implemented for the attributes `value`, `variance`, `error`, `min`, `max`, `bounds`, `fixed`, `unit` """ # noqa: E501 + if not isinstance(min, numbers.Number): + raise TypeError('`min` must be a number') + if not isinstance(max, numbers.Number): + raise TypeError('`max` must be a number') + if not isinstance(value, numbers.Number): + raise TypeError('`value` must be a number') + if value < min: + raise ValueError(f'{value=} can not be less than {min=}') + if value > max: + raise ValueError(f'{value=} can not be greater than {max=}') + if np.isclose(min, max, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') + if not isinstance(fixed, bool): + raise TypeError('`fixed` must be either True or False') + self._independent = True self._observers: List[DescriptorNumber] = [] - # If value is a string, the Parameter is a dependent parameter. - # The string is then run through the dependency interpreter to overwrite the value, unit, variance, min and max. # noqa: E501 - if isinstance(value, str): - if unit !='': - warnings.warn('Dependent parameters infer their unit from their dependency. The set unit will be ignored.') - if variance != 0.0: - warnings.warn('Dependent parameters compute their variance from their dependency. The set variance will be ignored.') # noqa: E501 - if min != -np.inf: - warnings.warn('Dependent parameters compute their minimum value from their dependency. The set min will be ignored.') # noqa: E501 - if max != np.inf: - warnings.warn('Dependent parameters compute their maximum value from their dependency. The set max will be ignored.') # noqa: E501 - self._dependency_interpreter = Interpreter(minimal=True) - self._dependency_interpreter.config['if'] = True - self._dependency_string = value - self._dependency_map = dependency_map - self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies - self._independent = False - self._process_dependency_unique_names(self._dependency_string) - for key, value in self._dependency_map.items(): - if isinstance(value, DescriptorNumber): - self._dependency_interpreter.symtable[key] = value - self._dependency_updates[value.unique_name] = 0 - value._attach_observer(self) - else: - raise TypeError(f'{key=} must be a DescriptorNumber or Parameter. Got {type(value)}') - try: - dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) - except NameError as message: - raise NameError('\nUnknown name encountered in dependecy expression:'+ - '\n'+'\n'.join(str(message).split("\n")[1:])+ - '\nPlease check your expression or add the name to the dependency_map') from None - value = dependency_result.value - unit = dependency_result.unit - variance = dependency_result.variance - min = dependency_result.min if isinstance(dependency_result, Parameter) else -np.inf - max = dependency_result.max if isinstance(dependency_result, Parameter) else np.inf - elif isinstance(value, numbers.Number): - if not isinstance(max, numbers.Number): - raise TypeError('`max` must be a number') - if value < min: - raise ValueError(f'{value=} can not be less than {min=}') - if value > max: - raise ValueError(f'{value=} can not be greater than {max=}') - if np.isclose(min, max, rtol=1e-9, atol=0.0): - raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') - if not isinstance(fixed, bool): - raise TypeError('`fixed` must be either True or False') - self._independent = True - else: - raise TypeError('`value` must be a number or a string representing a valid dependency expression.') self._fixed = fixed # For fitting, but must be initialized before super().__init__ self._min = sc.scalar(float(min), unit=unit) self._max = sc.scalar(float(max), unit=unit) @@ -163,6 +126,22 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) + @classmethod + def from_dependency(cls, name: str, dependency_expression: str, dependency_map: Optional[dict] = None, **kwargs) -> Parameter: # noqa: E501 + """ + Create a dependent Parameter directly from a dependency expression. + + :param name: The name of the parameter + :param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter. + :param dependency_map: A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies. + :param kwargs: Additional keyword arguments to pass to the Parameter constructor. + :return: A new dependent Parameter object. + """ # noqa: E501 + parameter = cls(name=name, value=0.0, unit='', variance=0.0, min=-np.inf, max=np.inf, **kwargs) + parameter.make_dependent(dependency_expression=dependency_expression, dependency_map=dependency_map) + return parameter + + def _update(self, update_id: int, updating_object: str) -> None: """ Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. @@ -189,6 +168,60 @@ def _update(self, update_id: int, updating_object: str) -> None: else: warnings.warn('This parameter is not dependent. It cannot be updated.') + def make_dependent(self, dependency_expression: str, dependency_map: Optional[dict] = None) -> None: + """ + Make this parameter dependent on another parameter. This will overwrite the current value, unit, variance, min and max. + + :param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter. + :param dependency_map: A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies. + """ # noqa: E501 + if not isinstance(dependency_expression, str): + raise TypeError('`dependency_expression` must be a string representing a valid dependency expression.') + if not (isinstance(dependency_map, dict) or dependency_map is None): + raise TypeError('`dependency_map` must be a dictionary of dependencies and their corresponding names in the dependecy expression.') # noqa: E501 + for key, value in self._dependency_map.items(): + if not isinstance(key, str): + raise TypeError('`dependency_map` keys must be strings representing the names of the dependencies in the dependency expression.') # noqa: E501 + if not isinstance(value, DescriptorNumber): + raise TypeError(f'`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}.') # noqa: E501 + + # If we're overwriting the dependency + if not self._independent: + for old_dependency in self._dependency_map.values(): + old_dependency._detach_observer(self) + + self._dependency_string = dependency_expression + self._dependency_map = dependency_map if dependency_map is not None else {} + self._dependency_interpreter = Interpreter(minimal=True) + self._dependency_interpreter.config['if'] = True + self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies + + self._process_dependency_unique_names(self._dependency_string) + for key, value in self._dependency_map.items(): + self._dependency_interpreter.symtable[key] = value + self._dependency_interpreter.readonly_symbols.add(key) + value._attach_observer(self) + try: + dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) + except NameError as message: + raise NameError('\nUnknown name encountered in dependecy expression:'+ + '\n'+'\n'.join(str(message).split("\n")[1:])+ + '\nPlease check your expression or add the name to the `dependency_map`') from None + except Exception as message: + raise Exception('\nError encountered in dependecy expression:'+ + '\n'+'\n'.join(str(message).split("\n")[1:])+ + '\nPlease check your expression') from None + if not isinstance(dependency_result, DescriptorNumber): + raise TypeError(f'The dependency expression: "{self._clean_dependency_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 + self._scalar.value = dependency_result.value + self._scalar.unit = dependency_result.unit + self._scalar.variance = dependency_result.variance + self._min = dependency_result.min if isinstance(dependency_result, Parameter) else dependency_result.value + self._max = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value + self._independent = False + self._fixed = False + self._notify_observers() + @property def value_no_call_back(self) -> numbers.Number: """ @@ -549,7 +582,7 @@ def _process_dependency_unique_names(self, dependency_expression: str): self._dependency_map['__'+stripped_name+'__'] = dependent_parameter clean_dependency_string = clean_dependency_string.replace(name, '__'+stripped_name+'__') else: - raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter/DescriptorNumber. Please check your dependency expression.') # noqa: E501 + raise ValueError(f'The object with unique_name {stripped_name} is not a Parameter or DescriptorNumber. Please check your dependency expression.') # noqa: E501 self._clean_dependency_string = clean_dependency_string def __copy__(self) -> Parameter: From 51bd9c48e2b3a025d4221f50591cbc87c76cfa2e Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 23 Apr 2025 14:47:32 +0200 Subject: [PATCH 13/58] Add make_dependent method --- src/easyscience/Objects/variable/parameter.py | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 2f6226df..ccc99a4f 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -222,6 +222,25 @@ def make_dependent(self, dependency_expression: str, dependency_map: Optional[di self._fixed = False self._notify_observers() + def make_independent(self) -> None: + """ + Make this parameter independent. + This will remove the dependency expression, the dependency map and the dependency interpreter. + + :return: None + """ + if not self._independent: + for dependency in self._dependency_map.values(): + dependency._detach_observer(self) + self._independent = True + del self._dependency_map + del self._dependency_updates + del self._dependency_interpreter + del self._dependency_string + del self._clean_dependency_string + else: + raise AttributeError('This parameter is already independent.') + @property def value_no_call_back(self) -> numbers.Number: """ @@ -303,7 +322,7 @@ def value(self, value: numbers.Number) -> None: # Notify observers of the change self._notify_observers() else: - raise AttributeError("This parameter is not independent, its value cannot be set directly. Please make it independent first.") # noqa: E501 + raise AttributeError("This is a dependent parameter, its value cannot be set directly.") @DescriptorNumber.variance.setter def variance(self, variance_float: float) -> None: @@ -315,7 +334,7 @@ def variance(self, variance_float: float) -> None: if self._independent: DescriptorNumber.variance.fset(self, variance_float) else: - raise AttributeError("This parameter is not independent, its variance cannot be set directly. Please make it independent first.") # noqa: E501 + raise AttributeError("This is a dependent parameter, its variance cannot be set directly.") @DescriptorNumber.error.setter def error(self, value: float) -> None: @@ -327,7 +346,7 @@ def error(self, value: float) -> None: if self._independent: DescriptorNumber.error.fset(self, value) else: - raise AttributeError("This parameter is not independent, its error cannot be set directly. Please make it independent first.") # noqa: E501 + raise AttributeError("This is a dependent parameter, its error cannot be set directly.") def _convert_unit(self, unit_str: str) -> None: """ @@ -381,7 +400,7 @@ def min(self, min_value: numbers.Number) -> None: raise ValueError(f'The current value ({self.value}) is smaller than the desired min value ({min_value}).') self._notify_observers() else: - raise AttributeError("This parameter is not independent, its min cannot be set directly. Please make it independent first.") # noqa: E501 + raise AttributeError("This is a dependent parameter, its minimum value cannot be set directly.") @property def max(self) -> numbers.Number: @@ -413,7 +432,7 @@ def max(self, max_value: numbers.Number) -> None: raise ValueError(f'The current value ({self.value}) is greater than the desired max value ({max_value}).') self._notify_observers() else: - raise AttributeError("This parameter is not independent, its max cannot be set directly. Please make it independent first.") # noqa: E501 + raise AttributeError("This is a dependent parameter, its maximum value cannot be set directly.") @property def fixed(self) -> bool: @@ -433,17 +452,17 @@ def fixed(self, fixed: bool) -> None: :param fixed: True = fixed, False = can vary """ - if not self.independent: + if not isinstance(fixed, bool): + raise ValueError(f'{fixed=} must be a boolean. Got {type(fixed)}') + if self._independent: + self._fixed = fixed + else: if self._global_object.stack.enabled: # Remove the recorded change from the stack global_object.stack.pop() - if global_object.debug: - raise CoreSetException(f'{str(self)} is not independent.') - return - if not isinstance(fixed, bool): - raise ValueError(f'{fixed=} must be a boolean. Got {type(fixed)}') - self._fixed = fixed + raise AttributeError("This is a dependent parameter, dependent parameters cannot be fixed.") + # Is this alias really needed? @property def free(self) -> bool: return not self.fixed @@ -452,6 +471,14 @@ def free(self) -> bool: def free(self, value: bool) -> None: self.fixed = not value + def independent(self) -> bool: + """ + Is the parameter independent? + + :return: True = independent, False = dependent + """ + return self._independent + @property def bounds(self) -> Tuple[numbers.Number, numbers.Number]: """ @@ -510,25 +537,6 @@ def builtin_constraints(self) -> Dict[str, SelfConstraint]: """ return MappingProxyType(self._constraints.builtin) - @property - def independent(self) -> bool: - """ - Logical property to see if the objects value can be directly set. - - :return: Can the objects value be set - """ - return self._independent - - @independent.setter - @property_stack_deco - def independent(self, value: bool) -> None: - """ - Enable and disable the direct setting of an objects value field. - - :param value: True - objects value can be set, False - the opposite - """ - self._independent = value - @property def user_constraints(self) -> Dict[str, ConstraintBase]: """ From b285ef18012e9e5fa72b7b1e9484bd12ea0134ef Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 23 Apr 2025 15:02:39 +0200 Subject: [PATCH 14/58] Add dependency property getters --- src/easyscience/Objects/variable/parameter.py | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index ccc99a4f..1cdf4f44 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -241,6 +241,51 @@ def make_independent(self) -> None: else: raise AttributeError('This parameter is already independent.') + @property + def independent(self) -> bool: + """ + Is the parameter independent? + + :return: True = independent, False = dependent + """ + return self._independent + + @independent.setter + def independent(self, value: bool) -> None: + raise AttributeError('This property is read-only. Use `make_independent` and `make_dependent` to change the state of the parameter.') # noqa: E501 + + @property + def depedency_expression(self) -> str: + """ + Get the dependency expression of this parameter. + + :return: The dependency expression of this parameter. + """ + if not self._independent: + return self._dependency_string + else: + raise AttributeError('This parameter is independent. It has no dependency expression.') + + @depedency_expression.setter + def depedency_expression(self, new_expression: str) -> None: + raise AttributeError('Dependency expression is read-only. Use `make_dependent` to change the dependency expression.') + + @property + def dependency_map(self) -> Dict[str, DescriptorNumber]: + """ + Get the dependency map of this parameter. + + :return: The dependency map of this parameter. + """ + if not self._independent: + return self._dependency_map + else: + raise AttributeError('This parameter is independent. It has no dependency map.') + + @dependency_map.setter + def dependency_map(self, new_map: Dict[str, DescriptorNumber]) -> None: + raise AttributeError('Dependency map is read-only. Use `make_dependent` to change the dependency map.') + @property def value_no_call_back(self) -> numbers.Number: """ @@ -471,14 +516,6 @@ def free(self) -> bool: def free(self, value: bool) -> None: self.fixed = not value - def independent(self) -> bool: - """ - Is the parameter independent? - - :return: True = independent, False = dependent - """ - return self._independent - @property def bounds(self) -> Tuple[numbers.Number, numbers.Number]: """ From e3fad227555322382cdf17b85363940af8c3d61a Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 25 Apr 2025 11:56:09 +0200 Subject: [PATCH 15/58] Remove old constraints and fix failing unit tests. Also fix small bugs in the dependent parameters. --- Examples/fitting/README.rst | 6 - Examples/fitting/plot_constraints.py | 22 - docs/src/fitting/constraints.rst | 75 --- docs/src/index.rst | 1 - src/easyscience/Constraints.py | 498 ------------------ src/easyscience/Objects/ObjectClasses.py | 16 - .../Objects/variable/descriptor_number.py | 3 + src/easyscience/Objects/variable/parameter.py | 140 +---- src/easyscience/fitting/fitter.py | 18 +- .../fitting/minimizers/minimizer_base.py | 21 - .../Fitting/minimizers/test_minimizer_base.py | 5 - .../Fitting/minimizers/test_minimizer_dfo.py | 4 - tests/unit_tests/Fitting/test_fitter.py | 40 -- tests/unit_tests/Objects/test_BaseObj.py | 3 +- tests/unit_tests/Objects/test_Groups.py | 16 - .../Objects/variable/test_parameter.py | 63 +-- .../variable/test_parameter_from_legacy.py | 424 --------------- .../global_object/test_undo_redo.py | 28 +- tests/unit_tests/utils/io_tests/test_core.py | 61 --- tests/unit_tests/utils/io_tests/test_dict.py | 235 +-------- tests/unit_tests/utils/io_tests/test_json.py | 73 --- tests/unit_tests/utils/io_tests/test_xml.py | 34 -- 22 files changed, 31 insertions(+), 1755 deletions(-) delete mode 100644 Examples/fitting/README.rst delete mode 100644 Examples/fitting/plot_constraints.py delete mode 100644 docs/src/fitting/constraints.rst delete mode 100644 src/easyscience/Constraints.py delete mode 100644 tests/unit_tests/Objects/variable/test_parameter_from_legacy.py diff --git a/Examples/fitting/README.rst b/Examples/fitting/README.rst deleted file mode 100644 index e0c24c4d..00000000 --- a/Examples/fitting/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _fitting_examples: - -Fitting Examples ------------------------- - -This section gathers examples which correspond to fitting data. diff --git a/Examples/fitting/plot_constraints.py b/Examples/fitting/plot_constraints.py deleted file mode 100644 index b150bc82..00000000 --- a/Examples/fitting/plot_constraints.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Constraints example -=================== -This example shows the usages of the different constraints. -""" - -from easyscience import Constraints -from easyscience.Objects.ObjectClasses import Parameter - -p1 = Parameter('p1', 1) -constraint = Constraints.NumericConstraint(p1, '<', 5) -p1.user_constraints['c1'] = constraint - -for value in range(4, 7): - p1.value = value - print(f'Set Value: {value}, Parameter Value: {p1}') - -# %% -# To include embedded rST, use a line of >= 20 ``#``'s or ``#%%`` between your -# rST and your code. This separates your example -# into distinct text and code blocks. You can continue writing code below the -# embedded rST text block: diff --git a/docs/src/fitting/constraints.rst b/docs/src/fitting/constraints.rst deleted file mode 100644 index d92c87c2..00000000 --- a/docs/src/fitting/constraints.rst +++ /dev/null @@ -1,75 +0,0 @@ -====================== -Constraints -====================== - -Constraints are a fundamental component in non-trivial fitting operations. They can also be used to affirm the minimum/maximum of a parameter or tie parameters together in a model. - -Anatomy of a constraint ------------------------ - -A constraint is a rule which is applied to a **dependent** variable. This rule can consist of a logical operation, relation to one or more **independent** variables or an arbitrary function. - - -Constraints on Parameters -^^^^^^^^^^^^^^^^^^^^^^^^^ - -:class:`easyscience.Objects.Base.Parameter` has the properties `builtin_constraints` and `user_constraints`. These are dictionaries which correspond to constraints which are intrinsic and extrinsic to the Parameter. This means that on the value change of the Parameter firstly the `builtin_constraints` are evaluated, followed by the `user_constraints`. - - -Constraints on Fitting -^^^^^^^^^^^^^^^^^^^^^^ - -:class:`easyscience.fitting.Fitter` has the ability to evaluate user supplied constraints which effect the value of both fixed and non-fixed parameters. A good example of one such use case would be the ratio between two parameters, where you would create a :class:`easyscience.fitting.Constraints.ObjConstraint`. - -Using constraints ------------------ - -A constraint can be used in one of three ways; Assignment to a parameter, assignment to fitting or on demand. The first two are covered and on demand is shown below. - -.. code-block:: python - - from easyscience.fitting.Constraints import NumericConstraint - from easyscience.Objects.Base import Parameter - # Create an `a < 1` constraint - a = Parameter('a', 0.5) - constraint = NumericConstraint(a, '<=', 1) - # Evaluate the constraint on demand - a.value = 5.0 - constraint() - # A will now equal 1 - -Constraint Reference --------------------- - -.. minigallery:: easyscience.fitting.Constraints.NumericConstraint - :add-heading: Examples using `Constraints` - -Built-in constraints -^^^^^^^^^^^^^^^^^^^^ - -These are the built in constraints which you can use - -.. autoclass:: easyscience.fitting.Constraints.SelfConstraint - :members: +enabled - -.. autoclass:: easyscience.fitting.Constraints.NumericConstraint - :members: +enabled - -.. autoclass:: easyscience.fitting.Constraints.ObjConstraint - :members: +enabled - -.. autoclass:: easyscience.fitting.Constraints.FunctionalConstraint - :members: +enabled - -.. autoclass:: easyscience.fitting.Constraints.MultiObjConstraint - :members: +enabled - -User created constraints -^^^^^^^^^^^^^^^^^^^^^^^^ - -You can also make your own constraints by subclassing the :class:`easyscience.fitting.Constraints.ConstraintBase` class. For this at a minimum the abstract methods ``_parse_operator`` and ``__repr__`` need to be written. - -.. autoclass:: easyscience.fitting.Constraints.ConstraintBase - :members: - :private-members: - :special-members: __repr__ \ No newline at end of file diff --git a/docs/src/index.rst b/docs/src/index.rst index 3683a186..ca99dda5 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -56,7 +56,6 @@ Documentation :maxdepth: 3 fitting/introduction - fitting/constraints .. toctree:: :maxdepth: 2 diff --git a/src/easyscience/Constraints.py b/src/easyscience/Constraints.py deleted file mode 100644 index c60c5015..00000000 --- a/src/easyscience/Constraints.py +++ /dev/null @@ -1,498 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project bool: - """ - Is the current constraint enabled. - - :return: Logical answer to if the constraint is enabled. - """ - return self._enabled - - @enabled.setter - def enabled(self, enabled_value: bool): - """ - Set the enabled state of the constraint. If the new value is the same as the current value only the state is - changed. - - ... note:: If the new value is ``True`` the constraint is also applied after enabling. - - :param enabled_value: New state of the constraint. - :return: None - """ - - if self._enabled == enabled_value: - return - elif enabled_value: - self.get_obj(self.dependent_obj_ids).independent = False - self() - else: - self.get_obj(self.dependent_obj_ids).independent = True - self._enabled = enabled_value - - def __call__(self, *args, no_set: bool = False, **kwargs): - """ - Method which applies the constraint - - :return: None if `no_set` is False, float otherwise. - """ - if not self.enabled: - if no_set: - return None - return - independent_objs = None - if isinstance(self.dependent_obj_ids, str): - dependent_obj = self.get_obj(self.dependent_obj_ids) - else: - raise AttributeError - if isinstance(self.independent_obj_ids, str): - independent_objs = self.get_obj(self.independent_obj_ids) - elif isinstance(self.independent_obj_ids, list): - independent_objs = [self.get_obj(obj_id) for obj_id in self.independent_obj_ids] - if independent_objs is not None: - value = self._parse_operator(independent_objs, *args, **kwargs) - else: - value = self._parse_operator(dependent_obj, *args, **kwargs) - - if not no_set: - toggle = False - if not dependent_obj.independent: - dependent_obj.independent = True - toggle = True - dependent_obj.value = value - if toggle: - dependent_obj.independent = False - return value - - @abstractmethod - def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - """ - Abstract method which contains the constraint logic - - :param obj: The object/objects which the constraint will use - :return: A numeric result of the constraint logic - """ - - @abstractmethod - def __repr__(self): - pass - - def get_obj(self, key: int) -> V: - """ - Get an EasyScience object from its unique key - - :param key: an EasyScience objects unique key - :return: EasyScience object - """ - return self._global_object.map.get_item_by_key(key) - - -C = TypeVar('C', bound=ConstraintBase) - - -class NumericConstraint(ConstraintBase): - """ - A `NumericConstraint` is a constraint whereby a dependent parameters value is something of an independent parameters - value. I.e. a < 1, a > 5 - """ - - def __init__(self, dependent_obj: V, operator: str, value: Number): - """ - A `NumericConstraint` is a constraint whereby a dependent parameters value is something of an independent - parameters value. I.e. a < 1, a > 5 - - :param dependent_obj: Dependent Parameter - :param operator: Relation to between the parameter and the values. e.g. ``=``, ``<``, ``>`` - :param value: What the parameters value should be compared against. - - :example: - - .. code-block:: python - - from easyscience.fitting.Constraints import NumericConstraint - from easyscience.Objects.Base import Parameter - # Create an `a < 1` constraint - a = Parameter('a', 0.2) - constraint = NumericConstraint(a, '<=', 1) - a.user_constraints['LEQ_1'] = constraint - # This works - a.value = 0.85 - # This triggers the constraint - a.value = 2.0 - # `a` is set to the maximum of the constraint (`a = 1`) - """ - super(NumericConstraint, self).__init__(dependent_obj, operator=operator, value=value) - - def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - ## TODO Probably needs to be updated when DescriptorArray is implemented - - value = obj.value_no_call_back - - if isinstance(value, list): - value = np.array(value) - self.aeval.symtable['value1'] = value - self.aeval.symtable['value2'] = self.value - try: - self.aeval.eval(f'value3 = value1 {self.operator} value2') - logic = self.aeval.symtable['value3'] - if isinstance(logic, np.ndarray): - value[not logic] = self.aeval.symtable['value2'] - else: - if not logic: - value = self.aeval.symtable['value2'] - except Exception as e: - raise e - finally: - self.aeval = Interpreter() - return value - - def __repr__(self) -> str: - return f'{self.__class__.__name__} with `value` {self.operator} {self.value}' - - -class SelfConstraint(ConstraintBase): - """ - A `SelfConstraint` is a constraint which tests a logical constraint on a property of itself, similar to a - `NumericConstraint`. i.e. a > a.min. These constraints are usually used in the internal EasyScience logic. - """ - - def __init__(self, dependent_obj: V, operator: str, value: str): - """ - A `SelfConstraint` is a constraint which tests a logical constraint on a property of itself, similar to - a `NumericConstraint`. i.e. a > a.min. - - :param dependent_obj: Dependent Parameter - :param operator: Relation to between the parameter and the values. e.g. ``=``, ``<``, ``>`` - :param value: Name of attribute to be compared against - - :example: - - .. code-block:: python - - from easyscience.fitting.Constraints import SelfConstraint - from easyscience.Objects.Base import Parameter - # Create an `a < a.max` constraint - a = Parameter('a', 0.2, max=1) - constraint = SelfConstraint(a, '<=', 'max') - a.user_constraints['MAX'] = constraint - # This works - a.value = 0.85 - # This triggers the constraint - a.value = 2.0 - # `a` is set to the maximum of the constraint (`a = 1`) - """ - super(SelfConstraint, self).__init__(dependent_obj, operator=operator, value=value) - - def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - value = obj.value_no_call_back - - self.aeval.symtable['value1'] = value - self.aeval.symtable['value2'] = getattr(obj, self.value) - try: - self.aeval.eval(f'value3 = value1 {self.operator} value2') - logic = self.aeval.symtable['value3'] - if isinstance(logic, np.ndarray): - value[not logic] = self.aeval.symtable['value2'] - else: - if not logic: - value = self.aeval.symtable['value2'] - except Exception as e: - raise e - finally: - self.aeval = Interpreter() - return value - - def __repr__(self) -> str: - return f'{self.__class__.__name__} with `value` {self.operator} obj.{self.value}' - - -class ObjConstraint(ConstraintBase): - """ - A `ObjConstraint` is a constraint whereby a dependent parameter is something of an independent parameter - value. E.g. a (Dependent Parameter) = 2* b (Independent Parameter) - """ - - def __init__(self, dependent_obj: V, operator: str, independent_obj: V): - """ - A `ObjConstraint` is a constraint whereby a dependent parameter is something of an independent parameter - value. E.g. a (Dependent Parameter) < b (Independent Parameter) - - :param dependent_obj: Dependent Parameter - :param operator: Relation to between the independent parameter and dependent parameter. e.g. ``2 *``, ``1 +`` - :param independent_obj: Independent Parameter - - :example: - - .. code-block:: python - - from easyscience.fitting.Constraints import ObjConstraint - from easyscience.Objects.Base import Parameter - # Create an `a = 2 * b` constraint - a = Parameter('a', 0.2) - b = Parameter('b', 1) - - constraint = ObjConstraint(a, '2*', b) - b.user_constraints['SET_A'] = constraint - b.value = 1 - # This triggers the constraint - a.value # Should equal 2 - - """ - super(ObjConstraint, self).__init__(dependent_obj, independent_obj=independent_obj, operator=operator) - self.external = True - - def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - value = obj.value_no_call_back - - self.aeval.symtable['value1'] = value - try: - self.aeval.eval(f'value2 = {self.operator} value1') - value = self.aeval.symtable['value2'] - except Exception as e: - raise e - finally: - self.aeval = Interpreter() - return value - - def __repr__(self) -> str: - return f'{self.__class__.__name__} with `dependent_obj` = {self.operator} `independent_obj`' - - -class MultiObjConstraint(ConstraintBase): - """ - A `MultiObjConstraint` is similar to :class:`EasyScience.fitting.Constraints.ObjConstraint` except that it relates to - multiple independent objects. - """ - - def __init__( - self, - independent_objs: List[V], - operator: List[str], - dependent_obj: V, - value: Number, - ): - """ - A `MultiObjConstraint` is similar to :class:`EasyScience.fitting.Constraints.ObjConstraint` except that it relates - to one or more independent objects. - - E.g. - * a (Dependent Parameter) + b (Independent Parameter) = 1 - * a (Dependent Parameter) + b (Independent Parameter) - 2*c (Independent Parameter) = 0 - - :param independent_objs: List of Independent Parameters - :param operator: List of operators operating on the Independent Parameters - :param dependent_obj: Dependent Parameter - :param value: Value of the expression - - :example: - - **a + b = 1** - - .. code-block:: python - - from easyscience.fitting.Constraints import MultiObjConstraint - from easyscience.Objects.Base import Parameter - # Create an `a + b = 1` constraint - a = Parameter('a', 0.2) - b = Parameter('b', 0.3) - - constraint = MultiObjConstraint([b], ['+'], a, 1) - b.user_constraints['SET_A'] = constraint - b.value = 0.4 - # This triggers the constraint - a.value # Should equal 0.6 - - **a + b - 2c = 0** - - .. code-block:: python - - from easyscience.fitting.Constraints import MultiObjConstraint - from easyscience.Objects.Base import Parameter - # Create an `a + b - 2c = 0` constraint - a = Parameter('a', 0.5) - b = Parameter('b', 0.3) - c = Parameter('c', 0.1) - - constraint = MultiObjConstraint([b, c], ['+', '-2*'], a, 0) - b.user_constraints['SET_A'] = constraint - c.user_constraints['SET_A'] = constraint - b.value = 0.4 - # This triggers the constraint. Or it could be triggered by changing the value of c - a.value # Should equal 0.2 - - .. note:: This constraint is evaluated as ``dependent`` = ``value`` - SUM(``operator_i`` ``independent_i``) - """ - super(MultiObjConstraint, self).__init__( - dependent_obj, - independent_obj=independent_objs, - operator=operator, - value=value, - ) - self.external = True - - def _parse_operator(self, independent_objs: List[V], *args, **kwargs) -> Number: - - in_str = '' - value = None - for idx, obj in enumerate(independent_objs): - self.aeval.symtable['p' + str(self.independent_obj_ids[idx])] = obj.value_no_call_back - - in_str += ' p' + str(self.independent_obj_ids[idx]) - if idx < len(self.operator): - in_str += ' ' + self.operator[idx] - try: - self.aeval.eval(f'final_value = {self.value} - ({in_str})') - value = self.aeval.symtable['final_value'] - except Exception as e: - raise e - finally: - self.aeval = Interpreter() - return value - - def __repr__(self) -> str: - return f'{self.__class__.__name__}' - - -class FunctionalConstraint(ConstraintBase): - """ - Functional constraints do not depend on other parameters and as such can be more complex. - """ - - def __init__( - self, - dependent_obj: V, - func: Callable, - independent_objs: Optional[List[V]] = None, - ): - """ - Functional constraints do not depend on other parameters and as such can be more complex. - - :param dependent_obj: Dependent Parameter - :param func: Function to be evaluated in the form ``f(value, *args, **kwargs)`` - - :example: - - .. code-block:: python - - import numpy as np - from easyscience.fitting.Constraints import FunctionalConstraint - from easyscience.Objects.Base import Parameter - - a = Parameter('a', 0.2, max=1) - constraint = FunctionalConstraint(a, np.abs) - - a.user_constraints['abs'] = constraint - - # This triggers the constraint - a.value = 0.85 # `a` is set to 0.85 - # This triggers the constraint - a.value = -0.5 # `a` is set to 0.5 - """ - super(FunctionalConstraint, self).__init__(dependent_obj, independent_obj=independent_objs) - self.function = func - if independent_objs is not None: - self.external = True - - def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - - self.aeval.symtable[f'f{id(self.function)}'] = self.function - value_str = f'r_value = f{id(self.function)}(' - if isinstance(obj, list): - for o in obj: - value_str += f'{o.value_no_call_back},' - - value_str = value_str[:-1] - else: - value_str += f'{obj.value_no_call_back}' - - value_str += ')' - try: - self.aeval.eval(value_str) - value = self.aeval.symtable['r_value'] - except Exception as e: - raise e - finally: - self.aeval = Interpreter() - return value - - def __repr__(self) -> str: - return f'{self.__class__.__name__}' - - -def cleanup_constraint(obj_id: str, independent: bool): - try: - obj = global_object.map.get_item_by_key(obj_id) - obj.independent = independent - except ValueError: - if global_object.debug: - print(f'Object with ID {obj_id} has already been deleted') diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/Objects/ObjectClasses.py index 020d9fc2..376162c5 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/Objects/ObjectClasses.py @@ -1,16 +1,11 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project List[C]: - pars = self.get_parameters() - constraints = [] - for par in pars: - con: Dict[str, C] = par.user_constraints - for key in con.keys(): - constraints.append(con[key]) - return constraints - def get_parameters(self) -> List[Parameter]: """ Get all parameter objects as a list. diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index f98e9c09..b80533d5 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -465,6 +465,9 @@ def __abs__(self) -> DescriptorNumber: return descriptor_number def _base_unit(self) -> str: + """ + Extract the base unit from the unit string by removing numeric components and scientific notation. + """ string = str(self._scalar.unit) for i, letter in enumerate(string): if letter == 'e': diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 1cdf4f44..d3b87663 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -9,13 +9,10 @@ import re import warnings import weakref -from collections import namedtuple -from types import MappingProxyType from typing import Any from typing import Dict from typing import List from typing import Optional -from typing import Tuple from typing import Union import numpy as np @@ -25,16 +22,11 @@ from scipp import Variable from easyscience import global_object -from easyscience.Constraints import ConstraintBase -from easyscience.Constraints import SelfConstraint from easyscience.global_object.undo_redo import property_stack_deco -from easyscience.Utils.Exceptions import CoreSetException from .descriptor_number import DescriptorNumber from .descriptor_number import notify_observers -Constraints = namedtuple('Constraints', ['user', 'builtin', 'virtual']) - class Parameter(DescriptorNumber): """ @@ -119,12 +111,6 @@ def __init__( # Create additional fitting elements self._initial_scalar = copy.deepcopy(self._scalar) - builtin_constraint = { - # Last argument in constructor is the name of the property holding the value of the constraint - 'min': SelfConstraint(self, '>=', 'min'), - 'max': SelfConstraint(self, '<=', 'max'), - } - self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) @classmethod def from_dependency(cls, name: str, dependency_expression: str, dependency_map: Optional[dict] = None, **kwargs) -> Parameter: # noqa: E501 @@ -152,6 +138,8 @@ def _update(self, update_id: int, updating_object: str) -> None: """ if not self._independent: # Check if this parameter has already been updated by the updating object with this update id + if updating_object not in self._dependency_updates: + self._dependency_updates[updating_object] = 0 if self._dependency_updates[updating_object] == update_id: warnings.warn('Warning: Cyclic dependency detected!\n' + f'This parameter, {self.unique_name}, has already been updated by {updating_object} during this update.\n' + # noqa: E501 @@ -179,7 +167,7 @@ def make_dependent(self, dependency_expression: str, dependency_map: Optional[di raise TypeError('`dependency_expression` must be a string representing a valid dependency expression.') if not (isinstance(dependency_map, dict) or dependency_map is None): raise TypeError('`dependency_map` must be a dictionary of dependencies and their corresponding names in the dependecy expression.') # noqa: E501 - for key, value in self._dependency_map.items(): + for key, value in dependency_map.items(): if not isinstance(key, str): raise TypeError('`dependency_map` keys must be strings representing the names of the dependencies in the dependency expression.') # noqa: E501 if not isinstance(value, DescriptorNumber): @@ -216,8 +204,8 @@ def make_dependent(self, dependency_expression: str, dependency_map: Optional[di self._scalar.value = dependency_result.value self._scalar.unit = dependency_result.unit self._scalar.variance = dependency_result.variance - self._min = dependency_result.min if isinstance(dependency_result, Parameter) else dependency_result.value - self._max = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value + self._min.value = dependency_result.min if isinstance(dependency_result, Parameter) else dependency_result.value + self._max.value = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value self._independent = False self._fixed = False self._notify_observers() @@ -337,30 +325,17 @@ def value(self, value: numbers.Number) -> None: :param value: New value of self """ if self._independent: - if not isinstance(value, numbers.Number) or isinstance(value, bool): + if not isinstance(value, numbers.Number): raise TypeError(f'{value=} must be a number') + + value = float(value) + if value < self._min.value: + value = self._min.value + if value > self._max.value: + value = self._max.value - # Need to set the value for constraints to be functional - self._scalar.value = float(value) - # if self._callback.fset is not None: - # self._callback.fset(self._scalar.value) - - # Deals with min/max - value = self._constraint_runner(self.builtin_constraints, self._scalar.value) - - # Deals with user constraints - # Changes should not be registrered in the undo/redo stack - stack_state = global_object.stack.enabled - if stack_state: - global_object.stack.force_state(False) - try: - value = self._constraint_runner(self.user_constraints, value) - finally: - global_object.stack.force_state(stack_state) - - value = self._constraint_runner(self._constraints.virtual, value) + self._scalar.value = value - self._scalar.value = float(value) if self._callback.fset is not None: self._callback.fset(self._scalar.value) @@ -516,95 +491,6 @@ def free(self) -> bool: def free(self, value: bool) -> None: self.fixed = not value - @property - def bounds(self) -> Tuple[numbers.Number, numbers.Number]: - """ - Get the bounds of the parameter. - - :return: Tuple of the parameters minimum and maximum values - """ - return self.min, self.max - - @bounds.setter - def bounds(self, new_bound: Tuple[numbers.Number, numbers.Number]) -> None: - """ - Set the bounds of the parameter. *This will also enable the parameter*. - - :param new_bound: New bounds. This should be a tuple of (min, max). - """ - old_min = self.min - old_max = self.max - new_min, new_max = new_bound - - # Begin macro operation for undo/redo - close_macro = False - if self._global_object.stack.enabled: - self._global_object.stack.beginMacro('Setting bounds') - close_macro = True - - try: - # Update bounds - self.min = new_min - self.max = new_max - except ValueError: - # Rollback on failure - self.min = old_min - self.max = old_max - if close_macro: - self._global_object.stack.endMacro() - raise ValueError(f'Current parameter value: {self._scalar.value} must be within {new_bound=}') - - # Enable the parameter if needed - if not self.independent: - self.independent = True - # Free parameter if needed - if self.fixed: - self.fixed = False - - # End macro operation - if close_macro: - self._global_object.stack.endMacro() - - @property - def builtin_constraints(self) -> Dict[str, SelfConstraint]: - """ - Get the built in constrains of the object. Typically these are the min/max - - :return: Dictionary of constraints which are built into the system - """ - return MappingProxyType(self._constraints.builtin) - - @property - def user_constraints(self) -> Dict[str, ConstraintBase]: - """ - Get the user specified constrains of the object. - - :return: Dictionary of constraints which are user supplied - """ - return self._constraints.user - - @user_constraints.setter - def user_constraints(self, constraints_dict: Dict[str, ConstraintBase]) -> None: - self._constraints.user = constraints_dict - - def _constraint_runner( - self, - this_constraint_type, - value: numbers.Number, - ) -> float: - for constraint in this_constraint_type.values(): - if constraint.external: - constraint() - continue - - constained_value = constraint(no_set=True) - if constained_value != value: - if global_object.debug: - print(f'Constraint `{constraint}` has been applied') - self._scalar.value = constained_value - value = constained_value - return value - def _process_dependency_unique_names(self, dependency_expression: str): """ Add the unique names of the parameters to the ASTEval interpreter. This is used to evaluate the dependency expression. diff --git a/src/easyscience/fitting/fitter.py b/src/easyscience/fitting/fitter.py index daea7782..53007879 100644 --- a/src/easyscience/fitting/fitter.py +++ b/src/easyscience/fitting/fitter.py @@ -34,15 +34,6 @@ def __init__(self, fit_object, fit_function: Callable): self._enum_current_minimizer: AvailableMinimizers = None # set in _update_minimizer self._update_minimizer(DEFAULT_MINIMIZER) - def fit_constraints(self) -> list: - return self._minimizer.fit_constraints() - - def add_fit_constraint(self, constraint) -> None: - self._minimizer.add_fit_constraint(constraint) - - def remove_fit_constraint(self, index: int) -> None: - self._minimizer.remove_fit_constraint(index) - def make_model(self, pars=None) -> Callable: return self._minimizer.make_model(pars) @@ -84,9 +75,7 @@ def switch_minimizer(self, minimizer_enum: Union[AvailableMinimizers, str]) -> N print(f'minimizer should be set with enum {minimizer_enum}') minimizer_enum = from_string_to_enum(minimizer_enum) - constraints = self._minimizer.fit_constraints() self._update_minimizer(minimizer_enum) - self._minimizer.set_fit_constraint(constraints) def _update_minimizer(self, minimizer_enum: AvailableMinimizers) -> None: self._minimizer = factory(minimizer_enum=minimizer_enum, fit_object=self._fit_object, fit_function=self.fit_function) @@ -235,11 +224,7 @@ def inner_fit_callable( # Fit fit_fun_org = self._fit_function fit_fun_wrap = self._fit_function_wrapper(x_new, flatten=True) # This should be wrapped. - - # We change the fit function, so have to reset constraints - constraints = self._minimizer.fit_constraints() self.fit_function = fit_fun_wrap - self._minimizer.set_fit_constraint(constraints) f_res = self._minimizer.fit( x_fit, y_new, @@ -251,9 +236,8 @@ def inner_fit_callable( # Postcompute fit_result = self._post_compute_reshaping(f_res, x, y) - # Reset the function and constrains + # Reset the function self.fit_function = fit_fun_org - self._minimizer.set_fit_constraint(constraints) return fit_result return inner_fit_callable diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 02130a6e..511057f5 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -16,8 +16,6 @@ import numpy as np -from easyscience.Constraints import ObjConstraint - # causes circular import when Parameter is imported # from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.variable import Parameter @@ -52,11 +50,6 @@ def __init__( self._cached_pars_vals: Dict[str, Tuple[float]] = {} self._cached_model = None self._fit_function = None - self._constraints = [] - - @property - def all_constraints(self) -> List[ObjConstraint]: - return [*self._constraints, *self._object._constraints] @property def enum(self) -> AvailableMinimizers: @@ -66,18 +59,6 @@ def enum(self) -> AvailableMinimizers: def name(self) -> str: return self._minimizer_enum.name - def fit_constraints(self) -> List[ObjConstraint]: - return self._constraints - - def set_fit_constraint(self, constraints: List[ObjConstraint]): - self._constraints = constraints - - def add_fit_constraint(self, constraint: ObjConstraint): - self._constraints.append(constraint) - - def remove_fit_constraint(self, index: int) -> None: - del self._constraints[index] - @abstractmethod def fit( self, @@ -237,8 +218,6 @@ def _fit_function(x: np.ndarray, **kwargs): # Since we are calling the parameter fset will be called. # TODO Pre processing here - for constraint in self.fit_constraints(): - constraint() return_data = func(x) # TODO Loading or manipulating data here return return_data diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py index 281b97e6..77158cac 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py @@ -46,7 +46,6 @@ def test_init(self, minimizer: MinimizerBase): assert minimizer._cached_pars_vals == {} assert minimizer._cached_model == None assert minimizer._fit_function == None - assert minimizer._constraints == [] def test_enum(self, minimizer: MinimizerBase): assert minimizer.enum == self._mock_minimizer_enum @@ -128,9 +127,6 @@ def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: # When minimizer._original_fit_function = MagicMock(return_value='fit_function_result') - mock_fit_constraint = MagicMock() - minimizer.fit_constraints = MagicMock(return_value=[mock_fit_constraint]) - minimizer._object = MagicMock() mock_parm_1 = MagicMock(Parameter) mock_parm_1.unique_name = 'mock_parm_1' @@ -148,7 +144,6 @@ def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: # Expect assert 'fit_function_result' == fit_function_result - mock_fit_constraint.assert_called_once_with() minimizer._original_fit_function.assert_called_once_with([10.0]) assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py index 8c39b8a5..1cd14cd5 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py @@ -72,9 +72,6 @@ def test_generate_fit_function(self, minimizer: DFO) -> None: # When minimizer._original_fit_function = MagicMock(return_value='fit_function_result') - mock_fit_constraint = MagicMock() - minimizer.fit_constraints = MagicMock(return_value=[mock_fit_constraint]) - minimizer._object = MagicMock() mock_parm_1 = MagicMock() mock_parm_1.unique_name = 'mock_parm_1' @@ -92,7 +89,6 @@ def test_generate_fit_function(self, minimizer: DFO) -> None: # Expect assert 'fit_function_result' == fit_function_result - mock_fit_constraint.assert_called_once_with() minimizer._original_fit_function.assert_called_once_with([10.0]) assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py index 63783c17..992225ce 100644 --- a/tests/unit_tests/Fitting/test_fitter.py +++ b/tests/unit_tests/Fitting/test_fitter.py @@ -24,42 +24,6 @@ def test_constructor(self, fitter: Fitter): assert fitter._minimizer is None fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) - def test_fit_constraints(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.fit_constraints = MagicMock(return_value='constraints') - fitter._minimizer = mock_minimizer - - # Then - constraints = fitter.fit_constraints() - - # Expect - assert constraints == 'constraints' - - def test_add_fit_constraint(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.add_fit_constraint = MagicMock() - fitter._minimizer = mock_minimizer - - # Then - fitter.add_fit_constraint('constraints') - - # Expect - mock_minimizer.add_fit_constraint.assert_called_once_with('constraints') - - def test_remove_fit_constraint(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.remove_fit_constraint = MagicMock() - fitter._minimizer = mock_minimizer - - # Then - fitter.remove_fit_constraint(10) - - # Expect - mock_minimizer.remove_fit_constraint.assert_called_once_with(10) - def test_make_model(self, fitter: Fitter): # When mock_minimizer = MagicMock() @@ -128,8 +92,6 @@ def test_create(self, fitter: Fitter, monkeypatch): def test_switch_minimizer(self, fitter: Fitter, monkeypatch): # When mock_minimizer = MagicMock() - mock_minimizer.fit_constraints = MagicMock(return_value='constraints') - mock_minimizer.set_fit_constraint = MagicMock() fitter._minimizer = mock_minimizer mock_string_to_enum = MagicMock(return_value=10) monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) @@ -139,8 +101,6 @@ def test_switch_minimizer(self, fitter: Fitter, monkeypatch): # Expect fitter._update_minimizer.count(2) - mock_minimizer.set_fit_constraint.assert_called_once_with('constraints') - mock_minimizer.fit_constraints.assert_called_once() mock_string_to_enum.assert_called_once_with('great-minimizer') def test_update_minimizer(self, monkeypatch): diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/Objects/test_BaseObj.py index ddccd631..67b91e82 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/Objects/test_BaseObj.py @@ -148,7 +148,7 @@ def test_baseobj_fit_objects(setup_pars: dict): pass -def test_baseobj_as_dict(setup_pars: dict): +def test_baseobj_as_dict(clear, setup_pars: dict): name = setup_pars["name"] del setup_pars["name"] obj = BaseObj(name, **setup_pars) @@ -266,7 +266,6 @@ def test_baseobj_dir(setup_pars): "encode", "decode", "as_dict", - "constraints", "des1", "des2", "from_dict", diff --git a/tests/unit_tests/Objects/test_Groups.py b/tests/unit_tests/Objects/test_Groups.py index 3850b5f3..b546373f 100644 --- a/tests/unit_tests/Objects/test_Groups.py +++ b/tests/unit_tests/Objects/test_Groups.py @@ -394,22 +394,6 @@ def test_baseCollection_from_dict(cls): assert item1.value == item2.value -@pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_constraints(cls): - name = "test" - p1 = Parameter("p1", 1) - p2 = Parameter("p2", 2) - - from easyscience.Constraints import ObjConstraint - - p2.user_constraints["testing"] = ObjConstraint(p2, "2*", p1) - - obj = cls(name, p1, p2) - - cons: List[ObjConstraint] = obj.constraints - assert len(cons) == 1 - - @pytest.mark.parametrize("cls", class_constructors) def test_baseCollection_repr(cls): name = "test" diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py index 1eac02a6..476827ce 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -24,7 +24,6 @@ def parameter(self) -> Parameter: url="url", display_name="display_name", callback=self.mock_callback, - independent="independent", parent=None, ) return parameter @@ -40,7 +39,7 @@ def test_init(self, parameter: Parameter): assert parameter._max.value == 10 assert parameter._max.unit == "m" assert parameter._callback == self.mock_callback - assert parameter._independent == "independent" + assert parameter._independent == True # From super assert parameter._scalar.value == 1 @@ -69,7 +68,6 @@ def test_init_value_min_exception(self): url="url", display_name="display_name", callback=mock_callback, - independent="independent", parent=None, ) @@ -91,7 +89,6 @@ def test_init_value_max_exception(self): url="url", display_name="display_name", callback=mock_callback, - independent="independent", parent=None, ) @@ -185,56 +182,6 @@ def test_repr_fixed(self, parameter: Parameter): # Then Expect assert repr(parameter) == "" - def test_bounds(self, parameter: Parameter): - # When Then Expect - assert parameter.bounds == (0, 10) - - def test_set_bounds(self, parameter: Parameter): - # When - parameter._independent = False - self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value - parameter._enabled = False - parameter._fixed = True - - # Then - parameter.bounds = (-10, 5) - - # Expect - assert parameter.min == -10 - assert parameter.max == 5 - assert parameter._independent == True - assert parameter._fixed == False - - def test_set_bounds_exception_min(self, parameter: Parameter): - # When - parameter._independent = False - parameter._fixed = True - - # Then - with pytest.raises(ValueError): - parameter.bounds = (2, 10) - - # Expect - assert parameter.min == 0 - assert parameter.max == 10 - assert parameter._independent == False - assert parameter._fixed == True - - def test_set_bounds_exception_max(self, parameter: Parameter): - # When - parameter._independent = False - parameter._fixed = True - - # Then - with pytest.raises(ValueError): - parameter.bounds = (0, 0.1) - - # Expect - assert parameter.min == 0 - assert parameter.max == 10 - assert parameter._independent == False - assert parameter._fixed == True - def test_independent(self, parameter: Parameter): # When parameter._independent = True @@ -242,13 +189,6 @@ def test_independent(self, parameter: Parameter): # Then Expect assert parameter.independent is True - def test_set_independent(self, parameter: Parameter): - # When - parameter.independent = False - - # Then Expect - assert parameter._independent is False - def test_value_match_callback(self, parameter: Parameter): # When self.mock_callback.fget.return_value = 1.0 @@ -337,7 +277,6 @@ def test_as_data_dict(self, clear, parameter: Parameter): "description": "description", "url": "url", "display_name": "display_name", - "independent": "independent", "unique_name": "Parameter_0", } diff --git a/tests/unit_tests/Objects/variable/test_parameter_from_legacy.py b/tests/unit_tests/Objects/variable/test_parameter_from_legacy.py deleted file mode 100644 index f4dcd2ea..00000000 --- a/tests/unit_tests/Objects/variable/test_parameter_from_legacy.py +++ /dev/null @@ -1,424 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project " - d = Parameter("test", 1, unit="cm") - assert repr(d) == f"<{d.__class__.__name__} 'test': 1.0000 cm, bounds=[-inf:inf]>" - d = Parameter("test", 1, variance=0.1) - assert repr(d) == f"<{d.__class__.__name__} 'test': 1.0000 ± 0.3162, bounds=[-inf:inf]>" - - d = Parameter("test", 1, fixed=True) - assert ( - repr(d) - == f"<{d.__class__.__name__} 'test': 1.0000 (fixed), bounds=[-inf:inf]>" - ) - d = Parameter("test", 1, unit="cm", variance=0.1, fixed=True) - assert ( - repr(d) - == f"<{d.__class__.__name__} 'test': 1.0000 ± 0.3162 cm (fixed), bounds=[-inf:inf]>" - ) - - -def test_parameter_as_dict(): - d = Parameter("test", 1) - result = d.as_dict() - expected = { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1.0, - "variance": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "unit": "dimensionless", - } - for key in expected.keys(): - assert result[key] == expected[key] - - # Check that additional arguments work - d = Parameter("test", 1, unit="km", url="https://www.boo.com") - result = d.as_dict() - expected = { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "unit": "km", - "value": 1.0, - "variance": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - } - for key in expected.keys(): - assert result[key] == expected[key] - - -def test_item_from_dict(): - reference = { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "unit": "km", - "value": 1.0, - "variance": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - } - constructor = Parameter - d = constructor.from_dict(reference) - for key, item in reference.items(): - if key == "callback" or key.startswith("@"): - continue - obtained = getattr(d, key) - assert obtained == item - - -@pytest.mark.parametrize( - "construct", - ( - { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "unit": "km", - "value": 1.0, - "variance": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - }, - ), - ids=["Parameter"], -) -def test_item_from_Decoder(construct): - - from easyscience.Utils.io.dict import DictSerializer - - d = DictSerializer().decode(construct) - assert d.__class__.__name__ == construct["@class"] - for key, item in construct.items(): - if key == "callback" or key.startswith("@"): - continue - obtained = getattr(d, key) - assert obtained == item - - -@pytest.mark.parametrize("value", (-np.inf, 0, 1.0, 2147483648, np.inf)) -def test_parameter_min(value): - d = Parameter("test", -0.1) - if d.value < value: - with pytest.raises(ValueError): - d.min = value - else: - d.min = value - assert d.min == value - - -@pytest.mark.parametrize("value", [-np.inf, 0, 1.1, 2147483648, np.inf]) -def test_parameter_max(value): - d = Parameter("test", 2147483649) - if d.value > value: - with pytest.raises(ValueError): - d.max = value - else: - d.max = value - assert d.max == value - - -@pytest.mark.parametrize("value", [True, False, 5]) -def test_parameter_fixed(value): - d = Parameter("test", -np.inf) - if isinstance(value, bool): - d.fixed = value - assert d.fixed == value - else: - with pytest.raises(ValueError): - d.fixed = value - - -@pytest.mark.parametrize("value", (-np.inf, -0.1, 0, 1.0, 2147483648, np.inf)) -def test_parameter_error(value): - d = Parameter("test", 1) - if value >= 0: - d.error = value - assert d.error == value - else: - with pytest.raises(ValueError): - d.error = value - - -def _generate_advanced_inputs(): - temp = _generate_inputs() - # These will be the optional parameters - advanced = {"variance": 1.0, "min": -0.1, "max": 2147483648, "fixed": False} - advanced_result = { - "variance": {"name": "variance", "value": advanced["variance"]}, - "min": {"name": "min", "value": advanced["min"]}, - "max": {"name": "max", "value": advanced["max"]}, - "fixed": {"name": "fixed", "value": advanced["fixed"]}, - } - - def create_entry(base, key, value, ref, ref_key=None): - this_temp = deepcopy(base) - for item in base: - test, res = item - new_opt = deepcopy(test[1]) - new_res = deepcopy(res) - if ref_key is None: - ref_key = key - new_res[ref_key] = ref - new_opt[key] = value - this_temp.append(([test[0], new_opt], new_res)) - return this_temp - - for add_opt in advanced.keys(): - if isinstance(advanced[add_opt], list): - for idx, item in enumerate(advanced[add_opt]): - temp = create_entry( - temp, - add_opt, - item, - advanced_result[add_opt]["value"][idx], - ref_key=advanced_result[add_opt]["name"], - ) - else: - temp = create_entry( - temp, - add_opt, - advanced[add_opt], - advanced_result[add_opt]["value"], - ref_key=advanced_result[add_opt]["name"], - ) - return temp - - -@pytest.mark.parametrize("element, expected", _generate_advanced_inputs()) -def test_parameter_advanced_creation(element, expected): - if len(element[0]) > 0: - value = element[0][1] - else: - value = element[1]["value"] - if "min" in element[1].keys(): - if element[1]["min"] > value: - with pytest.raises(ValueError): - d = Parameter(*element[0], **element[1]) - elif "max" in element[1].keys(): - if element[1]["max"] < value: - with pytest.raises(ValueError): - d = Parameter(*element[0], **element[1]) - else: - d = Parameter(*element[0], **element[1]) - for field in expected.keys(): - ref = expected[field] - obtained = getattr(d, field) - assert obtained == ref - - -@pytest.mark.parametrize("value", ("This is ", "a fun ", "test")) -def test_parameter_display_name(value): - p = Parameter("test", 1, display_name=value) - assert p.display_name == value - - -@pytest.mark.parametrize("value", (True, False)) -def test_parameter_bounds(value): - for fixed in (True, False): - p = Parameter("test", 1, enabled=value, fixed=fixed) - assert p.min == -np.inf - assert p.max == np.inf - assert p.fixed == fixed - assert p.bounds == (-np.inf, np.inf) - - p.bounds = (0, 2) - assert p.min == 0 - assert p.max == 2 - assert p.bounds == (0, 2) - assert p.enabled is True - assert p.fixed is False \ No newline at end of file diff --git a/tests/unit_tests/global_object/test_undo_redo.py b/tests/unit_tests/global_object/test_undo_redo.py index 72a2edc1..53c75934 100644 --- a/tests/unit_tests/global_object/test_undo_redo.py +++ b/tests/unit_tests/global_object/test_undo_redo.py @@ -116,10 +116,8 @@ def test_DescriptorStrUndoRedo(): for option in [ ("value", 500), ("error", 5), - ("independent", False), ("unit", "km/s"), ("display_name", "boom"), - ("enabled", False), ("fixed", False), ("max", 505), ("min", -1), @@ -135,27 +133,23 @@ def test_ParameterUndoRedo(test): e = doUndoRedo(obj, attr, value) assert not e -@pytest.mark.parametrize("value", (True, False)) -def test_Parameter_Bounds_UndoRedo(value): +def test_Parameter_Bounds_UndoRedo(): from easyscience import global_object global_object.stack.enabled = True - p = Parameter("test", 1, independent=value) - assert p.min == -np.inf - assert p.max == np.inf - assert p.bounds == (-np.inf, np.inf) + parameter = Parameter("test", 1) + assert parameter.min == -np.inf + assert parameter.max == np.inf - p.bounds = (0, 2) - assert p.min == 0 - assert p.max == 2 - assert p.bounds == (0, 2) - assert p.independent is True + parameter.min = 0 + parameter.max = 2 + assert parameter.min == 0 + assert parameter.max == 2 global_object.stack.undo() - assert p.min == -np.inf - assert p.max == np.inf - assert p.bounds == (-np.inf, np.inf) - assert p.independent is value + global_object.stack.undo() + assert parameter.min == -np.inf + assert parameter.max == np.inf def test_BaseObjUndoRedo(): diff --git a/tests/unit_tests/utils/io_tests/test_core.py b/tests/unit_tests/utils/io_tests/test_core.py index 3e87d539..2083ac3c 100644 --- a/tests/unit_tests/utils/io_tests/test_core.py +++ b/tests/unit_tests/utils/io_tests/test_core.py @@ -8,7 +8,6 @@ import pytest import easyscience -from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.variable import DescriptorNumber from easyscience.Objects.variable import Parameter @@ -45,7 +44,6 @@ "url": "https://www.boo.com", "description": "", "display_name": "test", - "enabled": True, }, Parameter, ], @@ -123,62 +121,3 @@ def test_variable_as_data_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorN assert len(dif) == 0 check_dict(data_dict, enc_d) - - -class A(BaseObj): - def __init__(self, name: str = "A", **kwargs): - super().__init__(name=name, **kwargs) - - -class B(BaseObj): - def __init__(self, a, b, unique_name): - super(B, self).__init__("B", a=a, unique_name=unique_name) - self.b = b - - -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_as_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = { - "@module": A.__module__, - "@class": A.__name__, - "@version": None, - "name": "A", - dp_kwargs["name"]: dp_kwargs, - } - - obj = A(**a_kw) - - enc = obj.as_dict() - expected_keys = set(full_d.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, enc) - - -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_as_data_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = {"name": "A", dp_kwargs["name"]: data_dict} - - obj = A(**a_kw) - - enc = obj.as_data_dict() - expected_keys = set(full_d.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, enc) diff --git a/tests/unit_tests/utils/io_tests/test_dict.py b/tests/unit_tests/utils/io_tests/test_dict.py index 884f86b6..a9b8ccd4 100644 --- a/tests/unit_tests/utils/io_tests/test_dict.py +++ b/tests/unit_tests/utils/io_tests/test_dict.py @@ -4,17 +4,13 @@ from copy import deepcopy from typing import Type -import numpy as np import pytest -from importlib import metadata from easyscience.Utils.io.dict import DataDictSerializer from easyscience.Utils.io.dict import DictSerializer from easyscience.Objects.variable import DescriptorNumber from easyscience.Objects.ObjectClasses import BaseObj -from .test_core import A -from .test_core import B from .test_core import check_dict from .test_core import dp_param_dict from .test_core import skip_dict @@ -120,144 +116,6 @@ def test_variable_encode_data(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], s check_dict(data_dict, enc_d) -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_DictSerializer_encode( - dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip -): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = { - "@module": A.__module__, - "@class": A.__name__, - "@version": None, - "name": "A", - dp_kwargs["name"]: deepcopy(dp_kwargs), - } - - if not isinstance(skip, list): - skip = [skip] - - full_d = recursive_remove(full_d, skip) - - obj = A(**a_kw) - - enc = obj.encode(skip=skip, encoder=DictSerializer) - expected_keys = set(full_d.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, enc) - - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_DataDictSerializer( - dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip -): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = {"name": "A", dp_kwargs["name"]: data_dict} - - full_d = recursive_remove(full_d, skip) - - obj = A(**a_kw) - - enc = obj.encode(skip=skip, encoder=DataDictSerializer) - expected_keys = set(full_d.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, enc) - - -@pytest.mark.parametrize( - "encoder", [None, DataDictSerializer], ids=["Default", "DataDictSerializer"] -) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_encode_data(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], encoder): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = {"name": "A", dp_kwargs["name"]: data_dict} - - obj = A(**a_kw) - - enc = obj.encode_data(encoder=encoder) - expected_keys = set(full_d.keys()) - obtained_keys = set(enc.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, enc) - - -def test_custom_class_full_encode_with_numpy(): - class B(BaseObj): - def __init__(self, a, b, unique_name): - super(B, self).__init__("B", a=a, unique_name=unique_name) - self.b = b - # Same as in __init__.py for easyscience - try: - version = metadata.version('easyscience') # 'easyscience' is the name of the package in 'setup.py - except metadata.PackageNotFoundError: - version = '0.0.0' - - obj = B(DescriptorNumber("a", 1.0, unique_name="a"), np.array([1.0, 2.0, 3.0]), unique_name="B_0") - full_enc = obj.encode(encoder=DictSerializer, full_encode=True) - expected = { - "@module": "tests.unit_tests.utils.io_tests.test_dict", - "@class": "B", - "@version": None, - "unique_name": "B_0", - "b": { - "@module": "numpy", - "@class": "array", - "dtype": "float64", - "data": [1.0, 2.0, 3.0], - }, - "a": { - "@module": "easyscience.Objects.variable.descriptor_number", - "@class": "DescriptorNumber", - "@version": version, - "description": "", - "unit": "dimensionless", - "display_name": "a", - "name": "a", - "value": 1.0, - "variance": None, - "unique_name": "a", - "url": "", - }, - } - check_dict(full_enc, expected) - - -def test_custom_class_full_decode_with_numpy(): - global_object.map._clear() - obj = B(DescriptorNumber("a", 1.0), np.array([1.0, 2.0, 3.0]), unique_name="B_0") - full_enc = obj.encode(encoder=DictSerializer, full_encode=True) - global_object.map._clear() - obj2 = B.decode(full_enc, decoder=DictSerializer) - assert obj.name == obj2.name - assert obj.unique_name == obj2.unique_name - assert obj.a.value == obj2.a.value - assert np.all(obj.b == obj2.b) - - ######################################################################################################################## # TESTING DECODING ######################################################################################################################## @@ -325,95 +183,4 @@ def test_group_encode2(): b = BaseObj("outer", b=BaseCollection("test", d0, d1)) d = b.as_dict() - assert isinstance(d["b"], dict) - - -#TODO: do we need/want this test? -# -# @pytest.mark.parametrize(**dp_param_dict) -# def test_custom_class_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[Descriptor]): -# -# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} -# -# a_kw = { -# data_dict['name']: dp_cls(**data_dict) -# } -# -# obj = A(**a_kw) -# -# enc = obj.encode(encoder=DictSerializer) -# -# stripped_encode = {k: v for k, v in enc.items() if k[0] != '@'} -# stripped_encode[data_dict['name']] = data_dict -# -# dec = obj.decode(enc, decoder=DictSerializer) -# -# def test_objs(reference_obj, test_obj, in_dict): -# if 'value' in in_dict.keys(): -# in_dict['value'] = in_dict.pop('value') -# if 'units' in in_dict.keys(): -# del in_dict['units'] -# for k in in_dict.keys(): -# if hasattr(reference_obj, k) and hasattr(test_obj, k): -# if isinstance(in_dict[k], dict): -# test_objs(getattr(obj, k), getattr(test_obj, k), in_dict[k]) -# assert getattr(obj, k) == getattr(dec, k) -# else: -# raise AttributeError(f"{k} not found in decoded object") -# test_objs(obj, dec, stripped_encode) -# -# -# @pytest.mark.parametrize(**skip_dict) -# @pytest.mark.parametrize(**dp_param_dict) -# def test_custom_class_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[Descriptor], skip): -# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} -# -# a_kw = { -# data_dict['name']: dp_cls(**data_dict) -# } -# -# full_d = { -# "name": "A", -# dp_kwargs['name']: data_dict -# } -# -# full_d = recursive_remove(full_d, skip) -# -# obj = A(**a_kw) -# -# enc = obj.encode(skip=skip, encoder=DataDictSerializer) -# expected_keys = set(full_d.keys()) -# obtained_keys = set(enc.keys()) -# -# dif = expected_keys.difference(obtained_keys) -# -# assert len(dif) == 0 -# -# check_dict(full_d, enc) -# -# -# @pytest.mark.parametrize('encoder', [None, DataDictSerializer], ids=['Default', 'DataDictSerializer']) -# @pytest.mark.parametrize(**dp_param_dict) -# def test_custom_class_encode_data(dp_kwargs: dict, dp_cls: Type[Descriptor], encoder): -# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != '@'} -# -# a_kw = { -# data_dict['name']: dp_cls(**data_dict) -# } -# -# full_d = { -# "name": "A", -# dp_kwargs['name']: data_dict -# } -# -# obj = A(**a_kw) -# -# enc = obj.encode_data(encoder=encoder) -# expected_keys = set(full_d.keys()) -# obtained_keys = set(enc.keys()) -# -# dif = expected_keys.difference(obtained_keys) -# -# assert len(dif) == 0 -# -# check_dict(full_d, enc) + assert isinstance(d["b"], dict) \ No newline at end of file diff --git a/tests/unit_tests/utils/io_tests/test_json.py b/tests/unit_tests/utils/io_tests/test_json.py index cec6e4c0..54f9ccb9 100644 --- a/tests/unit_tests/utils/io_tests/test_json.py +++ b/tests/unit_tests/utils/io_tests/test_json.py @@ -11,7 +11,6 @@ from easyscience.Utils.io.json import JsonSerializer from easyscience.Objects.variable import DescriptorNumber -from .test_core import A from .test_core import check_dict from .test_core import dp_param_dict from .test_core import skip_dict @@ -93,78 +92,6 @@ def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNum check_dict(data_dict, enc_d) - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_DictSerializer_encode( - dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip -): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = { - "@module": A.__module__, - "@class": A.__name__, - "@version": None, - "name": "A", - dp_kwargs["name"]: deepcopy(dp_kwargs), - } - - if not isinstance(skip, list): - skip = [skip] - - full_d = recursive_remove(full_d, skip) - - obj = A(**a_kw) - - enc = obj.encode(skip=skip, encoder=JsonSerializer) - assert isinstance(enc, str) - - # We can test like this as we don't have "complex" objects yet - dec = json.loads(enc) - - expected_keys = set(full_d.keys()) - obtained_keys = set(dec.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, dec) - - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_DataDictSerializer( - dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip -): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = {"name": "A", dp_kwargs["name"]: data_dict} - - if not isinstance(skip, list): - skip = [skip] - - full_d = recursive_remove(full_d, skip) - - obj = A(**a_kw) - - enc = obj.encode(skip=skip, encoder=JsonDataSerializer) - dec = json.loads(enc) - - expected_keys = set(full_d.keys()) - obtained_keys = set(dec.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(full_d, dec) - - # ######################################################################################################################## # # TESTING DECODING # ######################################################################################################################## diff --git a/tests/unit_tests/utils/io_tests/test_xml.py b/tests/unit_tests/utils/io_tests/test_xml.py index 2edb761e..b382bf89 100644 --- a/tests/unit_tests/utils/io_tests/test_xml.py +++ b/tests/unit_tests/utils/io_tests/test_xml.py @@ -11,7 +11,6 @@ from easyscience.Utils.io.xml import XMLSerializer from easyscience.Objects.variable import DescriptorNumber -from .test_core import A from .test_core import dp_param_dict from .test_core import skip_dict from easyscience import global_object @@ -65,39 +64,6 @@ def test_variable_XMLDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumb assert data_xml.tag == "data" recursive_test(data_xml, ref_encode) - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_custom_class_XMLDictSerializer_encode( - dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip -): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - a_kw = {data_dict["name"]: dp_cls(**data_dict)} - - full_d = { - "@module": A.__module__, - "@class": A.__name__, - "@version": None, - "name": "A", - dp_kwargs["name"]: deepcopy(dp_kwargs), - } - - if not isinstance(skip, list): - skip = [skip] - - full_d = recursive_remove(full_d, skip) - - obj = A(**a_kw) - - enc = obj.encode(skip=skip, encoder=XMLSerializer) - ref_encode = obj.encode(skip=skip) - assert isinstance(enc, str) - data_xml = ET.XML(enc) - assert data_xml.tag == "data" - recursive_test(data_xml, ref_encode) - - # ######################################################################################################################## # # TESTING DECODING # ######################################################################################################################## From 9a770fe5120151e8d72583c2a8a073fb6332bd18 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 25 Apr 2025 15:38:11 +0200 Subject: [PATCH 16/58] Fix integration tests --- .../integration_tests/Fitting/test_fitter.py | 12 +- .../Fitting/test_multi_fitter.py | 43 ++---- tests/unit_tests/Fitting/test_constraints.py | 134 ------------------ 3 files changed, 10 insertions(+), 179 deletions(-) delete mode 100644 tests/unit_tests/Fitting/test_constraints.py diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 19e0f876..92217b4d 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -2,13 +2,9 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Tuple[List[Parameter], List[int]]: - mock_callback = MagicMock() - mock_callback.fget = MagicMock(return_value=-10) - return [Parameter("a", 1, callback=mock_callback), Parameter("b", 2, callback=mock_callback)], [1, 2] - - -@pytest.fixture -def threePars(twoPars) -> Tuple[List[Parameter], List[int]]: - ps, vs = twoPars - ps.append(Parameter("c", 3)) - vs.append(3) - return ps, vs - - -def test_NumericConstraints_Equals(twoPars): - - value = 1 - - # Should skip - c = NumericConstraint(twoPars[0][0], "==", value) - c() - assert twoPars[0][0].value_no_call_back == twoPars[1][0] - - # Should update to new value - c = NumericConstraint(twoPars[0][1], "==", value) - c() - assert twoPars[0][1].value_no_call_back == value - - -def test_NumericConstraints_Greater(twoPars): - value = 1.5 - - # Should update to new value - c = NumericConstraint(twoPars[0][0], ">", value) - c() - assert twoPars[0][0].value_no_call_back == value - - # Should skip - c = NumericConstraint(twoPars[0][1], ">", value) - c() - assert twoPars[0][1].value_no_call_back == twoPars[1][1] - - -def test_NumericConstraints_Less(twoPars): - value = 1.5 - - # Should skip - c = NumericConstraint(twoPars[0][0], "<", value) - c() - assert twoPars[0][0].value_no_call_back == twoPars[1][0] - - # Should update to new value - c = NumericConstraint(twoPars[0][1], "<", value) - c() - assert twoPars[0][1].value_no_call_back == value - - -@pytest.mark.parametrize("multiplication_factor", [None, 1, 2, 3, 4.5]) -def test_ObjConstraintMultiply(twoPars, multiplication_factor): - if multiplication_factor is None: - multiplication_factor = 1 - operator_str = "" - else: - operator_str = f"{multiplication_factor}*" - c = ObjConstraint(twoPars[0][0], operator_str, twoPars[0][1]) - c() - assert twoPars[0][0].value_no_call_back == multiplication_factor * twoPars[1][1] - - -@pytest.mark.parametrize("division_factor", [1, 2, 3, 4.5]) -def test_ObjConstraintDivide(twoPars, division_factor): - operator_str = f"{division_factor}/" - c = ObjConstraint(twoPars[0][0], operator_str, twoPars[0][1]) - c() - assert twoPars[0][0].value_no_call_back == division_factor / twoPars[1][1] - - -def test_ObjConstraint_Multiple(threePars): - - p0 = threePars[0][0] - p1 = threePars[0][1] - p2 = threePars[0][2] - - value = 1.5 - - p0.user_constraints["num_1"] = ObjConstraint(p1, "", p0) - p0.user_constraints["num_2"] = ObjConstraint(p2, "", p0) - - p0.value = value - assert p0.value_no_call_back == value - assert p1.value_no_call_back == value - assert p2.value_no_call_back == value - - -def test_ConstraintEnable_Disable(twoPars): - - assert twoPars[0][0].independent - assert twoPars[0][1].independent - - c = ObjConstraint(twoPars[0][0], "", twoPars[0][1]) - twoPars[0][0].user_constraints["num_1"] = c - - assert c.enabled - assert twoPars[0][1].independent - assert not twoPars[0][0].independent - - c.enabled = False - assert not c.enabled - assert twoPars[0][1].independent - assert twoPars[0][0].independent - - c.enabled = True - assert c.enabled - assert twoPars[0][1].independent - assert not twoPars[0][0].independent From 30f19e36b81f8ac61786158433cc330092d6a3bc Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 25 Apr 2025 15:54:34 +0200 Subject: [PATCH 17/58] rename property_stack_deco and make_dependent --- .../Objects/variable/descriptor_any_type.py | 4 ++-- .../Objects/variable/descriptor_array.py | 8 ++++---- .../Objects/variable/descriptor_base.py | 6 +++--- .../Objects/variable/descriptor_bool.py | 4 ++-- .../Objects/variable/descriptor_number.py | 8 ++++---- .../Objects/variable/descriptor_str.py | 4 ++-- src/easyscience/Objects/variable/parameter.py | 20 +++++++++---------- src/easyscience/global_object/undo_redo.py | 6 +++--- .../integration_tests/Fitting/test_fitter.py | 2 +- .../Fitting/test_multi_fitter.py | 16 +++++++-------- 10 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_any_type.py b/src/easyscience/Objects/variable/descriptor_any_type.py index 0d117ce2..93745d97 100644 --- a/src/easyscience/Objects/variable/descriptor_any_type.py +++ b/src/easyscience/Objects/variable/descriptor_any_type.py @@ -9,7 +9,7 @@ import numpy as np -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_base import DescriptorBase @@ -62,7 +62,7 @@ def value(self) -> numbers.Number: return self._value @value.setter - @property_stack_deco + @property_stack def value(self, value: Union[list, np.ndarray]) -> None: """ Set the value of self. diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index c9b154e5..c7a1d8ca 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -16,7 +16,7 @@ from scipp import Variable from easyscience.global_object.undo_redo import PropertyStack -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_base import DescriptorBase from .descriptor_number import DescriptorNumber @@ -150,7 +150,7 @@ def value(self) -> numbers.Number: return self._array.values @value.setter - @property_stack_deco + @property_stack def value(self, value: Union[list, np.ndarray]) -> None: """ Set the value of self. Ensures the input is an array and matches the shape of the existing array. @@ -225,7 +225,7 @@ def variance(self) -> np.ndarray: return self._array.variances @variance.setter - @property_stack_deco + @property_stack def variance(self, variance: Union[list, np.ndarray]) -> None: """ Set the variance of self. Ensures the input is an array and matches the shape of the existing values. @@ -259,7 +259,7 @@ def error(self) -> Optional[np.ndarray]: return np.sqrt(self._array.variances) @error.setter - @property_stack_deco + @property_stack def error(self, error: Union[list, np.ndarray]) -> None: """ Set the standard deviation for the parameter, which updates the variances. diff --git a/src/easyscience/Objects/variable/descriptor_base.py b/src/easyscience/Objects/variable/descriptor_base.py index b525d4f1..b80065a2 100644 --- a/src/easyscience/Objects/variable/descriptor_base.py +++ b/src/easyscience/Objects/variable/descriptor_base.py @@ -9,7 +9,7 @@ from typing import Optional from easyscience import global_object -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from easyscience.Objects.core import ComponentSerializer @@ -94,7 +94,7 @@ def name(self) -> str: return self._name @name.setter - @property_stack_deco + @property_stack def name(self, new_name: str) -> None: """ Set the name. @@ -118,7 +118,7 @@ def display_name(self) -> str: return display_name @display_name.setter - @property_stack_deco + @property_stack def display_name(self, name: str) -> None: """ Set the pretty display name. diff --git a/src/easyscience/Objects/variable/descriptor_bool.py b/src/easyscience/Objects/variable/descriptor_bool.py index 768b35b1..23869172 100644 --- a/src/easyscience/Objects/variable/descriptor_bool.py +++ b/src/easyscience/Objects/variable/descriptor_bool.py @@ -3,7 +3,7 @@ from typing import Any from typing import Optional -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_base import DescriptorBase @@ -46,7 +46,7 @@ def value(self) -> bool: return self._bool_value @value.setter - @property_stack_deco + @property_stack def value(self, value: bool) -> None: """ Set the value of self. diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index b80533d5..f634a810 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -13,7 +13,7 @@ from scipp import Variable from easyscience.global_object.undo_redo import PropertyStack -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_base import DescriptorBase @@ -155,7 +155,7 @@ def value(self) -> numbers.Number: @value.setter @notify_observers - @property_stack_deco + @property_stack def value(self, value: numbers.Number) -> None: """ Set the value of self. This should be usable for most cases. The full value can be obtained from `obj.full_value`. @@ -195,7 +195,7 @@ def variance(self) -> float: @variance.setter @notify_observers - @property_stack_deco + @property_stack def variance(self, variance_float: float) -> None: """ Set the variance. @@ -223,7 +223,7 @@ def error(self) -> float: @error.setter @notify_observers - @property_stack_deco + @property_stack def error(self, value: float) -> None: """ Set the standard deviation for the parameter. diff --git a/src/easyscience/Objects/variable/descriptor_str.py b/src/easyscience/Objects/variable/descriptor_str.py index 1abe4e4e..17110166 100644 --- a/src/easyscience/Objects/variable/descriptor_str.py +++ b/src/easyscience/Objects/variable/descriptor_str.py @@ -3,7 +3,7 @@ from typing import Any from typing import Optional -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_base import DescriptorBase @@ -45,7 +45,7 @@ def value(self) -> str: return self._string @value.setter - @property_stack_deco + @property_stack def value(self, value: str) -> None: """ Set the value of self. diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index d3b87663..57c2cac6 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -22,7 +22,7 @@ from scipp import Variable from easyscience import global_object -from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.global_object.undo_redo import property_stack from .descriptor_number import DescriptorNumber from .descriptor_number import notify_observers @@ -124,7 +124,7 @@ def from_dependency(cls, name: str, dependency_expression: str, dependency_map: :return: A new dependent Parameter object. """ # noqa: E501 parameter = cls(name=name, value=0.0, unit='', variance=0.0, min=-np.inf, max=np.inf, **kwargs) - parameter.make_dependent(dependency_expression=dependency_expression, dependency_map=dependency_map) + parameter.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) return parameter @@ -156,7 +156,7 @@ def _update(self, update_id: int, updating_object: str) -> None: else: warnings.warn('This parameter is not dependent. It cannot be updated.') - def make_dependent(self, dependency_expression: str, dependency_map: Optional[dict] = None) -> None: + def make_dependent_on(self, dependency_expression: str, dependency_map: Optional[dict] = None) -> None: """ Make this parameter dependent on another parameter. This will overwrite the current value, unit, variance, min and max. @@ -240,7 +240,7 @@ def independent(self) -> bool: @independent.setter def independent(self, value: bool) -> None: - raise AttributeError('This property is read-only. Use `make_independent` and `make_dependent` to change the state of the parameter.') # noqa: E501 + raise AttributeError('This property is read-only. Use `make_independent` and `make_dependent_on` to change the state of the parameter.') # noqa: E501 @property def depedency_expression(self) -> str: @@ -256,7 +256,7 @@ def depedency_expression(self) -> str: @depedency_expression.setter def depedency_expression(self, new_expression: str) -> None: - raise AttributeError('Dependency expression is read-only. Use `make_dependent` to change the dependency expression.') + raise AttributeError('Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression.') @property def dependency_map(self) -> Dict[str, DescriptorNumber]: @@ -272,7 +272,7 @@ def dependency_map(self) -> Dict[str, DescriptorNumber]: @dependency_map.setter def dependency_map(self, new_map: Dict[str, DescriptorNumber]) -> None: - raise AttributeError('Dependency map is read-only. Use `make_dependent` to change the dependency map.') + raise AttributeError('Dependency map is read-only. Use `make_dependent_on` to change the dependency map.') @property def value_no_call_back(self) -> numbers.Number: @@ -317,7 +317,7 @@ def value(self) -> numbers.Number: return self._scalar.value @value.setter - @property_stack_deco + @property_stack def value(self, value: numbers.Number) -> None: """ Set the value of self. This only updates the value of the scipp scalar. @@ -400,7 +400,7 @@ def min(self) -> numbers.Number: return self._min.value @min.setter - @property_stack_deco + @property_stack def min(self, min_value: numbers.Number) -> None: """ Set the minimum value for fitting. @@ -432,7 +432,7 @@ def max(self) -> numbers.Number: return self._max.value @max.setter - @property_stack_deco + @property_stack def max(self, max_value: numbers.Number) -> None: """ Get the maximum value for fitting. @@ -464,7 +464,7 @@ def fixed(self) -> bool: return self._fixed @fixed.setter - @property_stack_deco + @property_stack def fixed(self, fixed: bool) -> None: """ Change the parameter vary while fitting state. diff --git a/src/easyscience/global_object/undo_redo.py b/src/easyscience/global_object/undo_redo.py index 02b30020..e421bcf1 100644 --- a/src/easyscience/global_object/undo_redo.py +++ b/src/easyscience/global_object/undo_redo.py @@ -428,18 +428,18 @@ def redo(self) -> NoReturn: self._parent.data = self._new_value -def property_stack_deco(arg: Union[str, Callable], begin_macro: bool = False) -> Callable: +def property_stack(arg: Union[str, Callable], begin_macro: bool = False) -> Callable: """ Decorate a `property` setter with undo/redo functionality This decorator can be used as: - @property_stack_deco + @property_stack def func() .... or - @property_stack_deco("This is the undo/redo text) + @property_stack("This is the undo/redo text) def func() .... diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 92217b4d..0706d3bc 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -240,7 +240,7 @@ def test_fit_constraints(fit_engine): f = Fitter(sp_sin, sp_sin) - sp_sin.offset.make_dependent(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) + sp_sin.offset.make_dependent_on(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) if fit_engine is not None: try: diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index e73e1c07..3a546b29 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -65,9 +65,9 @@ def test_multi_fit(fit_engine, with_errors): ref_sin_2 = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) sp_sin_2 = AbsSin(1, 0.5) - ref_sin_2.offset.make_dependent(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) + ref_sin_2.offset.make_dependent_on(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) - sp_sin_2.offset.make_dependent(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) + sp_sin_2.offset.make_dependent_on(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) x1 = np.linspace(0, 5, 200) y1 = ref_sin_1(x1) @@ -121,13 +121,13 @@ def test_multi_fit2(fit_engine, with_errors): sp_sin_2 = AbsSin(1, 0.5)# ref_sin_1_obj = genObjs[0] ref_line_obj = Line(1, 4.6) - ref_sin_2.offset.make_dependent(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) - ref_line_obj.m.make_dependent(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) + ref_sin_2.offset.make_dependent_on(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) + ref_line_obj.m.make_dependent_on(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin_1.offset}) sp_line = Line(0.43, 6.1) - sp_sin_2.offset.make_dependent(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) - sp_line.m.make_dependent(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) + sp_sin_2.offset.make_dependent_on(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) + sp_line.m.make_dependent_on(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin_1.offset}) x1 = np.linspace(0, 5, 200) @@ -192,8 +192,8 @@ def test_multi_fit_1D_2D(fit_engine, with_errors): ) # The fit is VERY sensitive to the initial values :-( # Link the parameters - ref_sin2D.offset.make_dependent(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin1D.offset}) - sp_sin2D.offset.make_dependent(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin1D.offset}) + ref_sin2D.offset.make_dependent_on(dependency_expression="ref_sin1", dependency_map={"ref_sin1": ref_sin1D.offset}) + sp_sin2D.offset.make_dependent_on(dependency_expression="sp_sin1", dependency_map={"sp_sin1": sp_sin1D.offset}) # Generate data x1D = np.linspace(0.2, 3.8, 400) From 0115eb72c78b2136c43deaef3c4139e15ccedc34 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 25 Apr 2025 16:18:47 +0200 Subject: [PATCH 18/58] Pr comments --- .../Objects/variable/descriptor_number.py | 3 +++ src/easyscience/Objects/variable/parameter.py | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py index f634a810..98631542 100644 --- a/src/easyscience/Objects/variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -240,6 +240,8 @@ def error(self, value: float) -> None: else: self._scalar.variance = None + # When we convert units internally, we dont want to notify observers as this can cause infinite recursion. + # Therefore the convert_unit method is split into two methods, a private internal method and a public method. def _convert_unit(self, unit_str: str) -> None: """ Convert the value from one unit system to another. @@ -271,6 +273,7 @@ def set_scalar(obj, scalar): # Update the scalar self._scalar = new_scalar + # When the user calls convert_unit, we want to notify observers of the change to propagate the change. @notify_observers def convert_unit(self, unit_str: str) -> None: """ diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 57c2cac6..d18414e5 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -64,7 +64,7 @@ def __init__( :param variance: The variance of the value :param min: The minimum value for fitting :param max: The maximum value for fitting - :param fixed: Can the parameter vary? + :param fixed: If the parameter is free to vary during fitting :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed @@ -160,7 +160,15 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional """ Make this parameter dependent on another parameter. This will overwrite the current value, unit, variance, min and max. - :param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter. + How to use the dependency map: + If a parameter c has a dependency expression of 'a + b', where a and b are parameters belonging to the model class, + then the dependency map needs to have the form {'a': model.a, 'b': model.b}, where model is the model class. + I.e. the values are the actual objects, whereas the keys are how they are represented in the dependency expression. + + The dependency map is not needed if the dependency expression uses the unique names of the parameters. + Unique names in dependency expressions are defined by quotes, e.g. 'Parameter_0' or "Parameter_0" depending on the quotes used for the expression. + + :param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by a python interpreter. :param dependency_map: A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies. """ # noqa: E501 if not isinstance(dependency_expression, str): @@ -181,13 +189,13 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._dependency_string = dependency_expression self._dependency_map = dependency_map if dependency_map is not None else {} self._dependency_interpreter = Interpreter(minimal=True) - self._dependency_interpreter.config['if'] = True + self._dependency_interpreter.config['if'] = True # allows logical statements in the dependency expression self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies self._process_dependency_unique_names(self._dependency_string) for key, value in self._dependency_map.items(): self._dependency_interpreter.symtable[key] = value - self._dependency_interpreter.readonly_symbols.add(key) + self._dependency_interpreter.readonly_symbols.add(key) # Dont allow overwriting of the dependencies in the dependency expression # noqa: E501 value._attach_observer(self) try: dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) @@ -243,7 +251,7 @@ def independent(self, value: bool) -> None: raise AttributeError('This property is read-only. Use `make_independent` and `make_dependent_on` to change the state of the parameter.') # noqa: E501 @property - def depedency_expression(self) -> str: + def dependency_expression(self) -> str: """ Get the dependency expression of this parameter. @@ -254,7 +262,7 @@ def depedency_expression(self) -> str: else: raise AttributeError('This parameter is independent. It has no dependency expression.') - @depedency_expression.setter + @dependency_expression.setter def depedency_expression(self, new_expression: str) -> None: raise AttributeError('Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression.') From b94ad0c005a4105222d59a2524e5037d130454bd Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 30 Apr 2025 11:39:55 +0200 Subject: [PATCH 19/58] First batch of tests with related fixes --- src/easyscience/Objects/variable/parameter.py | 41 +-- .../integration_tests/Fitting/test_fitter.py | 6 +- .../variable/test_descriptor_number.py | 1 + .../Objects/variable/test_parameter.py | 238 +++++++++++++++++- 4 files changed, 258 insertions(+), 28 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index d18414e5..9e2ac8b6 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -141,17 +141,24 @@ def _update(self, update_id: int, updating_object: str) -> None: if updating_object not in self._dependency_updates: self._dependency_updates[updating_object] = 0 if self._dependency_updates[updating_object] == update_id: - warnings.warn('Warning: Cyclic dependency detected!\n' + - f'This parameter, {self.unique_name}, has already been updated by {updating_object} during this update.\n' + # noqa: E501 - 'This update will be ignored. Please check your dependencies.') + raise RuntimeError('\n Potential cyclic dependency detected!\n' + + f'This parameter, {self.unique_name}, has already been updated by {updating_object} during this update.\n' + # noqa: E501 + 'Please check your dependencies.') else: # Update the value of the parameter using the dependency interpreter temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) self._scalar.value = temporary_parameter.value self._scalar.unit = temporary_parameter.unit self._scalar.variance = temporary_parameter.variance - self._min.value = temporary_parameter.min - self._max.value = temporary_parameter.max + if isinstance(temporary_parameter, Parameter): + self._min.value = temporary_parameter.min + self._max.value = temporary_parameter.max + else: + self._min.value = temporary_parameter.value + self._max.value = temporary_parameter.value + self._min.unit = temporary_parameter.unit + self._max.unit = temporary_parameter.unit + self._dependency_updates[updating_object] = update_id self._notify_observers(update_id=update_id) else: warnings.warn('This parameter is not dependent. It cannot be updated.') @@ -175,11 +182,12 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional raise TypeError('`dependency_expression` must be a string representing a valid dependency expression.') if not (isinstance(dependency_map, dict) or dependency_map is None): raise TypeError('`dependency_map` must be a dictionary of dependencies and their corresponding names in the dependecy expression.') # noqa: E501 - for key, value in dependency_map.items(): - if not isinstance(key, str): - raise TypeError('`dependency_map` keys must be strings representing the names of the dependencies in the dependency expression.') # noqa: E501 - if not isinstance(value, DescriptorNumber): - raise TypeError(f'`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}.') # noqa: E501 + if isinstance(dependency_map, dict): + for key, value in dependency_map.items(): + if not isinstance(key, str): + raise TypeError('`dependency_map` keys must be strings representing the names of the dependencies in the dependency expression.') # noqa: E501 + if not isinstance(value, DescriptorNumber): + raise TypeError(f'`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}.') # noqa: E501 # If we're overwriting the dependency if not self._independent: @@ -189,7 +197,8 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._dependency_string = dependency_expression self._dependency_map = dependency_map if dependency_map is not None else {} self._dependency_interpreter = Interpreter(minimal=True) - self._dependency_interpreter.config['if'] = True # allows logical statements in the dependency expression + self._dependency_interpreter.config['if'] = True + self._dependency_interpreter.config['ifexp'] = True # allows logical statements in the dependency expression self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies self._process_dependency_unique_names(self._dependency_string) @@ -204,16 +213,18 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional '\n'+'\n'.join(str(message).split("\n")[1:])+ '\nPlease check your expression or add the name to the `dependency_map`') from None except Exception as message: - raise Exception('\nError encountered in dependecy expression:'+ + raise SyntaxError('\nError encountered in dependecy expression:'+ '\n'+'\n'.join(str(message).split("\n")[1:])+ '\nPlease check your expression') from None if not isinstance(dependency_result, DescriptorNumber): - raise TypeError(f'The dependency expression: "{self._clean_dependency_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 + raise TypeError(f'The dependency expression: "{self._dependency_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 self._scalar.value = dependency_result.value self._scalar.unit = dependency_result.unit self._scalar.variance = dependency_result.variance self._min.value = dependency_result.min if isinstance(dependency_result, Parameter) else dependency_result.value self._max.value = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value + self._min.unit = dependency_result.unit + self._max.unit = dependency_result.unit self._independent = False self._fixed = False self._notify_observers() @@ -263,8 +274,8 @@ def dependency_expression(self) -> str: raise AttributeError('This parameter is independent. It has no dependency expression.') @dependency_expression.setter - def depedency_expression(self, new_expression: str) -> None: - raise AttributeError('Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression.') + def dependency_expression(self, new_expression: str) -> None: + raise AttributeError('Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression.') # noqa: E501 @property def dependency_map(self) -> Dict[str, DescriptorNumber]: diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 0706d3bc..7a644de2 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -11,7 +11,7 @@ from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.variable import Parameter - +# Model and container of parameters for tests class AbsSin(BaseObj): phase: Parameter offset: Parameter @@ -229,15 +229,13 @@ def test_bumps_methods(fit_method): @pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_fit_constraints(fit_engine): +def test_dependent_parameter(fit_engine): ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) sp_sin = AbsSin(1, 0.5) x = np.linspace(0, 5, 200) y = ref_sin(x) - sp_sin.phase.fixed = False - f = Fitter(sp_sin, sp_sin) sp_sin.offset.make_dependent_on(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_number.py b/tests/unit_tests/Objects/variable/test_descriptor_number.py index 62b359a4..b9de196a 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_number.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_number.py @@ -30,6 +30,7 @@ def test_init(self, descriptor: DescriptorNumber): assert descriptor._scalar.value == 1 assert descriptor._scalar.unit == "m" assert descriptor._scalar.variance == 0.1 + assert descriptor._observers == [] # From super assert descriptor._name == "name" diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py index 476827ce..db380744 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -8,6 +8,7 @@ from easyscience.Objects.variable.parameter import Parameter from easyscience.Objects.variable.descriptor_number import DescriptorNumber from easyscience import global_object +from easyscience.Objects.ObjectClasses import BaseObj class TestParameter: @pytest.fixture @@ -27,11 +28,32 @@ def parameter(self) -> Parameter: parent=None, ) return parameter + + @pytest.fixture + def normal_parameter(self) -> Parameter: + parameter = Parameter( + name="name", + value=1, + unit="m", + variance=0.01, + min=0, + max=10, + ) + return parameter @pytest.fixture def clear(self): global_object.map._clear() + def compare_parameters(self, parameter1: Parameter, parameter2: Parameter): + assert parameter1.value == parameter2.value + assert parameter1.unit == parameter2.unit + assert parameter1.variance == parameter2.variance + assert parameter1.min == parameter2.min + assert parameter1.max == parameter2.max + assert parameter1._min.unit == parameter2._min.unit + assert parameter1._max.unit == parameter2._max.unit + def test_init(self, parameter: Parameter): # When Then Expect assert parameter._min.value == 0 @@ -49,6 +71,8 @@ def test_init(self, parameter: Parameter): assert parameter._description == "description" assert parameter._url == "url" assert parameter._display_name == "display_name" + assert parameter._fixed == False + assert parameter._observers == [] def test_init_value_min_exception(self): # When @@ -92,6 +116,208 @@ def test_init_value_max_exception(self): parent=None, ) + def test_make_dependent_on(self, normal_parameter: Parameter): + # When + independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10) + + # Then + normal_parameter.make_dependent_on(dependency_expression='2*a', dependency_map={'a': independent_parameter}) + + # Expect + assert normal_parameter._independent == False + assert normal_parameter.dependency_expression == '2*a' + assert normal_parameter.dependency_map == {'a': independent_parameter} + self.compare_parameters(normal_parameter, 2*independent_parameter) + + # Then + independent_parameter.value = 2 + + # Expect + self.compare_parameters(normal_parameter, 2*independent_parameter) + + def test_parameter_from_dependency(self, normal_parameter: Parameter): + # When Then + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + display_name='display_name', + ) + + # Expect + assert dependent_parameter._independent == False + assert dependent_parameter.dependency_expression == '2*a' + assert dependent_parameter.dependency_map == {'a': normal_parameter} + assert dependent_parameter.name == 'dependent' + assert dependent_parameter.display_name == 'display_name' + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + # Then + normal_parameter.value = 2 + + # Expect + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + def test_dependent_parameter_with_unique_name(self, clear, normal_parameter: Parameter): + # When Then + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*"Parameter_0"', + ) + + # Expect + assert dependent_parameter.dependency_expression == '2*"Parameter_0"' + assert dependent_parameter.dependency_map == {'__Parameter_0__': normal_parameter} + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + # Then + normal_parameter.value = 2 + + # Expect + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + def test_process_dependency_unique_names_double_quotes(self, clear, normal_parameter: Parameter): + # When + independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name') + normal_parameter._dependency_map = {} + + # Then + normal_parameter._process_dependency_unique_names(dependency_expression='2*"Special_name"') + + # Expect + assert normal_parameter._dependency_map == {'__Special_name__': independent_parameter} + assert normal_parameter._clean_dependency_string == '2*__Special_name__' + + def test_process_dependency_unique_names_single_quotes(self, clear, normal_parameter: Parameter): + # When + independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name') + independent_parameter_2 = Parameter(name="independent_2", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name_2') + normal_parameter._dependency_map = {} + + # Then + normal_parameter._process_dependency_unique_names(dependency_expression="'Special_name' + 'Special_name_2'") + + # Expect + assert normal_parameter._dependency_map == {'__Special_name__': independent_parameter, + '__Special_name_2__': independent_parameter_2} + assert normal_parameter._clean_dependency_string == '__Special_name__ + __Special_name_2__' + + def test_process_dependency_unique_names_exception_unique_name_does_not_exist(self, clear, normal_parameter: Parameter): + # When + normal_parameter._dependency_map = {} + + # Then Expect + with pytest.raises(ValueError): + normal_parameter._process_dependency_unique_names(dependency_expression='2*"Special_name"') + + def test_process_dependency_unique_names_exception_not_a_descriptorNumber(self, clear, normal_parameter: Parameter): + # When + normal_parameter._dependency_map = {} + base_obj = BaseObj(name='BaseObj', unique_name='base_obj') + + # Then Expect + with pytest.raises(ValueError, match='The object with unique_name base_obj is not a Parameter or DescriptorNumber. Please check your dependency expression.'): + normal_parameter._process_dependency_unique_names(dependency_expression='2*"base_obj"') + + @pytest.mark.parametrize("dependency_expression, dependency_map", [ + (2, {'a': Parameter(name='a', value=1)}), + ('2*a', ['a', Parameter(name='a', value=1)]), + ('2*a', {4: Parameter(name='a', value=1)}), + ('2*a', {'a': BaseObj(name='a')}), + ], ids=["dependecy_expression_not_a_string", "dependency_map_not_a_dict", "dependency_map_keys_not_strings", "dependency_map_values_not_descriptor_number"]) + def test_parameter_from_dependency_input_exceptions(self, dependency_expression, dependency_map): + # When Then Expect + with pytest.raises(TypeError): + Parameter.from_dependency( + name = 'dependent', + dependency_expression=dependency_expression, + dependency_map=dependency_map, + ) + + @pytest.mark.parametrize("dependency_expression, error", [ + ('2*a + b', NameError), + ('2*a + 3*', SyntaxError), + ('2 + 2', TypeError), + ], ids=["parameter_not_in_map", "invalid_dependency_expression", "result_not_a_descriptor_number"]) + def test_parameter_from_dependency_evaluation_exceptions(self, normal_parameter, dependency_expression, error): + # When Then Expect + with pytest.raises(error): + Parameter.from_dependency( + name = 'dependent', + dependency_expression=dependency_expression, + dependency_map={'a': normal_parameter}, + ) + + def test_dependent_parameter_updates(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + normal_parameter.value = 2 + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + normal_parameter.variance = 0.02 + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + normal_parameter.error = 0.2 + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + normal_parameter.convert_unit("cm") + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + normal_parameter.min = 1 + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + normal_parameter.max = 300 + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + def test_dependent_parameter_indirect_updates(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + dependent_parameter_2 = Parameter.from_dependency( + name = 'dependent_2', + dependency_expression='10*a', + dependency_map={'a': normal_parameter}, + ) + dependent_parameter_3 = Parameter.from_dependency( + name = 'dependent_3', + dependency_expression='b+c', + dependency_map={'b': dependent_parameter, 'c': dependent_parameter_2}, + ) + # Then + normal_parameter.value = 2 + + # Expect + self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter_2, 10*normal_parameter) + self.compare_parameters(dependent_parameter_3, 2*normal_parameter + 10*normal_parameter) + + def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + dependent_parameter_2 = Parameter.from_dependency( + name = 'dependent_2', + dependency_expression='2*b', + dependency_map={'b': dependent_parameter}, + ) + + # Then Expect + with pytest.raises(RuntimeError): + normal_parameter.make_dependent_on(dependency_expression='2*c', dependency_map={'c': dependent_parameter_2}) + + def test_min(self, parameter: Parameter): # When Then Expect assert parameter.min == 0 @@ -132,10 +358,6 @@ def test_convert_unit(self, parameter: Parameter): assert parameter._max.value == 10000 assert parameter._max.unit == "mm" - def test_fixed(self, parameter: Parameter): - # When Then Expect - assert parameter.fixed == False - def test_set_fixed(self, parameter: Parameter): # When Then parameter.fixed = True @@ -183,11 +405,9 @@ def test_repr_fixed(self, parameter: Parameter): assert repr(parameter) == "" def test_independent(self, parameter: Parameter): - # When - parameter._independent = True - - # Then Expect - assert parameter.independent is True + # When Then Expect + with pytest.raises(AttributeError): + parameter.independent = False def test_value_match_callback(self, parameter: Parameter): # When From 0801ce271b0924c2eb95a9a7bc8efef7f695b163 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 30 Apr 2025 14:10:38 +0200 Subject: [PATCH 20/58] Fix logical dependencies and remaining unittests --- src/easyscience/Objects/variable/parameter.py | 19 +- .../Objects/variable/test_parameter.py | 194 +++++++++++++++++- 2 files changed, 198 insertions(+), 15 deletions(-) diff --git a/src/easyscience/Objects/variable/parameter.py b/src/easyscience/Objects/variable/parameter.py index 9e2ac8b6..63746993 100644 --- a/src/easyscience/Objects/variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -150,12 +150,8 @@ def _update(self, update_id: int, updating_object: str) -> None: self._scalar.value = temporary_parameter.value self._scalar.unit = temporary_parameter.unit self._scalar.variance = temporary_parameter.variance - if isinstance(temporary_parameter, Parameter): - self._min.value = temporary_parameter.min - self._max.value = temporary_parameter.max - else: - self._min.value = temporary_parameter.value - self._max.value = temporary_parameter.value + self._min.value = temporary_parameter.min if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 + self._max.value = temporary_parameter.max if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 self._min.unit = temporary_parameter.unit self._max.unit = temporary_parameter.unit self._dependency_updates[updating_object] = update_id @@ -196,9 +192,14 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._dependency_string = dependency_expression self._dependency_map = dependency_map if dependency_map is not None else {} - self._dependency_interpreter = Interpreter(minimal=True) - self._dependency_interpreter.config['if'] = True - self._dependency_interpreter.config['ifexp'] = True # allows logical statements in the dependency expression + # List of allowed python constructs for the asteval interpreter + asteval_config = {'import': False, 'importfrom': False, 'assert': False, + 'augassign': False, 'delete': False, 'if': True, + 'ifexp': True, 'for': False, 'formattedvalue': False, + 'functiondef': False, 'print': False, 'raise': False, + 'listcomp': False, 'dictcomp': False, 'setcomp': False, + 'try': False, 'while': False, 'with': False} + self._dependency_interpreter = Interpreter(config=asteval_config) self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies self._process_dependency_unique_names(self._dependency_string) diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py index db380744..78e19da0 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -317,6 +317,121 @@ def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Paramet with pytest.raises(RuntimeError): normal_parameter.make_dependent_on(dependency_expression='2*c', dependency_map={'c': dependent_parameter_2}) + def test_dependent_parameter_logical_dependency(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='a if a.value > 0 else -a', + dependency_map={'a': normal_parameter}, + ) + self.compare_parameters(dependent_parameter, normal_parameter) + + # Then + normal_parameter.value = -2 + + # Expect + self.compare_parameters(dependent_parameter, -normal_parameter) + + def test_dependent_parameter_return_is_descriptor_number(self): + # When + descriptor_number = DescriptorNumber(name='descriptor', value=1, unit='m', variance=0.01) + + # Then + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*descriptor', + dependency_map={'descriptor': descriptor_number}, + ) + + # Expect + assert dependent_parameter.value == 2*descriptor_number.value + assert dependent_parameter.unit == descriptor_number.unit + assert dependent_parameter.variance == 0.04 + assert dependent_parameter.min == 2*descriptor_number.value + assert dependent_parameter.max == 2*descriptor_number.value + + def test_dependent_parameter_overwrite_dependency(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + # Then + normal_parameter_2 = Parameter(name='a2', value=-2, unit='m', variance=0.01, min=-10, max=0) + dependent_parameter.make_dependent_on(dependency_expression='3*a2', dependency_map={'a2': normal_parameter_2}) + normal_parameter.value = 3 + + # Expect + self.compare_parameters(dependent_parameter, 3*normal_parameter_2) + assert dependent_parameter.dependency_expression == '3*a2' + assert dependent_parameter.dependency_map == {'a2': normal_parameter_2} + assert normal_parameter._observers == [] + + def test_make_independent(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + assert dependent_parameter.independent == False + self.compare_parameters(dependent_parameter, 2*normal_parameter) + + # Then + dependent_parameter.make_independent() + normal_parameter.value = 5 + + # Expect + assert dependent_parameter.independent == True + assert normal_parameter._observers == [] + assert dependent_parameter.value == 2 + + def test_make_independent_exception(self, normal_parameter: Parameter): + # When Then Expect + with pytest.raises(AttributeError): + normal_parameter.make_independent() + + def test_independent_setter(self, normal_parameter: Parameter): + # When Then Expect + with pytest.raises(AttributeError): + normal_parameter.independent = False + + def test_independent_parameter_dependency_expression(self, normal_parameter: Parameter): + # When Then Expect + with pytest.raises(AttributeError): + normal_parameter.dependency_expression + + def test_dependent_parameter_dependency_expression_setter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.dependency_expression = '3*a' + + def test_independent_parameter_dependency_map(self, normal_parameter: Parameter): + # When Then Expect + with pytest.raises(AttributeError): + normal_parameter.dependency_map + + def test_dependent_parameter_dependency_map_setter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.dependency_map = {'a': normal_parameter} def test_min(self, parameter: Parameter): # When Then Expect @@ -331,6 +446,18 @@ def test_set_min(self, parameter: Parameter): # Expect assert parameter.min == 0.1 + def test_set_min_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.min = 0.1 + def test_set_min_exception(self, parameter: Parameter): # When Then Expect with pytest.raises(ValueError): @@ -343,6 +470,18 @@ def test_set_max(self, parameter: Parameter): # Expect assert parameter.max == 10 + def test_set_max_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.max = 10 + def test_set_max_exception(self, parameter: Parameter): # When Then Expect with pytest.raises(ValueError): @@ -365,6 +504,18 @@ def test_set_fixed(self, parameter: Parameter): # Expect assert parameter.fixed == True + def test_set_fixed_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.fixed = True + @pytest.mark.parametrize("fixed", ["True", 1]) def test_set_fixed_exception(self, parameter: Parameter, fixed): # When Then Expect @@ -404,11 +555,6 @@ def test_repr_fixed(self, parameter: Parameter): # Then Expect assert repr(parameter) == "" - def test_independent(self, parameter: Parameter): - # When Then Expect - with pytest.raises(AttributeError): - parameter.independent = False - def test_value_match_callback(self, parameter: Parameter): # When self.mock_callback.fget.return_value = 1.0 @@ -439,6 +585,18 @@ def test_set_value(self, parameter: Parameter): assert parameter._callback.fset.call_count == 1 assert parameter._scalar == sc.scalar(2, unit='m') + def test_set_value_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.value = 3 + def test_full_value_match_callback(self, parameter: Parameter): # When self.mock_callback.fget.return_value = sc.scalar(1, unit='m') @@ -459,7 +617,31 @@ def test_set_full_value(self, parameter: Parameter): # When Then Expect with pytest.raises(AttributeError): parameter.full_value = sc.scalar(2, unit='s') - + + def test_set_variance_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.variance = 0.1 + + def test_set_error_dependent_parameter(self, normal_parameter: Parameter): + # When + dependent_parameter = Parameter.from_dependency( + name = 'dependent', + dependency_expression='2*a', + dependency_map={'a': normal_parameter}, + ) + + # Then Expect + with pytest.raises(AttributeError): + dependent_parameter.error = 0.1 + def test_copy(self, parameter: Parameter): # When Then self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value From 23b03dd42f13fe2f61cfb64d0dcc7c8949f1d176 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 14:34:18 +0200 Subject: [PATCH 21/58] Remove unused code from ComponentSerializer class and associated tests + now unused jsanatize from JsonDecoderTemplate --- src/easyscience/Objects/core.py | 58 -------- src/easyscience/Utils/io/json.py | 50 ------- src/easyscience/legacy/legacy_core.py | 136 ++++++++++++++++++ tests/unit_tests/Objects/test_BaseObj.py | 5 +- tests/unit_tests/Objects/test_Groups.py | 3 - .../variable/test_descriptor_any_type.py | 16 +-- .../Objects/variable/test_descriptor_array.py | 26 ---- .../Objects/variable/test_descriptor_base.py | 13 -- .../Objects/variable/test_descriptor_bool.py | 16 +-- .../variable/test_descriptor_number.py | 16 --- .../Objects/variable/test_descriptor_str.py | 16 +-- .../Objects/variable/test_parameter.py | 20 --- tests/unit_tests/utils/io_tests/test_core.py | 24 ---- tests/unit_tests/utils/io_tests/test_dict.py | 28 ---- 14 files changed, 141 insertions(+), 286 deletions(-) create mode 100644 src/easyscience/legacy/legacy_core.py diff --git a/src/easyscience/Objects/core.py b/src/easyscience/Objects/core.py index 0754040f..a765a672 100644 --- a/src/easyscience/Objects/core.py +++ b/src/easyscience/Objects/core.py @@ -4,21 +4,13 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - -import json -from collections import OrderedDict -from hashlib import sha1 from typing import TYPE_CHECKING from typing import Any from typing import Dict from typing import List from typing import Optional -from easyscience.Utils.io.dict import DataDictSerializer from easyscience.Utils.io.dict import DictSerializer -from easyscience.Utils.io.json import jsanitize if TYPE_CHECKING: from easyscience.Utils.io.template import EC @@ -84,53 +76,3 @@ def from_dict(cls, obj_dict: Dict[str, Any]) -> None: """ return cls.decode(obj_dict, decoder=DictSerializer) - - def encode_data(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, **kwargs) -> Any: - """ - Returns just the data in an EasyScience object win the format specified by an encoder. - - :param skip: List of field names as strings to skip when forming the dictionary - :param encoder: The encoder to be used for encoding the data. Default is `DataDictSerializer` - :param kwargs: Any additional keywords to pass to the encoder when encoding - :return: encoded object containing just the data of an EasyScience object. - """ - - if encoder is None: - encoder = DataDictSerializer - return self.encode(skip=skip, encoder=encoder, **kwargs) - - def as_data_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Returns a dictionary containing just the data of an EasyScience object. - - :param skip: List of field names as strings to skip when forming the dictionary - :return: dictionary containing just the data of an EasyScience object. - """ - - return self.encode(skip=skip, encoder=DataDictSerializer) - - def unsafe_hash(self) -> sha1: - """ - Returns an hash of the current object. This uses a generic but low - performance method of converting the object to a dictionary, flattening - any nested keys, and then performing a hash on the resulting object - """ - - def flatten(obj, seperator='.'): - # Flattens a dictionary - - flat_dict = {} - for key, value in obj.items(): - if isinstance(value, dict): - flat_dict.update({seperator.join([key, _key]): _value for _key, _value in flatten(value).items()}) - elif isinstance(value, list): - list_dict = {'{}{}{}'.format(key, seperator, num): item for num, item in enumerate(value)} - flat_dict.update(flatten(list_dict)) - else: - flat_dict[key] = value - - return flat_dict - - ordered_keys = sorted(flatten(jsanitize(self.as_dict())).items(), key=lambda x: x[0]) - ordered_keys = [item for item in ordered_keys if '@' not in item[0]] - return sha1(json.dumps(OrderedDict(ordered_keys)).encode('utf-8')) # noqa: S324 diff --git a/src/easyscience/Utils/io/json.py b/src/easyscience/Utils/io/json.py index 6307c69b..34b8ae10 100644 --- a/src/easyscience/Utils/io/json.py +++ b/src/easyscience/Utils/io/json.py @@ -12,8 +12,6 @@ from typing import TYPE_CHECKING from typing import List -import numpy as np - from .template import BaseEncoderDecoder if TYPE_CHECKING: @@ -121,51 +119,3 @@ def decode(self, s): """ d = json.JSONDecoder.decode(self, s) return self.__class__._converter(d) - - -def jsanitize(obj, strict=False, allow_bson=False): - """ - This method cleans an input json-like object, either a list or a dict or - some sequence, nested or otherwise, by converting all non-string - dictionary keys (such as int and float) to strings, and also recursively - encodes all objects using Monty's as_dict() protocol. - - Args: - obj: input json-like object. - strict (bool): This parameters sets the behavior when jsanitize - encounters an object it does not understand. If strict is True, - jsanitize will try to get the as_dict() attribute of the object. If - no such attribute is found, an attribute error will be thrown. If - strict is False, jsanitize will simply call str(object) to convert - the object to a string representation. - allow_bson (bool): This parameters sets the behavior when jsanitize - encounters an bson supported type such as objectid and datetime. If - True, such bson types will be ignored, allowing for proper - insertion into MongoDb databases. - - Returns: - Sanitized dict that can be json serialized. - """ - # if allow_bson and ( - # isinstance(obj, (datetime.datetime, bytes)) - # or (bson is not None and isinstance(obj, bson.objectid.ObjectId)) - # ): - # return obj - if isinstance(obj, (list, tuple)): - return [jsanitize(i, strict=strict, allow_bson=allow_bson) for i in obj] - if np is not None and isinstance(obj, np.ndarray): - return [jsanitize(i, strict=strict, allow_bson=allow_bson) for i in obj.tolist()] - if isinstance(obj, dict): - return {k.__str__(): jsanitize(v, strict=strict, allow_bson=allow_bson) for k, v in obj.items()} - if isinstance(obj, (int, float)): - return obj - if obj is None: - return None - - if not strict: - return obj.__str__() - - if isinstance(obj, str): - return obj.__str__() - - return jsanitize(obj.as_dict(), strict=strict, allow_bson=allow_bson) diff --git a/src/easyscience/legacy/legacy_core.py b/src/easyscience/legacy/legacy_core.py new file mode 100644 index 00000000..0754040f --- /dev/null +++ b/src/easyscience/legacy/legacy_core.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project Any: + """ + Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `DictSerializer`. + + :param skip: List of field names as strings to skip when forming the encoded object + :param encoder: The encoder to be used for encoding the data. Default is `DictSerializer` + :param kwargs: Any additional key word arguments to be passed to the encoder + :return: encoded object containing all information to reform an EasyScience object. + """ + if encoder is None: + encoder = DictSerializer + encoder_obj = encoder() + return encoder_obj.encode(self, skip=skip, **kwargs) + + @classmethod + def decode(cls, obj: Any, decoder: Optional[EC] = None) -> Any: + """ + Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. + + :param obj: encoded EasyScience object + :param decoder: decoder to be used to reform the EasyScience object + :return: Reformed EasyScience object + """ + + if decoder is None: + decoder = DictSerializer + return decoder.decode(obj) + + def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Convert an EasyScience object into a full dictionary using `DictSerializer`. + This is a shortcut for ```obj.encode(encoder=DictSerializer)``` + + :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. + """ + + return self.encode(skip=skip, encoder=DictSerializer) + + @classmethod + def from_dict(cls, obj_dict: Dict[str, Any]) -> None: + """ + Re-create an EasyScience object from a full encoded dictionary. + + :param obj_dict: dictionary containing the serialized contents (from `DictSerializer`) of an EasyScience object + :return: Reformed EasyScience object + """ + + return cls.decode(obj_dict, decoder=DictSerializer) + + def encode_data(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, **kwargs) -> Any: + """ + Returns just the data in an EasyScience object win the format specified by an encoder. + + :param skip: List of field names as strings to skip when forming the dictionary + :param encoder: The encoder to be used for encoding the data. Default is `DataDictSerializer` + :param kwargs: Any additional keywords to pass to the encoder when encoding + :return: encoded object containing just the data of an EasyScience object. + """ + + if encoder is None: + encoder = DataDictSerializer + return self.encode(skip=skip, encoder=encoder, **kwargs) + + def as_data_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Returns a dictionary containing just the data of an EasyScience object. + + :param skip: List of field names as strings to skip when forming the dictionary + :return: dictionary containing just the data of an EasyScience object. + """ + + return self.encode(skip=skip, encoder=DataDictSerializer) + + def unsafe_hash(self) -> sha1: + """ + Returns an hash of the current object. This uses a generic but low + performance method of converting the object to a dictionary, flattening + any nested keys, and then performing a hash on the resulting object + """ + + def flatten(obj, seperator='.'): + # Flattens a dictionary + + flat_dict = {} + for key, value in obj.items(): + if isinstance(value, dict): + flat_dict.update({seperator.join([key, _key]): _value for _key, _value in flatten(value).items()}) + elif isinstance(value, list): + list_dict = {'{}{}{}'.format(key, seperator, num): item for num, item in enumerate(value)} + flat_dict.update(flatten(list_dict)) + else: + flat_dict[key] = value + + return flat_dict + + ordered_keys = sorted(flatten(jsanitize(self.as_dict())).items(), key=lambda x: x[0]) + ordered_keys = [item for item in ordered_keys if '@' not in item[0]] + return sha1(json.dumps(OrderedDict(ordered_keys)).encode('utf-8')) # noqa: S324 diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/Objects/test_BaseObj.py index 67b91e82..96eea5b4 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/Objects/test_BaseObj.py @@ -279,12 +279,11 @@ def test_baseobj_dir(setup_pars): "par2", "par3", "switch_interface", - "as_data_dict", - "as_dict", - "unsafe_hash", "user_data", ] obtained = dir(obj) + print(obtained) + print(expected) assert len(obtained) == len(expected) assert obtained == sorted(obtained) assert len(set(expected).difference(set(obtained))) == 0 diff --git a/tests/unit_tests/Objects/test_Groups.py b/tests/unit_tests/Objects/test_Groups.py index b546373f..b6f835f9 100644 --- a/tests/unit_tests/Objects/test_Groups.py +++ b/tests/unit_tests/Objects/test_Groups.py @@ -316,7 +316,6 @@ def test_baseCollection_dir(cls): "extend", "encode", "remove", - "as_data_dict", "interface", "from_dict", "name", @@ -327,9 +326,7 @@ def test_baseCollection_dir(cls): "pop", "count", "generate_bindings", - "unsafe_hash", "decode", - "encode_data", "sort", } assert not d.difference(expected) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_any_type.py b/tests/unit_tests/Objects/variable/test_descriptor_any_type.py index 5b8a131b..3a906cf8 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_any_type.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_any_type.py @@ -75,18 +75,4 @@ def test_copy(self, descriptor: DescriptorAnyType): # Expect assert type(descriptor_copy) == DescriptorAnyType - assert descriptor_copy._value == descriptor._value - - def test_as_data_dict(self, clear, descriptor: DescriptorAnyType): - # When Then - descriptor_dict = descriptor.as_data_dict() - - # Expect - assert descriptor_dict == { - "name": "name", - "value": "string", - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorAnyType_0" - } \ No newline at end of file + assert descriptor_copy._value == descriptor._value \ No newline at end of file diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 2708f4ed..7e543ecf 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -218,32 +218,6 @@ def test_copy(self, descriptor: DescriptorArray): assert type(descriptor_copy) == DescriptorArray assert np.array_equal(descriptor_copy._array.values, descriptor._array.values) assert descriptor_copy._array.unit == descriptor._array.unit - - def test_as_data_dict(self, clear, descriptor: DescriptorArray): - # When - descriptor_dict = descriptor.as_data_dict() - - # Expected dictionary - expected_dict = { - "name": "name", - "value": np.array([[1.0, 2.0], [3.0, 4.0]]), # Use numpy array for comparison - "unit": "m", - "variance": np.array([[0.1, 0.2], [0.3, 0.4]]), # Use numpy array for comparison - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorArray_0", - "dimensions": np.array(['dim0', 'dim1']), # Use numpy array for comparison - } - - # Then: Compare dictionaries key by key - for key, expected_value in expected_dict.items(): - if isinstance(expected_value, np.ndarray): - # Compare numpy arrays - assert np.array_equal(descriptor_dict[key], expected_value), f"Mismatch for key: {key}" - else: - # Compare other values directly - assert descriptor_dict[key] == expected_value, f"Mismatch for key: {key}" @pytest.mark.parametrize("unit_string, expected", [ ("1e+9", "dimensionless"), diff --git a/tests/unit_tests/Objects/variable/test_descriptor_base.py b/tests/unit_tests/Objects/variable/test_descriptor_base.py index 38140a34..4e444358 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_base.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_base.py @@ -140,19 +140,6 @@ def test_copy(self, descriptor: DescriptorBase): assert descriptor_copy._url == descriptor._url assert descriptor_copy._display_name == descriptor._display_name - def test_as_data_dict(self, clear, descriptor: DescriptorBase): - # When Then - descriptor_dict = descriptor.as_data_dict() - - # Expect - assert descriptor_dict == { - "name": "name", - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorBase_0", - } - def test_unique_name_generator(self, clear, descriptor: DescriptorBase): # When second_descriptor = DescriptorBase(name="test", unique_name="DescriptorBase_2") diff --git a/tests/unit_tests/Objects/variable/test_descriptor_bool.py b/tests/unit_tests/Objects/variable/test_descriptor_bool.py index 63bf484a..09440074 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_bool.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_bool.py @@ -74,18 +74,4 @@ def test_copy(self, descriptor: DescriptorBool): # Expect assert type(descriptor_copy) == DescriptorBool - assert descriptor_copy._bool_value == descriptor._bool_value - - def test_as_data_dict(self, clear, descriptor: DescriptorBool): - # When Then - descriptor_dict = descriptor.as_data_dict() - - # Expect - assert descriptor_dict == { - "name": "name", - "value": True, - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorBool_0" - } \ No newline at end of file + assert descriptor_copy._bool_value == descriptor._bool_value \ No newline at end of file diff --git a/tests/unit_tests/Objects/variable/test_descriptor_number.py b/tests/unit_tests/Objects/variable/test_descriptor_number.py index b9de196a..ecd52408 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_number.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_number.py @@ -200,22 +200,6 @@ def test_copy(self, descriptor: DescriptorNumber): assert descriptor_copy._scalar.value == descriptor._scalar.value assert descriptor_copy._scalar.unit == descriptor._scalar.unit - def test_as_data_dict(self, clear, descriptor: DescriptorNumber): - # When Then - descriptor_dict = descriptor.as_data_dict() - - # Expect - assert descriptor_dict == { - "name": "name", - "value": 1.0, - "unit": "m", - "variance": 0.1, - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorNumber_0", - } - @pytest.mark.parametrize("unit_string, expected", [ ("1e+9", "dimensionless"), ("1000", "dimensionless"), diff --git a/tests/unit_tests/Objects/variable/test_descriptor_str.py b/tests/unit_tests/Objects/variable/test_descriptor_str.py index 71c50715..5ad903e3 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_str.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_str.py @@ -73,18 +73,4 @@ def test_copy(self, descriptor: DescriptorStr): # Expect assert type(descriptor_copy) == DescriptorStr - assert descriptor_copy._string == descriptor._string - - def test_as_data_dict(self, clear, descriptor: DescriptorStr): - # When Then - descriptor_dict = descriptor.as_data_dict() - - # Expect - assert descriptor_dict == { - "name": "name", - "value": "string", - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "DescriptorStr_0" - } \ No newline at end of file + assert descriptor_copy._string == descriptor._string \ No newline at end of file diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py index 78e19da0..0deefb29 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -662,26 +662,6 @@ def test_copy(self, parameter: Parameter): assert parameter_copy._display_name == parameter._display_name assert parameter_copy._independent == parameter._independent - def test_as_data_dict(self, clear, parameter: Parameter): - # When Then - self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value - parameter_dict = parameter.as_data_dict() - - # Expect - assert parameter_dict == { - "name": "name", - "value": 1.0, - "unit": "m", - "variance": 0.01, - "min": 0, - "max": 10, - "fixed": False, - "description": "description", - "url": "url", - "display_name": "display_name", - "unique_name": "Parameter_0", - } - @pytest.mark.parametrize("test, expected, expected_reverse", [ (Parameter("test", 2, "m", 0.01, -10, 20), Parameter("name + test", 3, "m", 0.02, -10, 30), Parameter("test + name", 3, "m", 0.02, -10, 30)), (Parameter("test", 2, "m", 0.01), Parameter("name + test", 3, "m", 0.02, min=-np.inf, max=np.inf),Parameter("test + name", 3, "m", 0.02, min=-np.inf, max=np.inf)), diff --git a/tests/unit_tests/utils/io_tests/test_core.py b/tests/unit_tests/utils/io_tests/test_core.py index 2083ac3c..c0625af6 100644 --- a/tests/unit_tests/utils/io_tests/test_core.py +++ b/tests/unit_tests/utils/io_tests/test_core.py @@ -97,27 +97,3 @@ def test_variable_as_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber check_dict(dp_kwargs, enc) - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_as_data_dict_methods(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - if isinstance(skip, str): - del data_dict[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc_d = obj.as_data_dict(skip=skip) - - expected_keys = set(data_dict.keys()) - obtained_keys = set(enc_d.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(data_dict, enc_d) diff --git a/tests/unit_tests/utils/io_tests/test_dict.py b/tests/unit_tests/utils/io_tests/test_dict.py index a9b8ccd4..b9fd0083 100644 --- a/tests/unit_tests/utils/io_tests/test_dict.py +++ b/tests/unit_tests/utils/io_tests/test_dict.py @@ -88,34 +88,6 @@ def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNum check_dict(data_dict, enc_d) -@pytest.mark.parametrize( - "encoder", [None, DataDictSerializer], ids=["Default", "DataDictSerializer"] -) -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_encode_data(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip, encoder): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - if isinstance(skip, str): - del data_dict[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc_d = obj.encode_data(skip=skip, encoder=encoder) - - expected_keys = set(data_dict.keys()) - obtained_keys = set(enc_d.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(data_dict, enc_d) - - ######################################################################################################################## # TESTING DECODING ######################################################################################################################## From 915e39f3d53f7bf05ac1c2cebff326006bf2668a Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 14:36:16 +0200 Subject: [PATCH 22/58] rename core.py to component_serializer.py --- examples_old/example4.py | 2 +- examples_old/example5_broken.py | 2 +- examples_old/example6_broken.py | 2 +- src/easyscience/Objects/ObjectClasses.py | 2 +- src/easyscience/Objects/{core.py => component_serializer.py} | 0 src/easyscience/Objects/variable/descriptor_base.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/easyscience/Objects/{core.py => component_serializer.py} (100%) diff --git a/examples_old/example4.py b/examples_old/example4.py index 3b01387a..720ff961 100644 --- a/examples_old/example4.py +++ b/examples_old/example4.py @@ -11,7 +11,7 @@ from easyscience import global_object from easyscience.fitting import Fitter -from easyscience.Objects.core import ComponentSerializer +from easyscience.Objects.component_serializer import ComponentSerializer from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.ObjectClasses import Parameter diff --git a/examples_old/example5_broken.py b/examples_old/example5_broken.py index de12e67a..a7967ee8 100644 --- a/examples_old/example5_broken.py +++ b/examples_old/example5_broken.py @@ -12,7 +12,7 @@ from easyscience.fitting import Fitter from easyscience.Objects.Base import BaseObj from easyscience.Objects.Base import Parameter -from easyscience.Objects.core import ComponentSerializer +from easyscience.Objects.component_serializer import ComponentSerializer # from easyscience.Objects.Base import LoggedProperty from easyscience.Objects.Inferface import InterfaceFactoryTemplate diff --git a/examples_old/example6_broken.py b/examples_old/example6_broken.py index c93f7577..fe2dbbf2 100644 --- a/examples_old/example6_broken.py +++ b/examples_old/example6_broken.py @@ -12,7 +12,7 @@ from easyscience.fitting import Fitter from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.Variable import Parameter -from easyscience.Objects.core import ComponentSerializer +from easyscience.Objects.component_serializer import ComponentSerializer from easyscience.Objects.Inferface import InterfaceFactoryTemplate # This is a much more complex case where we have calculators, interfaces, interface factory and an diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/Objects/ObjectClasses.py index 376162c5..403f5313 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/Objects/ObjectClasses.py @@ -15,7 +15,7 @@ from easyscience import global_object from easyscience.Utils.classTools import addLoggedProp -from .core import ComponentSerializer +from .component_serializer import ComponentSerializer from .variable import Parameter from .variable.descriptor_base import DescriptorBase diff --git a/src/easyscience/Objects/core.py b/src/easyscience/Objects/component_serializer.py similarity index 100% rename from src/easyscience/Objects/core.py rename to src/easyscience/Objects/component_serializer.py diff --git a/src/easyscience/Objects/variable/descriptor_base.py b/src/easyscience/Objects/variable/descriptor_base.py index b80065a2..04c77697 100644 --- a/src/easyscience/Objects/variable/descriptor_base.py +++ b/src/easyscience/Objects/variable/descriptor_base.py @@ -10,7 +10,7 @@ from easyscience import global_object from easyscience.global_object.undo_redo import property_stack -from easyscience.Objects.core import ComponentSerializer +from easyscience.Objects.component_serializer import ComponentSerializer class DescriptorBase(ComponentSerializer, metaclass=abc.ABCMeta): From c3a6769ce6e6075cca05f8da8510796e46c5f015 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 15:46:09 +0200 Subject: [PATCH 23/58] Finally remove xarray --- docs/src/conf.py | 3 +- docs/src/reference/base.rst | 13 - pyproject.toml | 1 - src/easyscience/Datasets/__init__.py | 6 - src/easyscience/Datasets/xarray.py | 823 ------------------------ src/easyscience/Objects/job/analysis.py | 9 +- 6 files changed, 5 insertions(+), 850 deletions(-) delete mode 100644 src/easyscience/Datasets/__init__.py delete mode 100644 src/easyscience/Datasets/xarray.py diff --git a/docs/src/conf.py b/docs/src/conf.py index 2d95445a..f56f26f8 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -52,8 +52,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'numpy': ('https://numpy.org/doc/stable/', None), - 'pint': ('https://pint.readthedocs.io/en/stable/', None), - 'xarray': ('https://xarray.pydata.org/en/stable/', None) + 'pint': ('https://pint.readthedocs.io/en/stable/', None) } # -- General configuration --------------------------------------------------- diff --git a/docs/src/reference/base.rst b/docs/src/reference/base.rst index 59e9de32..62fb8fe1 100644 --- a/docs/src/reference/base.rst +++ b/docs/src/reference/base.rst @@ -36,16 +36,3 @@ Collections .. autoclass:: easyscience.Objects.Groups.BaseCollection :members: :inherited-members: - -=============== -Data Containers -=============== - -.. autoclass:: easyscience.Datasets.xarray.EasyScienceDataarrayAccessor - :members: - :inherited-members: - -.. autoclass:: easyscience.Datasets.xarray.EasyScienceDatasetAccessor - :members: - :inherited-members: - diff --git a/pyproject.toml b/pyproject.toml index 734444b8..86d0fdcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ dependencies = [ "lmfit", "numpy", "uncertainties", - "xarray", "pint", # Only to ensure that unit is reported as dimensionless rather than empty string "scipp" ] diff --git a/src/easyscience/Datasets/__init__.py b/src/easyscience/Datasets/__init__.py deleted file mode 100644 index 22e236a6..00000000 --- a/src/easyscience/Datasets/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project str: - """ - Get the common name of the DataSet. - - :return: Common name of the DataSet - :rtype: str - """ - return self._obj.attrs['name'] - - @name.setter - def name(self, new_name: str): - """ - Set the common name of the DataSet i.e could be experiment name... - - :param new_name: Common name of the DataSet - :type new_name: str - :return: None - :rtype: None - """ - self._obj.attrs['name'] = new_name - - @property - def description(self) -> str: - """ - Get a description of the DataSet - - :return: Description of the DataSet - :rtype: str - """ - return self._obj.attrs['description'] - - @description.setter - def description(self, new_description: str): - """ - Set the description of the DataSet - - :param new_description: Description of the DataSet - :type new_description: str - :return: None - :rtype: None - """ - self._obj.attrs['description'] = new_description - - @property - def url(self) -> str: - """ - Get the url of the DataSet - - :return: URL of the DataSet (empty if no URL) - :rtype: str - """ - return self._obj.attrs['url'] - - @url.setter - def url(self, new_url: str): - """ - Set the URL of the DataSet. This may be a DOI. - - :param new_url: New URL/DOI of the DataSet - :type new_url: str - :return:None - :rtype: None - """ - self._obj.attrs['url'] = new_url - - @property - def core_object(self): - """ - Get the core object associated to a DataSet. Note that this is called from a weakref. If the EasyScience obj is - garbage collected, None will be returned. - - :return: EasyScience object associated with the DataSet - :rtype: Any - """ - if self._core_object is None: - return None - return self._core_object() - - @core_object.setter - def core_object(self, new_core_object: Any): - """ - Associate an EasyScience object to a DataSet. - - :param new_core_object: EasyScience object to be associated to the DataSet - :type new_core_object: Any - :return: None - :rtype: None - """ - self._core_object = weakref.ref(new_core_object) - - def add_coordinate( - self, - coordinate_name: str, - coordinate_values: Union[List[T_], np.ndarray], - unit: str = '', - ): - """ - Add a coordinate to the DataSet. This can be then be assigned to one or more DataArrays. - - :param coordinate_name: Name of the coordinate e.g. `x` - :type coordinate_name: str - :param coordinate_values: Points for the coordinates - :type coordinate_values: Union[List[T_], numpy.ndarray] - :param unit: Unit associated with the coordinate - :type unit: str - :return: None - :rtype: None - """ - self._obj.coords[coordinate_name] = coordinate_values - self._obj.attrs['units'][coordinate_name] = ureg.Unit(unit) - - def remove_coordinate(self, coordinate_name: str): - """ - Remove a coordinate from the DataSet. Note that this will not remove the coordinate from DataArrays which have - already used the it! - - :param coordinate_name: Name of the coordinate to be removed - :type coordinate_name: str - :return: None - :rtype: None - """ - del self._obj.coords[coordinate_name] - del self._obj.attrs['units'][coordinate_name] - - def add_variable( - self, - variable_name, - variable_coordinates: Union[str, List[str]], - variable_values: Union[List[T_], np.ndarray], - variable_sigma: Union[List[T_], np.ndarray] = None, - unit: str = '', - auto_sigma: bool = False, - ): - """ - Create a DataArray from known coordinates and data, assign it to the dataset under a given name. Variances can - be calculated assuming gaussian distribution to 1 sigma. - - :param variable_name: Name of the DataArray which will be created and added to the dataset - :type variable_name: str - :param variable_coordinates: List of coordinates used in the supplied data array. - :type variable_coordinates: str, List[str] - :param variable_values: Numpy or list of data which will be assigned to the DataArray - :type variable_values: Union[numpy.ndarray, list] - :param variable_sigma: If the sigmas of the dataset are known, they can be supplied here. - :type variable_sigma: Union[numpy.ndarray, list] - :param unit: Unit associated with the DataArray - :type unit: str - :param auto_sigma: Should the sigma DataArray be automatically calculated assuming gaussian probability? - :type auto_sigma: bool - :return: None - :rtype: None - """ - - # Check if a user has supplied a coordinate as a string. Make it a list of strings - if isinstance(variable_coordinates, str): - variable_coordinates = [variable_coordinates] - - # The variable_coordinates can be any iterable object. Though we would assume list/tuple - if not isinstance(variable_coordinates, Iterable): - raise ValueError('The variable coordinates must be a list of strings') - - # Check to see if the user want to assign a coordinate which does not exist yet. - known_keys = self._obj.coords.keys() - for dimension in variable_coordinates: - if dimension not in known_keys: - raise ValueError(f'The supplied coordinate `{dimension}` must first be defined.') - - # Create the dataset. - self._obj[variable_name] = (variable_coordinates, variable_values) - - # Deal with sigmas - if variable_sigma is not None: - # CASE 1, user has supplied sigmas - if isinstance(variable_sigma, Callable): - # CASE 1-1, The sigmas are created by some kind of generator - self.sigma_generator(variable_name, variable_sigma) - elif isinstance(variable_sigma, np.ndarray): - # CASE 1-2, The sigmas are a numpy arrays - self.sigma_attach(variable_name, variable_sigma) - elif isinstance(variable_sigma, list): - # CASE 1-3, We have been given a list. Make it a numpy array - self.sigma_attach(variable_name, np.array(variable_sigma)) - else: - raise ValueError('User supplied sigmas must be of the form; Callable fn, numpy array, list') - else: - # CASE 2, No sigmas have been supplied. - if auto_sigma: - # CASE 2-1, Automatically generate the sigmas using gaussian probability - self.sigma_generator(variable_name) - - # Set units for the newly created DataArray - self._obj.attrs['units'][variable_name] = ureg.Unit(unit) - # If a sigma has been attached, attempt to work out the units. - if unit and variable_sigma is None and auto_sigma: - self._obj.attrs['units'][self.sigma_label_prefix + variable_name] = ureg.Unit(unit + ' ** 0.5') - else: - if auto_sigma: - self._obj.attrs['units'][self.sigma_label_prefix + variable_name] = ureg.Unit('') - - def remove_variable(self, variable_name: str): - """ - Remove a DataArray from the DataSet by supplied name. - - :param variable_name: Name of DataArray to be removed - :type variable_name: str - :return: None - :rtype: None - """ - del self._obj[variable_name] - - def sigma_generator( - self, - variable_label: str, - sigma_func: Callable = lambda x: np.sqrt(np.abs(x)), - label_prefix: str = None, - ): - """ - Generate sigmas off of a DataArray based on a function. - - :param variable_label: Name of the DataArray to perform the calculation on - :type variable_label: str - :param sigma_func: Function to generate the sigmas. Must be of the form f(x) and return an array of the same shape as the input. Default sqrt(\\|x\\|) - :type sigma_func: Callable - :param label_prefix: What prefix should be used to designate a sigma DataArray from a data DataArray - :type label_prefix: str - :return: None - :rtype: None - """ # noqa: E501 - sigma_values = sigma_func(self._obj[variable_label]) - self.sigma_attach(variable_label, sigma_values, label_prefix) - - def sigma_attach( - self, - variable_label: str, - sigma_values: Union[List[T_], np.ndarray, xr.DataArray], - label_prefix: str = None, - ): - """ - Attach an array of sigmas to the DataSet. - - :param variable_label: Name of the DataArray to perform the calculation on - :type variable_label: str - :param sigma_values: Array of sigmas in list, numpy or DataArray form - :type sigma_values: Union[List[T_], numpy.ndarray, xarray.DataArray] - :param label_prefix: What prefix should be used to designate a sigma DataArray from a data DataArray - :type label_prefix: str - :return: None - :rtype: None - """ - # Use the default sigma prefix if not defined. - if label_prefix is None: - label_prefix = self.sigma_label_prefix - - # Form the label for the new DataArray - sigma_label = label_prefix + variable_label - - # Map the original DataArray to the new sigma DataArray - self.__error_mapper[variable_label] = sigma_label - # Assign the sigma DataArray to the DataSet - if not isinstance(sigma_values, xr.DataArray): - self._obj[sigma_label] = ( - list(self._obj[variable_label].coords.keys()), - sigma_values, - ) - else: - self._obj[sigma_label] = sigma_values - - def generate_points(self, coordinates: List[str]) -> xr.DataArray: - """ - Generate an expanded DataArray of points which corresponds to broadcasted dimensions (`all_x`) which have been - concatenated along the second axis (`fit_dim`). - - :param coordinates: List of coordinate names to broadcast and concatenate along - :type coordinates: List[str] - :return: Broadcasted and concatenated coordinates - :rtype: xarray.DataArray - - .. code-block:: python - - x = [1, 2], y = [3, 4] - d = xr.DataArray() - d.EasyScience.add_coordinate('x', x) - d.EasyScience.add_coordinate('y', y) - points = d.EasyScience.generate_points(['x', 'y']) - print(points) - """ - - coords = [self._obj.coords[da] for da in coordinates] - c_array = [] - n_array = [] - for da in xr.broadcast(*coords): - c_array.append(da) - n_array.append(da.name) - - f = xr.concat(c_array, dim='fit_dim') - f = f.stack(all_x=n_array) - return f - - def fit( - self, - fitter, - data_arrays: list, - *args, - dask: str = 'forbidden', - fit_kwargs: dict = None, - fn_kwargs: dict = None, - vectorized: bool = False, - **kwargs, - ) -> List[FitResults]: - """ - Perform a fit on one or more DataArrays. This fit utilises a given fitter from `EasyScience.fitting.Fitter`, though - there are a few differences to a standard EasyScience fit. In particular, key-word arguments to control the - optimisation algorithm go in the `fit_kwargs` dictionary, fit function key-word arguments go in the `fn_kwargs` - and given key-word arguments control the `xarray.apply_ufunc` function. - - :param fitter: Fitting object which controls the fitting - :type fitter: EasyScience.fitting.Fitter - :param args: Arguments to go to the fit function - :type args: Any - :param dask: Dask control string. See `xarray.apply_ufunc` documentation - :type dask: str - :param fit_kwargs: Dictionary of key-word arguments to be supplied to the Fitting control - :type fit_kwargs: dict - :param fn_kwargs: Dictionary of key-words to be supplied to the fit function - :type fn_kwargs: dict - :param vectorized: Should the fit function be given dependents in a single object or split - :type vectorized: bool - :param kwargs: Key-word arguments for `xarray.apply_ufunc`. See `xarray.apply_ufunc` documentation - :type kwargs: Any - :return: Results of the fit - :rtype: List[FitResults] - """ - - if fn_kwargs is None: - fn_kwargs = {} - if fit_kwargs is None: - fit_kwargs = {} - if not isinstance(data_arrays, (list, tuple)): - data_arrays = [data_arrays] - - # In this case we are only fitting 1 dataset - if len(data_arrays) == 1: - variable_label = data_arrays[0] - dataset = self._obj[variable_label] - if self.__error_mapper.get(variable_label, False): - # Pull out any sigmas and send them to the fitter. - temp = self._obj[self.__error_mapper[variable_label]] - temp[xr.ufuncs.isnan(temp)] = 1e5 - fit_kwargs['weights'] = temp - # Perform a standard DataArray fit. - return dataset.EasyScience.fit( - fitter, - *args, - fit_kwargs=fit_kwargs, - fn_kwargs=fn_kwargs, - dask=dask, - vectorize=vectorized, - **kwargs, - ) - else: - # In this case we are fitting multiple datasets to the same fn! - bdim_f = [self._obj[p].EasyScience.fit_prep(fitter.fit_function) for p in data_arrays] - dim_names = [ - list(self._obj[p].dims.keys()) if isinstance(self._obj[p].dims, dict) else self._obj[p].dims - for p in data_arrays - ] - bdims = [bdim[0] for bdim in bdim_f] - fs = [bdim[1] for bdim in bdim_f] - old_fit_func = fitter.fit_function - - fn_array = [] - y_list = [] - for _idx, d in enumerate(bdims): - dims = self._obj[data_arrays[_idx]].dims - if isinstance(dims, dict): - dims = list(dims.keys()) - - def local_fit_func(x, *args, idx=None, **kwargs): - kwargs['vectorize'] = vectorized - res = xr.apply_ufunc( - fs[idx], - *bdims[idx], - *args, - dask=dask, - kwargs=fn_kwargs, - **kwargs, - ) - if dask != 'forbidden': - res.compute() - return res.stack(all_x=dim_names[idx]) - - y_list.append(self._obj[data_arrays[_idx]].stack(all_x=dims)) - fn_array.append(local_fit_func) - - def fit_func(x, *args, **kwargs): - res = [] - for idx in range(len(fn_array)): - res.append(fn_array[idx](x, *args, idx=idx, **kwargs)) - return xr.DataArray(np.concatenate(res, axis=0), coords={'all_x': x}, dims='all_x') - - fitter.initialize(fitter.fit_object, fit_func) - try: - if fit_kwargs.get('weights', None) is not None: - del fit_kwargs['weights'] - x = xr.DataArray(np.arange(np.sum([y.size for y in y_list])), dims='all_x') - y = xr.DataArray(np.concatenate(y_list, axis=0), coords={'all_x': x}, dims='all_x') - f_res = fitter.fit(x, y, **fit_kwargs) - f_res = check_sanity_multiple(f_res, [self._obj[p] for p in data_arrays]) - finally: - fitter.fit_function = old_fit_func - return f_res - - -@xr.register_dataarray_accessor('EasyScience') -class EasyScienceDataarrayAccessor: - """ - Accessor to extend an xarray DataArray to EasyScience. These functions can be accessed by `obj.EasyScience.func`. - - """ - - def __init__(self, xarray_obj: xr.DataArray): - self._obj = xarray_obj - self._core_object = None - self.sigma_label_prefix = 's_' - if self._obj.attrs.get('computation', None) is None: - self._obj.attrs['computation'] = { - 'precompute_func': None, - 'compute_func': None, - 'postcompute_func': None, - } - - def __empty_functional(self) -> Callable: - def outer(): - def empty_fn(input, *args, **kwargs): - return input - - return empty_fn - - class wrapper: - def __init__(obj): - obj.obj = self - obj.data = {} - obj.fn = outer() - - def __call__(self, *args, **kwargs): - return self.fn(*args, **kwargs) - - return wrapper() - - @property - def core_object(self): - """ - Get the core object associated to a DataArray. Note that this is called from a weakref. If the EasyScience obj is - garbage collected, None will be returned. - - :return: EasyScience object associated with the DataArray - :rtype: Any - """ - if self._core_object is None: - return None - return self._core_object() - - @core_object.setter - def core_object(self, new_core_object: Any): - """ - Set the core object associated to a dataset - - :param new_core_object: EasyScience object to be associated with the DataArray - :type new_core_object: Any - :return: None - :rtype: None - """ - self._core_object = weakref.ref(new_core_object) - - @property - def compute_func(self) -> Callable: - """ - Get the computational function which will be executed during a fit - - :return: Computational function applied to the DataArray - :rtype: Callable - """ - result = self._obj.attrs['computation']['compute_func'] - if result is None: - result = self.__empty_functional() - return result - - @compute_func.setter - def compute_func(self, new_computational_fn: Callable): - """ - Set the computational function which is called during a fit - - :param new_computational_fn: Computational function applied to the DataArray - :type new_computational_fn: Callable - :return: None - :rtype: None - """ - self._obj.attrs['computation']['compute_func'] = new_computational_fn - - @property - def precompute_func(self) -> Callable: - """ - Get the pre-computational function which will be executed before a fit - - :return: Computational function applied to the DataArray before fitting - :rtype: Callable - """ - result = self._obj.attrs['computation']['precompute_func'] - if result is None: - result = self.__empty_functional() - return result - - @precompute_func.setter - def precompute_func(self, new_computational_fn: Callable): - """ - Set the computational function which is called before a fit - - :param new_computational_fn: Computational function applied to the DataArray before fitting - :type new_computational_fn: Callable - :return: None - :rtype: None - """ - self._obj.attrs['computation']['precompute_func'] = new_computational_fn - - @property - def postcompute_func(self) -> Callable: - """ - Get the post-computational function which will be executed after a fit - - :return: Computational function applied to the DataArray after fitting - :rtype: Callable - """ - result = self._obj.attrs['computation']['postcompute_func'] - if result is None: - result = self.__empty_functional() - return result - - @postcompute_func.setter - def postcompute_func(self, new_computational_fn: Callable): - """ - Set the computational function which is called after a fit - - :param new_computational_fn: Computational function applied to the DataArray after fitting - :type new_computational_fn: Callable - :return: None - :rtype: None - """ - self._obj.attrs['computation']['postcompute_func'] = new_computational_fn - - def fit_prep(self, func_in: Callable, bdims=None, dask_chunks=None) -> Tuple[xr.DataArray, Callable]: - """ - Generate broadcasted coordinates for fitting and reform the fitting function into one which can handle xarrays. - - :param func_in: Function to be wrapped and made xarray fitting compatible. - :type func_in: Callable - :param bdims: Optional precomputed broadcasted dimensions. - :type bdims: xarray.DataArray - :param dask_chunks: How to split the broadcasted dimensions for dask. - :type dask_chunks: Tuple[int..] - :return: Tuple of broadcasted fit arrays and wrapped fit function. - :rtype: xarray.DataArray, Callable - """ - - if bdims is None: - coords = [self._obj.coords[da].transpose() for da in self._obj.dims] - bdims = xr.broadcast(*coords) - self._obj.attrs['computation']['compute_func'] = func_in - - def func(x, *args, vectorize: bool = False, **kwargs): - old_shape = x.shape - if not vectorize: - xs = [x_new.flatten() for x_new in [x, *args] if isinstance(x_new, np.ndarray)] - x_new = np.column_stack(xs) - if len(x_new.shape) > 1 and x_new.shape[1] == 1: - x_new = x_new.reshape((-1)) - result = self.compute_func(x_new, **kwargs) - else: - result = self.compute_func( - *[d for d in [x, args] if isinstance(d, np.ndarray)], - *[d for d in args if not isinstance(d, np.ndarray)], - **kwargs, - ) - if isinstance(result, np.ndarray): - result = result.reshape(old_shape) - result = self.postcompute_func(result) - return result - - return bdims, func - - def generate_points(self) -> xr.DataArray: - """ - Generate an expanded DataArray of points which corresponds to broadcasted dimensions (`all_x`) which have been - concatenated along the second axis (`fit_dim`). - - :return: Broadcasted and concatenated coordinates - :rtype: xarray.DataArray - """ - - coords = [self._obj.coords[da] for da in self._obj.dims] - c_array = [] - n_array = [] - for da in xr.broadcast(*coords): - c_array.append(da) - n_array.append(da.name) - - f = xr.concat(c_array, dim='fit_dim') - f = f.stack(all_x=n_array) - return f - - def fit( - self, - fitter, - *args, - fit_kwargs: dict = None, - fn_kwargs: dict = None, - vectorize: bool = False, - dask: str = 'forbidden', - **kwargs, - ) -> FitResults: - """ - Perform a fit on the given DataArray. This fit utilises a given fitter from `EasyScience.fitting.Fitter`, though - there are a few differences to a standard EasyScience fit. In particular, key-word arguments to control the - optimisation algorithm go in the `fit_kwargs` dictionary, fit function key-word arguments go in the `fn_kwargs` - and given key-word arguments control the `xarray.apply_ufunc` function. - - :param fitter: Fitting object which controls the fitting - :type fitter: EasyScience.fitting.Fitter - :param args: Arguments to go to the fit function - :type args: Any - :param dask: Dask control string. See `xarray.apply_ufunc` documentation - :type dask: str - :param fit_kwargs: Dictionary of key-word arguments to be supplied to the Fitting control - :type fit_kwargs: dict - :param fn_kwargs: Dictionary of key-words to be supplied to the fit function - :type fn_kwargs: dict - :param vectorize: Should the fit function be given dependents in a single object or split - :type vectorize: bool - :param kwargs: Key-word arguments for `xarray.apply_ufunc`. See `xarray.apply_ufunc` documentation - :type kwargs: Any - :return: Results of the fit - :rtype: FitResults - """ - - # Deal with any kwargs which has been given - if fn_kwargs is None: - fn_kwargs = {} - if fit_kwargs is None: - fit_kwargs = {} - old_fit_func = fitter.fit_function - - # Wrap and broadcast - bdims, f = self.fit_prep(fitter.fit_function) - dims = self._obj.dims - - # Find which coords we need - if isinstance(dims, dict): - dims = list(dims.keys()) - - # Wrap the wrap in a callable - def local_fit_func(x, *args, **kwargs): - """ - Function which will be called by the fitter. This will deal with sending the function the correct data. - """ - kwargs['vectorize'] = vectorize - res = xr.apply_ufunc(f, *bdims, *args, dask=dask, kwargs=fn_kwargs, **kwargs) - if dask != 'forbidden': - res.compute() - return res.stack(all_x=dims) - - # Set the new callable to the fitter and initialize - fitter.initialize(fitter.fit_object, local_fit_func) - # Make EasyScience.fitting.Fitter compatible `x` - x_for_fit = xr.concat(bdims, dim='fit_dim') - x_for_fit = x_for_fit.stack(all_x=[d.name for d in bdims]) - try: - # Deal with any sigmas if supplied - if fit_kwargs.get('weights', None) is not None: - fit_kwargs['weights'] = xr.DataArray( - np.array(fit_kwargs['weights']), - dims=['all_x'], - coords={'all_x': x_for_fit.all_x}, - ) - # Try to perform a fit - f_res = fitter.fit(x_for_fit, self._obj.stack(all_x=dims), **fit_kwargs) - f_res = check_sanity_single(f_res) - finally: - # Reset the fit function on the fitter to the old fit function. - fitter.fit_function = old_fit_func - return f_res - - -def check_sanity_single(fit_results: FitResults) -> FitResults: - """ - Convert the FitResults from a fitter compatible state to a recognizable DataArray state. - - :param fit_results: Results of a fit to be modified - :type fit_results: FitResults - :return: Modified fit results - :rtype: FitResults - """ - items = ['y_obs', 'y_calc', 'residual'] - - for item in items: - array = getattr(fit_results, item) - if isinstance(array, xr.DataArray): - array = array.unstack() - array.name = item - setattr(fit_results, item, array) - - x_array = fit_results.x - if isinstance(x_array, xr.DataArray): - fit_results.x.name = 'axes_broadcast' - x_array = x_array.unstack() - x_dataset = xr.Dataset() - dims = [dims for dims in x_array.dims if dims != 'fit_dim'] - for idx, dim in enumerate(dims): - x_dataset[dim + '_broadcast'] = x_array[idx] - x_dataset[dim + '_broadcast'].name = dim + '_broadcast' - fit_results.x_matrices = x_dataset - else: - fit_results.x_matrices = x_array - return fit_results - - -def check_sanity_multiple(fit_results: FitResults, originals: List[xr.DataArray]) -> List[FitResults]: - """ - Convert the multifit FitResults from a fitter compatible state to a list of recognizable DataArray states. - - :param fit_results: Results of a fit to be modified - :type fit_results: FitResults - :param originals: List of DataArrays which were fitted against, so we can resize and re-chunk the results - :type originals: List[xr.DataArray] - :return: Modified fit results - :rtype: List[FitResults] - """ - - return_results = [] - offset = 0 - for item in originals: - current_results = fit_results.__class__() - # Fill out the basic stuff.... - current_results.engine_result = fit_results.engine_result - current_results.minimizer_engine = fit_results.minimizer_engine - current_results.success = fit_results.success - current_results.p = fit_results.p - current_results.p0 = fit_results.p0 - # now the tricky stuff - current_results.x = item.EasyScience.generate_points() - current_results.y_obs = item.copy() - current_results.y_obs.name = f'{item.name}_obs' - current_results.y_calc = xr.DataArray( - fit_results.y_calc[offset : offset + item.size].data, - dims=item.dims, - coords=item.coords, - name=f'{item.name}_calc', - ) - offset += item.size - current_results.residual = current_results.y_calc - current_results.y_obs - current_results.residual.name = f'{item.name}_residual' - return_results.append(current_results) - return return_results diff --git a/src/easyscience/Objects/job/analysis.py b/src/easyscience/Objects/job/analysis.py index 1d99ece1..9766a45a 100644 --- a/src/easyscience/Objects/job/analysis.py +++ b/src/easyscience/Objects/job/analysis.py @@ -7,7 +7,6 @@ import numpy as np -from easyscience.Datasets.xarray import xr # type: ignore from easyscience.fitting.minimizers import MinimizerBase from easyscience.Objects.ObjectClasses import BaseObj @@ -26,15 +25,15 @@ def __init__(self, name: str, interface=None, *args, **kwargs): @abstractmethod def calculate_theory(self, - x: Union[xr.DataArray, np.ndarray], + x: np.ndarray, **kwargs) -> np.ndarray: raise NotImplementedError("calculate_theory not implemented") @abstractmethod def fit(self, - x: Union[xr.DataArray, np.ndarray], - y: Union[xr.DataArray, np.ndarray], - e: Union[xr.DataArray, np.ndarray], + x: np.ndarray, + y: np.ndarray, + e: np.ndarray, **kwargs) -> None: raise NotImplementedError("fit not implemented") From f62526c22e01d64ee580c08fe5c3cbad41099816 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 15:52:02 +0200 Subject: [PATCH 24/58] finally remove pint --- docs/src/conf.py | 3 +-- pyproject.toml | 1 - src/easyscience/__init__.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index f56f26f8..9bf23650 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -51,8 +51,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'pint': ('https://pint.readthedocs.io/en/stable/', None) + 'numpy': ('https://numpy.org/doc/stable/', None) } # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 86d0fdcf..bb9103e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ dependencies = [ "lmfit", "numpy", "uncertainties", - "pint", # Only to ensure that unit is reported as dimensionless rather than empty string "scipp" ] diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index 4913e0a9..c046d2af 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -1,11 +1,8 @@ import warnings -import pint - from .global_object import GlobalObject # Must be executed before any other imports -ureg = pint.UnitRegistry() global_object = GlobalObject() global_object.instantiate_stack() global_object.stack.enabled = False From 05252b877209aab15d93cb1aa0c37343e17294fa Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 15:55:31 +0200 Subject: [PATCH 25/58] remove borg alias for global_object --- src/easyscience/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index c046d2af..534ba11b 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -16,13 +16,3 @@ AvailableMinimizers, global_object, ] - - -# alias for global_object, remove later -def __getattr__(name): - if name == 'borg': - warnings.warn( - "The 'borg' has been renamed to 'global_object', this alias will be deprecated in the future", DeprecationWarning - ) # noqa: E501 - print("The 'borg' has been renamed to 'global_object', this alias will be deprecated in the future") - return global_object From 4abb6c53ddc21ef4f733313ac648a884028fba36 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 16:18:55 +0200 Subject: [PATCH 26/58] move io folder one level up --- src/easyscience/Objects/ObjectClasses.py | 2 +- src/easyscience/Objects/component_serializer.py | 4 ++-- src/easyscience/Objects/job/analysis.py | 1 - src/easyscience/Utils/__init__.py | 5 +---- src/easyscience/__init__.py | 2 -- src/easyscience/global_object/global_object.py | 6 +----- src/easyscience/global_object/hugger/hugger.py | 2 +- src/easyscience/{Utils => }/io/__init__.py | 3 --- src/easyscience/{Utils => }/io/dict.py | 2 +- src/easyscience/{Utils => }/io/json.py | 2 +- src/easyscience/{Utils => }/io/template.py | 3 --- src/easyscience/{Utils => }/io/xml.py | 9 +++------ src/easyscience/legacy/legacy_core.py | 8 ++++---- tests/unit_tests/Objects/test_BaseObj.py | 2 +- tests/unit_tests/utils/io_tests/test_dict.py | 4 ++-- tests/unit_tests/utils/io_tests/test_json.py | 4 ++-- tests/unit_tests/utils/io_tests/test_xml.py | 2 +- 17 files changed, 21 insertions(+), 40 deletions(-) rename src/easyscience/{Utils => }/io/__init__.py (79%) rename src/easyscience/{Utils => }/io/dict.py (98%) rename src/easyscience/{Utils => }/io/json.py (98%) rename src/easyscience/{Utils => }/io/template.py (99%) rename src/easyscience/{Utils => }/io/xml.py (96%) diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/Objects/ObjectClasses.py index 403f5313..6b65d694 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/Objects/ObjectClasses.py @@ -13,8 +13,8 @@ from typing import TypeVar from easyscience import global_object -from easyscience.Utils.classTools import addLoggedProp +from ..utils.classTools import addLoggedProp from .component_serializer import ComponentSerializer from .variable import Parameter from .variable.descriptor_base import DescriptorBase diff --git a/src/easyscience/Objects/component_serializer.py b/src/easyscience/Objects/component_serializer.py index a765a672..259d490c 100644 --- a/src/easyscience/Objects/component_serializer.py +++ b/src/easyscience/Objects/component_serializer.py @@ -10,10 +10,10 @@ from typing import List from typing import Optional -from easyscience.Utils.io.dict import DictSerializer +from ..io.dict import DictSerializer if TYPE_CHECKING: - from easyscience.Utils.io.template import EC + from ..io.template import EC class ComponentSerializer: diff --git a/src/easyscience/Objects/job/analysis.py b/src/easyscience/Objects/job/analysis.py index 9766a45a..ff01009f 100644 --- a/src/easyscience/Objects/job/analysis.py +++ b/src/easyscience/Objects/job/analysis.py @@ -3,7 +3,6 @@ # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project str: """ Returns a json string representation of the ComponentSerializer object. """ - from easyscience.Utils.io.dict import DataDictSerializer + from .dict import DataDictSerializer ENCODER = type( JsonEncoderTemplate.__name__, diff --git a/src/easyscience/Utils/io/template.py b/src/easyscience/io/template.py similarity index 99% rename from src/easyscience/Utils/io/template.py rename to src/easyscience/io/template.py index 031d3466..2deb00f0 100644 --- a/src/easyscience/Utils/io/template.py +++ b/src/easyscience/io/template.py @@ -4,9 +4,6 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - import datetime import json from abc import abstractmethod diff --git a/src/easyscience/Utils/io/xml.py b/src/easyscience/io/xml.py similarity index 96% rename from src/easyscience/Utils/io/xml.py rename to src/easyscience/io/xml.py index 7179b259..20f13a59 100644 --- a/src/easyscience/Utils/io/xml.py +++ b/src/easyscience/io/xml.py @@ -4,9 +4,6 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - import sys import xml.etree.ElementTree as ET from numbers import Number @@ -17,9 +14,9 @@ import numpy as np -from easyscience.Utils.io.dict import DataDictSerializer -from easyscience.Utils.io.dict import DictSerializer -from easyscience.Utils.io.template import BaseEncoderDecoder +from .dict import DataDictSerializer +from .dict import DictSerializer +from .template import BaseEncoderDecoder if TYPE_CHECKING: from easyscience.Objects.ObjectClasses import BV diff --git a/src/easyscience/legacy/legacy_core.py b/src/easyscience/legacy/legacy_core.py index 0754040f..a0471bec 100644 --- a/src/easyscience/legacy/legacy_core.py +++ b/src/easyscience/legacy/legacy_core.py @@ -16,12 +16,12 @@ from typing import List from typing import Optional -from easyscience.Utils.io.dict import DataDictSerializer -from easyscience.Utils.io.dict import DictSerializer -from easyscience.Utils.io.json import jsanitize +from ..io.dict import DataDictSerializer +from ..io.dict import DictSerializer +from ..io.json import jsanitize if TYPE_CHECKING: - from easyscience.Utils.io.template import EC + from ..io.template import EC class ComponentSerializer: diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/Objects/test_BaseObj.py index 96eea5b4..8e8d1cfd 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/Objects/test_BaseObj.py @@ -21,7 +21,7 @@ from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.variable import DescriptorNumber from easyscience.Objects.variable import Parameter -from easyscience.Utils.io.dict import DictSerializer +from easyscience.io.dict import DictSerializer from easyscience import global_object @pytest.fixture diff --git a/tests/unit_tests/utils/io_tests/test_dict.py b/tests/unit_tests/utils/io_tests/test_dict.py index b9fd0083..1e7ce928 100644 --- a/tests/unit_tests/utils/io_tests/test_dict.py +++ b/tests/unit_tests/utils/io_tests/test_dict.py @@ -6,8 +6,8 @@ import pytest -from easyscience.Utils.io.dict import DataDictSerializer -from easyscience.Utils.io.dict import DictSerializer +from easyscience.io.dict import DataDictSerializer +from easyscience.io.dict import DictSerializer from easyscience.Objects.variable import DescriptorNumber from easyscience.Objects.ObjectClasses import BaseObj diff --git a/tests/unit_tests/utils/io_tests/test_json.py b/tests/unit_tests/utils/io_tests/test_json.py index 54f9ccb9..6d08f8e4 100644 --- a/tests/unit_tests/utils/io_tests/test_json.py +++ b/tests/unit_tests/utils/io_tests/test_json.py @@ -7,8 +7,8 @@ import pytest -from easyscience.Utils.io.json import JsonDataSerializer -from easyscience.Utils.io.json import JsonSerializer +from easyscience.io.json import JsonDataSerializer +from easyscience.io.json import JsonSerializer from easyscience.Objects.variable import DescriptorNumber from .test_core import check_dict diff --git a/tests/unit_tests/utils/io_tests/test_xml.py b/tests/unit_tests/utils/io_tests/test_xml.py index b382bf89..490b702d 100644 --- a/tests/unit_tests/utils/io_tests/test_xml.py +++ b/tests/unit_tests/utils/io_tests/test_xml.py @@ -8,7 +8,7 @@ import pytest -from easyscience.Utils.io.xml import XMLSerializer +from easyscience.io.xml import XMLSerializer from easyscience.Objects.variable import DescriptorNumber from .test_core import dp_param_dict From dff10256c99e721b2777bbd801dae94559511b58 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 1 May 2025 16:30:32 +0200 Subject: [PATCH 27/58] Remove ridiculous test --- src/easyscience/models/polynomial.py | 3 -- tests/unit_tests/Objects/test_BaseObj.py | 44 ------------------------ 2 files changed, 47 deletions(-) diff --git a/src/easyscience/models/polynomial.py b/src/easyscience/models/polynomial.py index 76689c01..cc61f6f1 100644 --- a/src/easyscience/models/polynomial.py +++ b/src/easyscience/models/polynomial.py @@ -2,9 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Date: Fri, 2 May 2025 13:40:44 +0200 Subject: [PATCH 28/58] Re-organize source code folders to new structure --- src/easyscience/Objects/__init__.py | 6 - src/easyscience/REDIRECT.py | 9 - src/easyscience/Utils/Exceptions.py | 4 - src/easyscience/Utils/classTools.py | 7 +- src/easyscience/Utils/classUtils.py | 3 - src/easyscience/Utils/decorators.py | 3 - src/easyscience/Utils/string.py | 4 - src/easyscience/__init__.py | 4 + src/easyscience/base_classes/__init__.py | 9 + src/easyscience/base_classes/base_obj.py | 167 ++++++++++++++++++ .../based_base.py} | 157 +--------------- .../component_serializer.py | 0 .../{Objects/Groups.py => base_collection.py} | 12 +- .../Inferface.py => interface_factory.py} | 3 - src/easyscience/io/dict.py | 2 +- src/easyscience/io/json.py | 2 +- src/easyscience/io/template.py | 2 +- src/easyscience/io/xml.py | 2 +- src/easyscience/{Objects => }/job/__init__.py | 0 src/easyscience/{Objects => }/job/analysis.py | 4 +- .../{Objects => }/job/experiment.py | 2 +- src/easyscience/{Objects => }/job/job.py | 8 +- .../{Objects => }/job/theoreticalmodel.py | 2 +- src/easyscience/models/__init__.py | 5 +- src/easyscience/models/polynomial.py | 6 +- .../{Objects => }/variable/__init__.py | 0 .../variable/descriptor_any_type.py | 0 .../variable/descriptor_array.py | 0 .../{Objects => }/variable/descriptor_base.py | 0 .../{Objects => }/variable/descriptor_bool.py | 0 .../variable/descriptor_number.py | 0 .../{Objects => }/variable/descriptor_str.py | 0 .../{Objects => }/variable/parameter.py | 0 33 files changed, 206 insertions(+), 217 deletions(-) delete mode 100644 src/easyscience/Objects/__init__.py delete mode 100644 src/easyscience/REDIRECT.py create mode 100644 src/easyscience/base_classes/__init__.py create mode 100644 src/easyscience/base_classes/base_obj.py rename src/easyscience/{Objects/ObjectClasses.py => base_classes/based_base.py} (53%) rename src/easyscience/{Objects => base_classes}/component_serializer.py (100%) rename src/easyscience/{Objects/Groups.py => base_collection.py} (97%) rename src/easyscience/{Objects/Inferface.py => interface_factory.py} (99%) rename src/easyscience/{Objects => }/job/__init__.py (100%) rename src/easyscience/{Objects => }/job/analysis.py (94%) rename src/easyscience/{Objects => }/job/experiment.py (91%) rename src/easyscience/{Objects => }/job/job.py (90%) rename src/easyscience/{Objects => }/job/theoreticalmodel.py (93%) rename src/easyscience/{Objects => }/variable/__init__.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_any_type.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_array.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_base.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_bool.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_number.py (100%) rename src/easyscience/{Objects => }/variable/descriptor_str.py (100%) rename src/easyscience/{Objects => }/variable/parameter.py (100%) diff --git a/src/easyscience/Objects/__init__.py b/src/easyscience/Objects/__init__.py deleted file mode 100644 index 22e236a6..00000000 --- a/src/easyscience/Objects/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project None: diff --git a/src/easyscience/Utils/classUtils.py b/src/easyscience/Utils/classUtils.py index 0b405fa5..df00895c 100644 --- a/src/easyscience/Utils/classUtils.py +++ b/src/easyscience/Utils/classUtils.py @@ -2,9 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project None: + """ + Dynamically add a component to the class. This is an internal method, though can be called remotely. + The recommended alternative is to use typing, i.e. + + class Foo(Bar): + def __init__(self, foo: Parameter, bar: Parameter): + super(Foo, self).__init__(bar=bar) + self._add_component("foo", foo) + + Goes to: + class Foo(Bar): + foo: ClassVar[Parameter] + def __init__(self, foo: Parameter, bar: Parameter): + super(Foo, self).__init__(bar=bar) + self.foo = foo + + :param key: Name of component to be added + :param component: Component to be added + :return: None + """ + self._kwargs[key] = component + self._global_object.map.add_edge(self, component) + self._global_object.map.reset_type(component, 'created_internal') + addLoggedProp( + self, + key, + self.__getter(key), + self.__setter(key), + get_id=key, + my_self=self, + test_class=BaseObj, + ) + + def __setattr__(self, key: str, value: BV) -> None: + # Assume that the annotation is a ClassVar + old_obj = None + if ( + hasattr(self.__class__, '__annotations__') + and key in self.__class__.__annotations__ + and hasattr(self.__class__.__annotations__[key], '__args__') + and issubclass( + getattr(value, '__old_class__', value.__class__), + self.__class__.__annotations__[key].__args__, + ) + ): + if issubclass(type(getattr(self, key, None)), (BasedBase, DescriptorBase)): + old_obj = self.__getattribute__(key) + self._global_object.map.prune_vertex_from_edge(self, old_obj) + self._add_component(key, value) + else: + if hasattr(self, key) and issubclass(type(value), (BasedBase, DescriptorBase)): + old_obj = self.__getattribute__(key) + self._global_object.map.prune_vertex_from_edge(self, old_obj) + self._global_object.map.add_edge(self, value) + super(BaseObj, self).__setattr__(key, value) + # Update the interface bindings if something changed (BasedBase and Descriptor) + if old_obj is not None: + old_interface = getattr(self, 'interface', None) + if old_interface is not None: + self.generate_bindings() + + def __repr__(self) -> str: + return f"{self.__class__.__name__} `{getattr(self, 'name')}`" + + @staticmethod + def __getter(key: str) -> Callable[[BV], BV]: + def getter(obj: BV) -> BV: + return obj._kwargs[key] + + return getter + + @staticmethod + def __setter(key: str) -> Callable[[BV], None]: + def setter(obj: BV, value: float) -> None: + if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( + value.__class__, (DescriptorBase) + ): + obj._kwargs[key].value = value + else: + obj._kwargs[key] = value + + return setter + + # @staticmethod + # def __setter(key: str) -> Callable[[Union[B, V]], None]: + # def setter(obj: Union[V, B], value: float) -> None: + # if issubclass(obj._kwargs[key].__class__, Descriptor): + # if issubclass(obj._kwargs[key].__class__, Descriptor): + # obj._kwargs[key] = value + # else: + # obj._kwargs[key].value = value + # else: + # obj._kwargs[key] = value + # + # return setter diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/base_classes/based_base.py similarity index 53% rename from src/easyscience/Objects/ObjectClasses.py rename to src/easyscience/base_classes/based_base.py index 6b65d694..35cf76ba 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/base_classes/based_base.py @@ -5,7 +5,6 @@ # © 2021-2023 Contributors to the EasyScience project BasedBase: return new_obj -if TYPE_CHECKING: - B = TypeVar('B', bound=BasedBase) - BV = TypeVar('BV', bound=ComponentSerializer) - - -class BaseObj(BasedBase): - """ - This is the base class for which all higher level classes are built off of. - NOTE: This object is serializable only if parameters are supplied as: - `BaseObj(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can - cheat with `BaseObj(*[Descriptor(...), Parameter(...), ...])`. - """ - - def __init__( - self, - name: str, - unique_name: Optional[str] = None, - *args: Optional[BV], - **kwargs: Optional[BV], - ): - """ - Set up the base class. - - :param name: Name of this object - :param args: Any arguments? - :param kwargs: Fields which this class should contain - """ - super(BaseObj, self).__init__(name=name, unique_name=unique_name) - # If Parameter or Descriptor is given as arguments... - for arg in args: - if issubclass(type(arg), (BaseObj, DescriptorBase)): - kwargs[getattr(arg, 'name')] = arg - # Set kwargs, also useful for serialization - known_keys = self.__dict__.keys() - self._kwargs = kwargs - for key in kwargs.keys(): - if key in known_keys: - raise AttributeError('Kwargs cannot overwrite class attributes in BaseObj.') - if issubclass(type(kwargs[key]), (BasedBase, DescriptorBase)) or 'BaseCollection' in [ - c.__name__ for c in type(kwargs[key]).__bases__ - ]: - self._global_object.map.add_edge(self, kwargs[key]) - self._global_object.map.reset_type(kwargs[key], 'created_internal') - addLoggedProp( - self, - key, - self.__getter(key), - self.__setter(key), - get_id=key, - my_self=self, - test_class=BaseObj, - ) - - def _add_component(self, key: str, component: BV) -> None: - """ - Dynamically add a component to the class. This is an internal method, though can be called remotely. - The recommended alternative is to use typing, i.e. - - class Foo(Bar): - def __init__(self, foo: Parameter, bar: Parameter): - super(Foo, self).__init__(bar=bar) - self._add_component("foo", foo) - - Goes to: - class Foo(Bar): - foo: ClassVar[Parameter] - def __init__(self, foo: Parameter, bar: Parameter): - super(Foo, self).__init__(bar=bar) - self.foo = foo - - :param key: Name of component to be added - :param component: Component to be added - :return: None - """ - self._kwargs[key] = component - self._global_object.map.add_edge(self, component) - self._global_object.map.reset_type(component, 'created_internal') - addLoggedProp( - self, - key, - self.__getter(key), - self.__setter(key), - get_id=key, - my_self=self, - test_class=BaseObj, - ) - - def __setattr__(self, key: str, value: BV) -> None: - # Assume that the annotation is a ClassVar - old_obj = None - if ( - hasattr(self.__class__, '__annotations__') - and key in self.__class__.__annotations__ - and hasattr(self.__class__.__annotations__[key], '__args__') - and issubclass( - getattr(value, '__old_class__', value.__class__), - self.__class__.__annotations__[key].__args__, - ) - ): - if issubclass(type(getattr(self, key, None)), (BasedBase, DescriptorBase)): - old_obj = self.__getattribute__(key) - self._global_object.map.prune_vertex_from_edge(self, old_obj) - self._add_component(key, value) - else: - if hasattr(self, key) and issubclass(type(value), (BasedBase, DescriptorBase)): - old_obj = self.__getattribute__(key) - self._global_object.map.prune_vertex_from_edge(self, old_obj) - self._global_object.map.add_edge(self, value) - super(BaseObj, self).__setattr__(key, value) - # Update the interface bindings if something changed (BasedBase and Descriptor) - if old_obj is not None: - old_interface = getattr(self, 'interface', None) - if old_interface is not None: - self.generate_bindings() - - def __repr__(self) -> str: - return f"{self.__class__.__name__} `{getattr(self, 'name')}`" - - @staticmethod - def __getter(key: str) -> Callable[[BV], BV]: - def getter(obj: BV) -> BV: - return obj._kwargs[key] - - return getter - - @staticmethod - def __setter(key: str) -> Callable[[BV], None]: - def setter(obj: BV, value: float) -> None: - if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( - value.__class__, (DescriptorBase) - ): - obj._kwargs[key].value = value - else: - obj._kwargs[key] = value - - return setter - - # @staticmethod - # def __setter(key: str) -> Callable[[Union[B, V]], None]: - # def setter(obj: Union[V, B], value: float) -> None: - # if issubclass(obj._kwargs[key].__class__, Descriptor): - # if issubclass(obj._kwargs[key].__class__, Descriptor): - # obj._kwargs[key] = value - # else: - # obj._kwargs[key].value = value - # else: - # obj._kwargs[key] = value - # - # return setter diff --git a/src/easyscience/Objects/component_serializer.py b/src/easyscience/base_classes/component_serializer.py similarity index 100% rename from src/easyscience/Objects/component_serializer.py rename to src/easyscience/base_classes/component_serializer.py diff --git a/src/easyscience/Objects/Groups.py b/src/easyscience/base_collection.py similarity index 97% rename from src/easyscience/Objects/Groups.py rename to src/easyscience/base_collection.py index 90d4f0c6..01bd9479 100644 --- a/src/easyscience/Objects/Groups.py +++ b/src/easyscience/base_collection.py @@ -4,9 +4,6 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.1.0' - from collections.abc import MutableSequence from numbers import Number from typing import TYPE_CHECKING @@ -19,12 +16,13 @@ from typing import Union from easyscience.global_object.undo_redo import NotarizedDict -from easyscience.Objects.ObjectClasses import BasedBase -from easyscience.Objects.variable.descriptor_base import DescriptorBase +from easyscience.variable.descriptor_base import DescriptorBase + +from .base_classes.based_base import BasedBase if TYPE_CHECKING: - from easyscience.Objects.Inferface import iF - from easyscience.Objects.ObjectClasses import B + from .base_classes.base_obj import B + from .interface_factory import iF V = TypeVar('V', bound=DescriptorBase) diff --git a/src/easyscience/Objects/Inferface.py b/src/easyscience/interface_factory.py similarity index 99% rename from src/easyscience/Objects/Inferface.py rename to src/easyscience/interface_factory.py index 04b4e72b..960eb8d8 100644 --- a/src/easyscience/Objects/Inferface.py +++ b/src/easyscience/interface_factory.py @@ -4,9 +4,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project 2) & (sys.version_info.minor > 8) diff --git a/src/easyscience/Objects/job/__init__.py b/src/easyscience/job/__init__.py similarity index 100% rename from src/easyscience/Objects/job/__init__.py rename to src/easyscience/job/__init__.py diff --git a/src/easyscience/Objects/job/analysis.py b/src/easyscience/job/analysis.py similarity index 94% rename from src/easyscience/Objects/job/analysis.py rename to src/easyscience/job/analysis.py index ff01009f..34a37bd4 100644 --- a/src/easyscience/Objects/job/analysis.py +++ b/src/easyscience/job/analysis.py @@ -6,8 +6,8 @@ import numpy as np -from easyscience.fitting.minimizers import MinimizerBase -from easyscience.Objects.ObjectClasses import BaseObj +from ..base_classes.base_obj import BaseObj +from ..fitting.minimizers import MinimizerBase class AnalysisBase(BaseObj, metaclass=ABCMeta): diff --git a/src/easyscience/Objects/job/experiment.py b/src/easyscience/job/experiment.py similarity index 91% rename from src/easyscience/Objects/job/experiment.py rename to src/easyscience/job/experiment.py index 1f2a63aa..ee7158eb 100644 --- a/src/easyscience/Objects/job/experiment.py +++ b/src/easyscience/job/experiment.py @@ -3,7 +3,7 @@ # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Date: Fri, 2 May 2025 13:48:18 +0200 Subject: [PATCH 29/58] Getting rid of auther annotation on files --- resources/scripts/generate_html.py | 2 -- src/easyscience/global_object/hugger/__init__.py | 2 -- src/easyscience/global_object/hugger/hugger.py | 3 --- src/easyscience/global_object/hugger/property.py | 3 --- src/easyscience/global_object/logger.py | 3 --- src/easyscience/global_object/map.py | 3 --- src/easyscience/global_object/undo_redo.py | 3 --- src/easyscience/io/json.py | 2 -- src/easyscience/legacy/legacy_core.py | 3 --- tests/integration_tests/Fitting/test_multi_fitter.py | 3 --- tests/unit_tests/Datasets/__init__.py | 2 -- tests/unit_tests/Fitting/__init__.py | 2 -- tests/unit_tests/Objects/__init__.py | 3 --- tests/unit_tests/Objects/test_BaseObj.py | 2 -- tests/unit_tests/Objects/test_Groups.py | 2 -- .../unit_tests/Objects/variable/test_descriptor_from_legacy.py | 2 -- tests/unit_tests/__init__.py | 3 --- tests/unit_tests/global_object/test_undo_redo.py | 3 --- tests/unit_tests/models/__init__.py | 2 -- tests/unit_tests/models/test_polynomial.py | 2 -- tests/unit_tests/utils/__init__.py | 2 -- tests/unit_tests/utils/io_tests/__init__.py | 2 -- tests/unit_tests/utils/io_tests/test_core.py | 2 -- tests/unit_tests/utils/io_tests/test_dict.py | 2 -- tests/unit_tests/utils/io_tests/test_json.py | 2 -- tests/unit_tests/utils/io_tests/test_xml.py | 2 -- 26 files changed, 62 deletions(-) diff --git a/resources/scripts/generate_html.py b/resources/scripts/generate_html.py index 92a90981..0a399639 100644 --- a/resources/scripts/generate_html.py +++ b/resources/scripts/generate_html.py @@ -1,5 +1,3 @@ -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' import sys diff --git a/src/easyscience/global_object/hugger/__init__.py b/src/easyscience/global_object/hugger/__init__.py index 22e236a6..2f639849 100644 --- a/src/easyscience/global_object/hugger/__init__.py +++ b/src/easyscience/global_object/hugger/__init__.py @@ -2,5 +2,3 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project str: diff --git a/src/easyscience/legacy/legacy_core.py b/src/easyscience/legacy/legacy_core.py index a0471bec..78bf03a1 100644 --- a/src/easyscience/legacy/legacy_core.py +++ b/src/easyscience/legacy/legacy_core.py @@ -4,9 +4,6 @@ from __future__ import annotations -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - import json from collections import OrderedDict from hashlib import sha1 diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index 3a546b29..7a279234 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -2,9 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause diff --git a/tests/unit_tests/Objects/test_Groups.py b/tests/unit_tests/Objects/test_Groups.py index b6f835f9..1098e629 100644 --- a/tests/unit_tests/Objects/test_Groups.py +++ b/tests/unit_tests/Objects/test_Groups.py @@ -1,5 +1,3 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.1.0" # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause diff --git a/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py b/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py index 038697ae..ecb71e94 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" diff --git a/tests/unit_tests/models/test_polynomial.py b/tests/unit_tests/models/test_polynomial.py index adccb9e1..382693c6 100644 --- a/tests/unit_tests/models/test_polynomial.py +++ b/tests/unit_tests/models/test_polynomial.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2022 Contributors to the EasyScience project -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" import numpy as np import pytest diff --git a/tests/unit_tests/utils/__init__.py b/tests/unit_tests/utils/__init__.py index 3d57f66c..17adea14 100644 --- a/tests/unit_tests/utils/__init__.py +++ b/tests/unit_tests/utils/__init__.py @@ -2,5 +2,3 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2022 Contributors to the EasyScience project -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" diff --git a/tests/unit_tests/utils/io_tests/__init__.py b/tests/unit_tests/utils/io_tests/__init__.py index 3d57f66c..17adea14 100644 --- a/tests/unit_tests/utils/io_tests/__init__.py +++ b/tests/unit_tests/utils/io_tests/__init__.py @@ -2,5 +2,3 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2022 Contributors to the EasyScience project -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" diff --git a/tests/unit_tests/utils/io_tests/test_core.py b/tests/unit_tests/utils/io_tests/test_core.py index c0625af6..86d274e0 100644 --- a/tests/unit_tests/utils/io_tests/test_core.py +++ b/tests/unit_tests/utils/io_tests/test_core.py @@ -1,5 +1,3 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" import numpy as np from copy import deepcopy diff --git a/tests/unit_tests/utils/io_tests/test_dict.py b/tests/unit_tests/utils/io_tests/test_dict.py index 1e7ce928..8cec69ec 100644 --- a/tests/unit_tests/utils/io_tests/test_dict.py +++ b/tests/unit_tests/utils/io_tests/test_dict.py @@ -1,5 +1,3 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" from copy import deepcopy from typing import Type diff --git a/tests/unit_tests/utils/io_tests/test_json.py b/tests/unit_tests/utils/io_tests/test_json.py index 6d08f8e4..2c8d5dd5 100644 --- a/tests/unit_tests/utils/io_tests/test_json.py +++ b/tests/unit_tests/utils/io_tests/test_json.py @@ -1,5 +1,3 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" import json from copy import deepcopy diff --git a/tests/unit_tests/utils/io_tests/test_xml.py b/tests/unit_tests/utils/io_tests/test_xml.py index 490b702d..4cddbc4c 100644 --- a/tests/unit_tests/utils/io_tests/test_xml.py +++ b/tests/unit_tests/utils/io_tests/test_xml.py @@ -1,5 +1,3 @@ -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" import sys import xml.etree.ElementTree as ET From e31dfefa15750d86d12dc8dced749d01653300c0 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 2 May 2025 14:31:50 +0200 Subject: [PATCH 30/58] Fix confusing type hints --- src/easyscience/Utils/classTools.py | 12 ++++----- src/easyscience/base_classes/base_obj.py | 26 +++++++------------ src/easyscience/base_classes/based_base.py | 14 +++++----- .../base_classes/component_serializer.py | 6 ++--- src/easyscience/base_collection.py | 21 +++++++-------- src/easyscience/interface_factory.py | 24 +++++++---------- src/easyscience/io/dict.py | 12 ++++----- src/easyscience/io/json.py | 10 +++---- src/easyscience/io/template.py | 14 +++------- src/easyscience/io/xml.py | 6 ++--- src/easyscience/legacy/legacy_core.py | 8 +++--- 11 files changed, 64 insertions(+), 89 deletions(-) diff --git a/src/easyscience/Utils/classTools.py b/src/easyscience/Utils/classTools.py index 22f544f1..a203a25a 100644 --- a/src/easyscience/Utils/classTools.py +++ b/src/easyscience/Utils/classTools.py @@ -12,11 +12,11 @@ from easyscience.global_object.hugger.property import LoggedProperty if TYPE_CHECKING: - from ..base_classes.base_obj import BV - from ..base_classes.base_obj import B + from ..base_classes import BasedBase + from ..base_classes import ComponentSerializer -def addLoggedProp(inst: BV, name: str, *args, **kwargs) -> None: +def addLoggedProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: cls = type(inst) annotations = getattr(cls, '__annotations__', False) if not hasattr(cls, '__perinstance'): @@ -29,7 +29,7 @@ def addLoggedProp(inst: BV, name: str, *args, **kwargs) -> None: setattr(cls, name, LoggedProperty(*args, **kwargs)) -def addProp(inst: BV, name: str, *args, **kwargs) -> None: +def addProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: cls = type(inst) annotations = getattr(cls, '__annotations__', False) if not hasattr(cls, '__perinstance'): @@ -43,7 +43,7 @@ def addProp(inst: BV, name: str, *args, **kwargs) -> None: setattr(cls, name, property(*args, **kwargs)) -def removeProp(inst: BV, name: str) -> None: +def removeProp(inst: ComponentSerializer, name: str) -> None: cls = type(inst) if not hasattr(cls, '__perinstance'): cls = type(cls.__name__, (cls,), {'__module__': __name__}) @@ -53,7 +53,7 @@ def removeProp(inst: BV, name: str) -> None: delattr(cls, name) -def generatePath(model_obj: B, skip_first: bool = False) -> Tuple[List[int], List[str]]: +def generatePath(model_obj: BasedBase, skip_first: bool = False) -> Tuple[List[int], List[str]]: pars = model_obj.get_parameters() start_idx = 0 + int(skip_first) unique_names = [] diff --git a/src/easyscience/base_classes/base_obj.py b/src/easyscience/base_classes/base_obj.py index 604d16d3..faf5d3fe 100644 --- a/src/easyscience/base_classes/base_obj.py +++ b/src/easyscience/base_classes/base_obj.py @@ -6,19 +6,13 @@ from typing import TYPE_CHECKING from typing import Callable from typing import Optional -from typing import TypeVar from ..utils.classTools import addLoggedProp -from ..variable.descriptor_base import DescriptorBase from .based_base import BasedBase -from .component_serializer import ComponentSerializer if TYPE_CHECKING: - V = TypeVar('V', bound=DescriptorBase) - -if TYPE_CHECKING: - B = TypeVar('B', bound=BasedBase) - BV = TypeVar('BV', bound=ComponentSerializer) + from ..variable.descriptor_base import DescriptorBase + from .component_serializer import ComponentSerializer class BaseObj(BasedBase): @@ -33,8 +27,8 @@ def __init__( self, name: str, unique_name: Optional[str] = None, - *args: Optional[BV], - **kwargs: Optional[BV], + *args: Optional[ComponentSerializer], + **kwargs: Optional[ComponentSerializer], ): """ Set up the base class. @@ -69,7 +63,7 @@ def __init__( test_class=BaseObj, ) - def _add_component(self, key: str, component: BV) -> None: + def _add_component(self, key: str, component: ComponentSerializer) -> None: """ Dynamically add a component to the class. This is an internal method, though can be called remotely. The recommended alternative is to use typing, i.e. @@ -103,7 +97,7 @@ def __init__(self, foo: Parameter, bar: Parameter): test_class=BaseObj, ) - def __setattr__(self, key: str, value: BV) -> None: + def __setattr__(self, key: str, value: ComponentSerializer) -> None: # Assume that the annotation is a ClassVar old_obj = None if ( @@ -135,15 +129,15 @@ def __repr__(self) -> str: return f"{self.__class__.__name__} `{getattr(self, 'name')}`" @staticmethod - def __getter(key: str) -> Callable[[BV], BV]: - def getter(obj: BV) -> BV: + def __getter(key: str) -> Callable[[ComponentSerializer], ComponentSerializer]: + def getter(obj: ComponentSerializer) -> ComponentSerializer: return obj._kwargs[key] return getter @staticmethod - def __setter(key: str) -> Callable[[BV], None]: - def setter(obj: BV, value: float) -> None: + def __setter(key: str) -> Callable[[ComponentSerializer], None]: + def setter(obj: ComponentSerializer, value: float) -> None: if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( value.__class__, (DescriptorBase) ): diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index 35cf76ba..c01ac66c 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -9,17 +9,15 @@ from typing import List from typing import Optional from typing import Set -from typing import TypeVar from easyscience import global_object from ..variable import Parameter -from ..variable.descriptor_base import DescriptorBase from .component_serializer import ComponentSerializer if TYPE_CHECKING: - from easyscience.interface_factory import iF - V = TypeVar('V', bound=DescriptorBase) + from ..interface_factory import InterfaceFactoryTemplate + from ..variable.descriptor_base import DescriptorBase class BasedBase(ComponentSerializer): @@ -27,7 +25,7 @@ class BasedBase(ComponentSerializer): _REDIRECT = {} - def __init__(self, name: str, interface: Optional[iF] = None, unique_name: Optional[str] = None): + def __init__(self, name: str, interface: Optional[InterfaceFactoryTemplate] = None, unique_name: Optional[str] = None): self._global_object = global_object if unique_name is None: unique_name = self._global_object.generate_unique_name(self.__class__.__name__) @@ -91,14 +89,14 @@ def name(self, new_name: str): self._name = new_name @property - def interface(self) -> iF: + def interface(self) -> InterfaceFactoryTemplate: """ Get the current interface of the object """ return self._interface @interface.setter - def interface(self, new_interface: iF): + def interface(self, new_interface: InterfaceFactoryTemplate): """ Set the current interface to the object and generate bindings if possible. iF.e. ``` @@ -153,7 +151,7 @@ def get_parameters(self) -> List[Parameter]: par_list.append(item) return par_list - def _get_linkable_attributes(self) -> List[V]: + def _get_linkable_attributes(self) -> List[DescriptorBase]: """ Get all objects which can be linked against as a list. diff --git a/src/easyscience/base_classes/component_serializer.py b/src/easyscience/base_classes/component_serializer.py index 259d490c..8a040ae6 100644 --- a/src/easyscience/base_classes/component_serializer.py +++ b/src/easyscience/base_classes/component_serializer.py @@ -13,7 +13,7 @@ from ..io.dict import DictSerializer if TYPE_CHECKING: - from ..io.template import EC + from ..io.template import BaseEncoderDecoder class ComponentSerializer: @@ -27,7 +27,7 @@ class ComponentSerializer: def __deepcopy__(self, memo): return self.from_dict(self.as_dict()) - def encode(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, **kwargs) -> Any: + def encode(self, skip: Optional[List[str]] = None, encoder: Optional[BaseEncoderDecoder] = None, **kwargs) -> Any: """ Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `DictSerializer`. @@ -42,7 +42,7 @@ def encode(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, return encoder_obj.encode(self, skip=skip, **kwargs) @classmethod - def decode(cls, obj: Any, decoder: Optional[EC] = None) -> Any: + def decode(cls, obj: Any, decoder: Optional[BaseEncoderDecoder] = None) -> Any: """ Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. diff --git a/src/easyscience/base_collection.py b/src/easyscience/base_collection.py index 01bd9479..d33a515c 100644 --- a/src/easyscience/base_collection.py +++ b/src/easyscience/base_collection.py @@ -12,18 +12,15 @@ from typing import List from typing import Optional from typing import Tuple -from typing import TypeVar from typing import Union from easyscience.global_object.undo_redo import NotarizedDict -from easyscience.variable.descriptor_base import DescriptorBase -from .base_classes.based_base import BasedBase +from .base_classes import BasedBase if TYPE_CHECKING: - from .base_classes.base_obj import B - from .interface_factory import iF - V = TypeVar('V', bound=DescriptorBase) + from .interface_factory import InterfaceFactoryTemplate + from .variable.descriptor_base import DescriptorBase class BaseCollection(BasedBase, MutableSequence): @@ -37,8 +34,8 @@ class BaseCollection(BasedBase, MutableSequence): def __init__( self, name: str, - *args: Union[B, V], - interface: Optional[iF] = None, + *args: Union[BasedBase, DescriptorBase], + interface: Optional[InterfaceFactoryTemplate] = None, unique_name: Optional[str] = None, **kwargs, ): @@ -93,7 +90,7 @@ def __init__( self.interface = interface self._kwargs._stack_enabled = True - def insert(self, index: int, value: Union[V, B]) -> None: + def insert(self, index: int, value: Union[DescriptorBase, BasedBase]) -> None: """ Insert an object into the collection at an index. @@ -120,7 +117,7 @@ def insert(self, index: int, value: Union[V, B]) -> None: else: raise AttributeError('Only EasyScience objects can be put into an EasyScience group') - def __getitem__(self, idx: Union[int, slice]) -> Union[V, B]: + def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase]: """ Get an item in the collection based on its index. @@ -154,7 +151,7 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[V, B]: keys = list(self._kwargs.keys()) return self._kwargs[keys[idx]] - def __setitem__(self, key: int, value: Union[B, V]) -> None: + def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase]) -> None: """ Set an item via it's index. @@ -236,7 +233,7 @@ def data(self) -> Tuple: def __repr__(self) -> str: return f"{self.__class__.__name__} `{getattr(self, 'name')}` of length {len(self)}" - def sort(self, mapping: Callable[[Union[B, V]], Any], reverse: bool = False) -> None: + def sort(self, mapping: Callable[[Union[BasedBase, DescriptorBase]], Any], reverse: bool = False) -> None: """ Sort the collection according to the given mapping. diff --git a/src/easyscience/interface_factory.py b/src/easyscience/interface_factory.py index 960eb8d8..f4952456 100644 --- a/src/easyscience/interface_factory.py +++ b/src/easyscience/interface_factory.py @@ -3,20 +3,17 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project List[str]: return [self.return_name(this_interface) for this_interface in self._interfaces] @property - def current_interface(self) -> _C: + def current_interface(self) -> ABCMeta: """ Returns the constructor for the currently selected interface. @@ -171,7 +168,7 @@ def generate_bindings(self, model, *args, ifun=None, **kwargs): prop._callback = item.make_prop(item_key) prop._callback.fset(prop_value) - def __call__(self, *args, **kwargs) -> _M: + def __call__(self, *args, **kwargs) -> None: return self.__interface_obj def __reduce__(self): @@ -230,6 +227,3 @@ def set_value(value): self.setter_fn(self.link_name, **{inner_key: value}) return set_value - - -iF = TypeVar('iF', bound=InterfaceFactoryTemplate) diff --git a/src/easyscience/io/dict.py b/src/easyscience/io/dict.py index b5b5e41e..5ce96e45 100644 --- a/src/easyscience/io/dict.py +++ b/src/easyscience/io/dict.py @@ -16,7 +16,7 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes.base_obj import BV + from ..base_classes import ComponentSerializer _KNOWN_CORE_TYPES = ("Descriptor", "Parameter") @@ -28,7 +28,7 @@ class DictSerializer(BaseEncoderDecoder): def encode( self, - obj: BV, + obj: ComponentSerializer, skip: Optional[List[str]] = None, full_encode: bool = False, **kwargs, @@ -46,7 +46,7 @@ def encode( return self._convert_to_dict(obj, skip=skip, full_encode=full_encode, **kwargs) @classmethod - def decode(cls, d: Dict) -> BV: + def decode(cls, d: Dict) -> ComponentSerializer: """ :param d: Dict representation. :return: ComponentSerializer class. @@ -55,7 +55,7 @@ def decode(cls, d: Dict) -> BV: return BaseEncoderDecoder._convert_from_dict(d) @classmethod - def from_dict(cls, d: Dict[str, Any]) -> BV: + def from_dict(cls, d: Dict[str, Any]) -> ComponentSerializer: """ :param d: Dict representation. :return: ComponentSerializer class. @@ -70,7 +70,7 @@ class DataDictSerializer(DictSerializer): def encode( self, - obj: BV, + obj: ComponentSerializer, skip: Optional[List[str]] = None, full_encode: bool = False, **kwargs, @@ -95,7 +95,7 @@ def encode( return self._parse_dict(encoded) @classmethod - def decode(cls, d: Dict[str, Any]) -> BV: + def decode(cls, d: Dict[str, Any]) -> ComponentSerializer: """ This function is not implemented as a data dictionary does not contain the necessary information to re-form an EasyScience object. diff --git a/src/easyscience/io/json.py b/src/easyscience/io/json.py index 5c6cb441..845cbcc1 100644 --- a/src/easyscience/io/json.py +++ b/src/easyscience/io/json.py @@ -15,11 +15,11 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes.base_obj import BV + from ..base_classes import ComponentSerializer class JsonSerializer(BaseEncoderDecoder): - def encode(self, obj: BV, skip: List[str] = []) -> str: + def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: """ Returns a json string representation of the ComponentSerializer object. """ @@ -31,12 +31,12 @@ def encode(self, obj: BV, skip: List[str] = []) -> str: return json.dumps(obj, cls=ENCODER) @classmethod - def decode(cls, data: str) -> BV: + def decode(cls, data: str) -> ComponentSerializer: return json.loads(data, cls=JsonDecoderTemplate) class JsonDataSerializer(BaseEncoderDecoder): - def encode(self, obj: BV, skip: List[str] = []) -> str: + def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: """ Returns a json string representation of the ComponentSerializer object. """ @@ -56,7 +56,7 @@ def encode(self, obj: BV, skip: List[str] = []) -> str: return json.dumps(obj, cls=ENCODER) @classmethod - def decode(cls, data: str) -> BV: + def decode(cls, data: str) -> ComponentSerializer: raise NotImplementedError('It is not possible to reconstitute objects from data only objects.') diff --git a/src/easyscience/io/template.py b/src/easyscience/io/template.py index a1dda0ac..3cb5fcb6 100644 --- a/src/easyscience/io/template.py +++ b/src/easyscience/io/template.py @@ -18,13 +18,11 @@ from typing import MutableSequence from typing import Optional from typing import Tuple -from typing import Type -from typing import TypeVar import numpy as np if TYPE_CHECKING: - from ..base_classes.base_obj import BV + from ..base_classes import ComponentSerializer _e = json.JSONEncoder() @@ -37,7 +35,7 @@ class BaseEncoderDecoder: """ @abstractmethod - def encode(self, obj: BV, skip: Optional[List[str]] = None, **kwargs) -> any: + def encode(self, obj: ComponentSerializer, skip: Optional[List[str]] = None, **kwargs) -> any: """ Abstract implementation of an encoder. @@ -114,7 +112,7 @@ def _encode_objs(obj: Any) -> Dict[str, Any]: def _convert_to_dict( self, - obj: BV, + obj: ComponentSerializer, skip: Optional[List[str]] = None, full_encode: bool = False, **kwargs, @@ -265,12 +263,6 @@ def _convert_from_dict(d): return [BaseEncoderDecoder._convert_from_dict(x) for x in d] return d - -if TYPE_CHECKING: - _ = TypeVar('EC', bound=BaseEncoderDecoder) - EC = Type[_] - - def recursive_encoder(obj, skip: List[str] = [], encoder=None, full_encode=False, **kwargs): """ Walk through an object encoding it diff --git a/src/easyscience/io/xml.py b/src/easyscience/io/xml.py index 6da2b2fa..14ccaf33 100644 --- a/src/easyscience/io/xml.py +++ b/src/easyscience/io/xml.py @@ -19,7 +19,7 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes.base_obj import BV + from ..base_classes import ComponentSerializer can_intent = (sys.version_info.major > 2) & (sys.version_info.minor > 8) @@ -32,7 +32,7 @@ class XMLSerializer(BaseEncoderDecoder): def encode( self, - obj: BV, + obj: ComponentSerializer, skip: Optional[List[str]] = None, data_only: bool = False, fast: bool = False, @@ -73,7 +73,7 @@ def encode( return header + ET.tostring(block, encoding='unicode') @classmethod - def decode(cls, data: str) -> BV: + def decode(cls, data: str) -> ComponentSerializer: """ Decode an EasyScience object which has been encoded in XML format. diff --git a/src/easyscience/legacy/legacy_core.py b/src/easyscience/legacy/legacy_core.py index 78bf03a1..79f05bc2 100644 --- a/src/easyscience/legacy/legacy_core.py +++ b/src/easyscience/legacy/legacy_core.py @@ -18,7 +18,7 @@ from ..io.json import jsanitize if TYPE_CHECKING: - from ..io.template import EC + from ..io.template import BaseEncoderDecoder class ComponentSerializer: @@ -32,7 +32,7 @@ class ComponentSerializer: def __deepcopy__(self, memo): return self.from_dict(self.as_dict()) - def encode(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, **kwargs) -> Any: + def encode(self, skip: Optional[List[str]] = None, encoder: Optional[BaseEncoderDecoder] = None, **kwargs) -> Any: """ Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `DictSerializer`. @@ -47,7 +47,7 @@ def encode(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, return encoder_obj.encode(self, skip=skip, **kwargs) @classmethod - def decode(cls, obj: Any, decoder: Optional[EC] = None) -> Any: + def decode(cls, obj: Any, decoder: Optional[BaseEncoderDecoder] = None) -> Any: """ Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. @@ -82,7 +82,7 @@ def from_dict(cls, obj_dict: Dict[str, Any]) -> None: return cls.decode(obj_dict, decoder=DictSerializer) - def encode_data(self, skip: Optional[List[str]] = None, encoder: Optional[EC] = None, **kwargs) -> Any: + def encode_data(self, skip: Optional[List[str]] = None, encoder: Optional[BaseEncoderDecoder] = None, **kwargs) -> Any: """ Returns just the data in an EasyScience object win the format specified by an encoder. From 23dfa046bff904b5626e3968073c5d131e920693 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 5 May 2025 14:00:43 +0200 Subject: [PATCH 31/58] Fix imports and cyclic imports from the source code structure change --- Examples/base/README.rst | 2 +- Examples/base/plot_baseclass1.py | 8 +- docs/src/reference/base.rst | 10 +- src/easyscience/Utils/classTools.py | 2 +- src/easyscience/__init__.py | 2 - src/easyscience/base_classes/__init__.py | 2 - src/easyscience/base_classes/base_obj.py | 5 +- src/easyscience/base_classes/based_base.py | 2 +- src/easyscience/base_collection.py | 3 +- .../fitting/minimizers/minimizer_base.py | 6 +- .../fitting/minimizers/minimizer_bumps.py | 8 +- .../fitting/minimizers/minimizer_dfo.py | 4 +- .../fitting/minimizers/minimizer_lmfit.py | 8 +- src/easyscience/fitting/multi_fitter.py | 2 +- .../component_serializer.py | 4 +- src/easyscience/io/dict.py | 2 +- src/easyscience/io/json.py | 2 +- src/easyscience/io/template.py | 2 +- src/easyscience/io/xml.py | 2 +- src/easyscience/variable/descriptor_base.py | 2 +- .../integration_tests/Fitting/test_fitter.py | 8 +- .../Fitting/test_multi_fitter.py | 4 +- tests/unit_tests/Datasets/__init__.py | 4 - .../Fitting/minimizers/test_minimizer_base.py | 2 +- .../Fitting/minimizers/test_minimizer_dfo.py | 2 +- .../minimizers/test_minimizer_lmfit.py | 2 +- tests/unit_tests/Objects/__init__.py | 3 - .../variable/test_descriptor_from_legacy.py | 213 ------------------ tests/unit_tests/base_classes/__init__.py | 0 .../test_base_obj.py} | 8 +- .../global_object/test_global_object.py | 2 +- tests/unit_tests/global_object/test_map.py | 5 +- .../global_object/test_undo_redo.py | 12 +- tests/unit_tests/{utils => io}/__init__.py | 0 .../{utils/io_tests => io}/test_core.py | 4 +- .../{utils/io_tests => io}/test_dict.py | 8 +- .../{utils/io_tests => io}/test_json.py | 2 +- .../{utils/io_tests => io}/test_xml.py | 2 +- tests/unit_tests/models/test_polynomial.py | 2 +- ...test_Groups.py => test_base_collection.py} | 10 +- tests/unit_tests/utils/io_tests/__init__.py | 4 - .../variable/test_descriptor_any_type.py | 2 +- .../variable/test_descriptor_array.py | 4 +- .../variable/test_descriptor_base.py | 2 +- .../variable/test_descriptor_bool.py | 2 +- .../variable/test_descriptor_number.py | 2 +- .../variable/test_descriptor_str.py | 2 +- .../{Objects => }/variable/test_parameter.py | 6 +- 48 files changed, 84 insertions(+), 311 deletions(-) rename src/easyscience/{base_classes => io}/component_serializer.py (97%) delete mode 100644 tests/unit_tests/Datasets/__init__.py delete mode 100644 tests/unit_tests/Objects/__init__.py delete mode 100644 tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py create mode 100644 tests/unit_tests/base_classes/__init__.py rename tests/unit_tests/{Objects/test_BaseObj.py => base_classes/test_base_obj.py} (98%) rename tests/unit_tests/{utils => io}/__init__.py (100%) rename tests/unit_tests/{utils/io_tests => io}/test_core.py (95%) rename tests/unit_tests/{utils/io_tests => io}/test_dict.py (95%) rename tests/unit_tests/{utils/io_tests => io}/test_json.py (98%) rename tests/unit_tests/{utils/io_tests => io}/test_xml.py (98%) rename tests/unit_tests/{Objects/test_Groups.py => test_base_collection.py} (98%) delete mode 100644 tests/unit_tests/utils/io_tests/__init__.py rename tests/unit_tests/{Objects => }/variable/test_descriptor_any_type.py (96%) rename tests/unit_tests/{Objects => }/variable/test_descriptor_array.py (99%) rename tests/unit_tests/{Objects => }/variable/test_descriptor_base.py (98%) rename tests/unit_tests/{Objects => }/variable/test_descriptor_bool.py (96%) rename tests/unit_tests/{Objects => }/variable/test_descriptor_number.py (99%) rename tests/unit_tests/{Objects => }/variable/test_descriptor_str.py (96%) rename tests/unit_tests/{Objects => }/variable/test_parameter.py (99%) diff --git a/Examples/base/README.rst b/Examples/base/README.rst index 24202501..a91eda2f 100644 --- a/Examples/base/README.rst +++ b/Examples/base/README.rst @@ -3,4 +3,4 @@ Subclassing Examples ------------------------ -This section gathers examples which correspond to subclassing the :class:`easyscience.Objects.Base.BaseObj` class. +This section gathers examples which correspond to subclassing the :class:`easyscience.base_classes.BaseObj` class. diff --git a/Examples/base/plot_baseclass1.py b/Examples/base/plot_baseclass1.py index f0ca9c13..0f4eddfb 100644 --- a/Examples/base/plot_baseclass1.py +++ b/Examples/base/plot_baseclass1.py @@ -1,8 +1,8 @@ """ Subclassing BaseObj - Simple Pendulum ===================================== -This example shows how to subclass :class:`easyscience.Objects.Base.BaseObj` with parameters from -:class:`EasyScience.Objects.Base.Parameter`. For this example a simple pendulum will be modeled. +This example shows how to subclass :class:`easyscience.base_classes.BaseObj` with parameters from +:class:`EasyScience.variable.Parameter`. For this example a simple pendulum will be modeled. .. math:: y = A \sin (2 \pi f t + \phi ) @@ -17,8 +17,8 @@ import matplotlib.pyplot as plt import numpy as np -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.ObjectClasses import Parameter +from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter # %% # Subclassing diff --git a/docs/src/reference/base.rst b/docs/src/reference/base.rst index 62fb8fe1..887500ba 100644 --- a/docs/src/reference/base.rst +++ b/docs/src/reference/base.rst @@ -5,13 +5,13 @@ Parameters and Objects Descriptors =========== -.. autoclass:: easyscience.Objects.Variable.Descriptor +.. autoclass:: easyscience.variable.Descriptor :members: Parameters ========== -.. autoclass:: easyscience.Objects.Variable.Parameter +.. autoclass:: easyscience.variable.Parameter :members: :inherited-members: @@ -22,17 +22,17 @@ Super Classes and Collections Super Classes ============= -.. autoclass:: easyscience.Objects.ObjectClasses.BasedBase +.. autoclass:: easyscience.base_classes.BasedBase :members: :inherited-members: -.. autoclass:: easyscience.Objects.ObjectClasses.BaseObj +.. autoclass:: easyscience.base_classes.BaseObj :members: +_add_component :inherited-members: Collections =========== -.. autoclass:: easyscience.Objects.Groups.BaseCollection +.. autoclass:: easyscience.BaseCollection :members: :inherited-members: diff --git a/src/easyscience/Utils/classTools.py b/src/easyscience/Utils/classTools.py index a203a25a..475d3299 100644 --- a/src/easyscience/Utils/classTools.py +++ b/src/easyscience/Utils/classTools.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from ..base_classes import BasedBase - from ..base_classes import ComponentSerializer + from ..io.component_serializer import ComponentSerializer def addLoggedProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index 38833df9..d120ddda 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -9,12 +9,10 @@ from .__version__ import __version__ as __version__ # noqa: E402 from .base_collection import BaseCollection # noqa: E402 from .fitting.available_minimizers import AvailableMinimizers # noqa: E402 -from .interface_factory import InterfaceFactory # noqa: E402 __all__ = [ __version__, AvailableMinimizers, global_object, BaseCollection, - InterfaceFactory, ] diff --git a/src/easyscience/base_classes/__init__.py b/src/easyscience/base_classes/__init__.py index 759e93a7..dd9bf3aa 100644 --- a/src/easyscience/base_classes/__init__.py +++ b/src/easyscience/base_classes/__init__.py @@ -1,9 +1,7 @@ from .base_obj import BaseObj from .based_base import BasedBase -from .component_serializer import ComponentSerializer __all__ = [ BaseObj, BasedBase, - ComponentSerializer, ] diff --git a/src/easyscience/base_classes/base_obj.py b/src/easyscience/base_classes/base_obj.py index faf5d3fe..47b57d18 100644 --- a/src/easyscience/base_classes/base_obj.py +++ b/src/easyscience/base_classes/base_obj.py @@ -8,11 +8,12 @@ from typing import Optional from ..utils.classTools import addLoggedProp +from ..variable.descriptor_base import DescriptorBase from .based_base import BasedBase if TYPE_CHECKING: - from ..variable.descriptor_base import DescriptorBase - from .component_serializer import ComponentSerializer + from ..io.component_serializer import ComponentSerializer + class BaseObj(BasedBase): diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index c01ac66c..4a7de21e 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -12,8 +12,8 @@ from easyscience import global_object +from ..io.component_serializer import ComponentSerializer from ..variable import Parameter -from .component_serializer import ComponentSerializer if TYPE_CHECKING: from ..interface_factory import InterfaceFactoryTemplate diff --git a/src/easyscience/base_collection.py b/src/easyscience/base_collection.py index d33a515c..0341f359 100644 --- a/src/easyscience/base_collection.py +++ b/src/easyscience/base_collection.py @@ -17,10 +17,11 @@ from easyscience.global_object.undo_redo import NotarizedDict from .base_classes import BasedBase +from .variable.descriptor_base import DescriptorBase if TYPE_CHECKING: from .interface_factory import InterfaceFactoryTemplate - from .variable.descriptor_base import DescriptorBase + class BaseCollection(BasedBase, MutableSequence): diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 511057f5..fdc3bbde 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -17,8 +17,8 @@ import numpy as np # causes circular import when Parameter is imported -# from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +# from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers from .utils import FitError @@ -161,7 +161,7 @@ def all_methods() -> List[str]: @abstractmethod def convert_to_par_object(obj): # todo after constraint changes, add type hint: obj: BaseObj """ - Convert an `EasyScience.Objects.Base.Parameter` object to an engine Parameter object. + Convert an `EasyScience.variable.Parameter` object to an engine Parameter object. """ def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]: diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 14df1d0f..157448be 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -15,8 +15,8 @@ from bumps.parameter import Parameter as BumpsParameter # causes circular import when Parameter is imported -# from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +# from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -32,7 +32,7 @@ class Bumps(MinimizerBase): """ This is a wrapper to Bumps: https://bumps.readthedocs.io/ - It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. + It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.base_classes.BaseObj`. """ package = 'bumps' @@ -160,7 +160,7 @@ def convert_to_pars_obj(self, par_list: Optional[List] = None) -> List[BumpsPara @staticmethod def convert_to_par_object(obj) -> BumpsParameter: """ - Convert an `EasyScience.Objects.Base.Parameter` object to a bumps Parameter object + Convert an `EasyScience.variable.Parameter` object to a bumps Parameter object :return: bumps Parameter compatible object. :rtype: BumpsParameter diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 27f7eba4..867dcb69 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -11,8 +11,8 @@ import numpy as np # causes circular import when Parameter is imported -# from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +# from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index bb29e8de..bc4de348 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -14,8 +14,8 @@ from lmfit.model import ModelResult # causes circular import when Parameter is imported -# from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +# from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -27,7 +27,7 @@ class LMFit(MinimizerBase): # noqa: S101 """ This is a wrapper to the extended Levenberg-Marquardt Fit: https://lmfit.github.io/lmfit-py/ - It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.Objects.Base.BaseObj`. + It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.base_classes.BaseObj`. """ package = 'lmfit' @@ -175,7 +175,7 @@ def convert_to_pars_obj(self, parameters: Optional[List[Parameter]] = None) -> L @staticmethod def convert_to_par_object(parameter: Parameter) -> LMParameter: """ - Convert an `EasyScience.Objects.Base.Parameter` object to a lmfit Parameter object. + Convert an EasyScience Parameter object to a lmfit Parameter object. :return: lmfit Parameter compatible object. :rtype: LMParameter diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index c812ff0e..b4d620fa 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -7,7 +7,7 @@ import numpy as np -from easyscience.Objects.Groups import BaseCollection +from easyscience import BaseCollection from .fitter import Fitter from .minimizers import FitResults diff --git a/src/easyscience/base_classes/component_serializer.py b/src/easyscience/io/component_serializer.py similarity index 97% rename from src/easyscience/base_classes/component_serializer.py rename to src/easyscience/io/component_serializer.py index 8a040ae6..be4210b6 100644 --- a/src/easyscience/base_classes/component_serializer.py +++ b/src/easyscience/io/component_serializer.py @@ -10,10 +10,10 @@ from typing import List from typing import Optional -from ..io.dict import DictSerializer +from .dict import DictSerializer if TYPE_CHECKING: - from ..io.template import BaseEncoderDecoder + from .template import BaseEncoderDecoder class ComponentSerializer: diff --git a/src/easyscience/io/dict.py b/src/easyscience/io/dict.py index 5ce96e45..2907f04d 100644 --- a/src/easyscience/io/dict.py +++ b/src/easyscience/io/dict.py @@ -16,7 +16,7 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes import ComponentSerializer + from .component_serializer import ComponentSerializer _KNOWN_CORE_TYPES = ("Descriptor", "Parameter") diff --git a/src/easyscience/io/json.py b/src/easyscience/io/json.py index 845cbcc1..074c524d 100644 --- a/src/easyscience/io/json.py +++ b/src/easyscience/io/json.py @@ -15,7 +15,7 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes import ComponentSerializer + from .component_serializer import ComponentSerializer class JsonSerializer(BaseEncoderDecoder): diff --git a/src/easyscience/io/template.py b/src/easyscience/io/template.py index 3cb5fcb6..b20246d9 100644 --- a/src/easyscience/io/template.py +++ b/src/easyscience/io/template.py @@ -22,7 +22,7 @@ import numpy as np if TYPE_CHECKING: - from ..base_classes import ComponentSerializer + from .component_serializer import ComponentSerializer _e = json.JSONEncoder() diff --git a/src/easyscience/io/xml.py b/src/easyscience/io/xml.py index 14ccaf33..5d4d8744 100644 --- a/src/easyscience/io/xml.py +++ b/src/easyscience/io/xml.py @@ -19,7 +19,7 @@ from .template import BaseEncoderDecoder if TYPE_CHECKING: - from ..base_classes import ComponentSerializer + from .component_serializer import ComponentSerializer can_intent = (sys.version_info.major > 2) & (sys.version_info.minor > 8) diff --git a/src/easyscience/variable/descriptor_base.py b/src/easyscience/variable/descriptor_base.py index 04c77697..3918d528 100644 --- a/src/easyscience/variable/descriptor_base.py +++ b/src/easyscience/variable/descriptor_base.py @@ -10,7 +10,7 @@ from easyscience import global_object from easyscience.global_object.undo_redo import property_stack -from easyscience.Objects.component_serializer import ComponentSerializer +from easyscience.io.component_serializer import ComponentSerializer class DescriptorBase(ComponentSerializer, metaclass=abc.ABCMeta): diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index 7a644de2..780dfb21 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -5,11 +5,11 @@ import pytest import numpy as np -from easyscience.fitting.fitter import Fitter +from easyscience.fitting import Fitter from easyscience.fitting.minimizers import FitError -from easyscience.fitting.available_minimizers import AvailableMinimizers -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +from easyscience.fitting import AvailableMinimizers +from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter # Model and container of parameters for tests class AbsSin(BaseObj): diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py index 7a279234..1c3178de 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -7,8 +7,8 @@ import numpy as np from easyscience.fitting.multi_fitter import MultiFitter from easyscience.fitting.minimizers import FitError -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import Parameter +from easyscience.base_classes import BaseObj +from easyscience.variable import Parameter class Line(BaseObj): diff --git a/tests/unit_tests/Datasets/__init__.py b/tests/unit_tests/Datasets/__init__.py deleted file mode 100644 index 2f639849..00000000 --- a/tests/unit_tests/Datasets/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project " - d = DescriptorNumber("test", 1, unit="cm") - assert repr(d) == f"<{d.__class__.__name__} 'test': 1.0000 cm>" - - -def test_descriptor_number_as_dict(): - d = DescriptorNumber("test", 1) - result = d.as_dict() - expected = { - "@module": DescriptorNumber.__module__, - "@class": DescriptorNumber.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1, - "unit": "dimensionless", - "description": "", - "url": "", - "display_name": "test", - "callback": None, - } - for key in expected.keys(): - if key == "callback": - continue - assert result[key] == expected[key] - - -@pytest.mark.parametrize( - "reference, constructor", - ( - [ - { - "@module": DescriptorBool.__module__, - "@class": DescriptorBool.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": False, - "description": "", - "url": "", - "display_name": "test", - }, - DescriptorBool, - ], - [ - { - "@module": DescriptorNumber.__module__, - "@class": DescriptorNumber.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1, - "unit": "dimensionless", - "variance": 0.0, - "description": "", - "url": "", - "display_name": "test", - }, - DescriptorNumber, - ], - [ - { - "@module": DescriptorStr.__module__, - "@class": DescriptorStr.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": "string", - "description": "", - "url": "", - "display_name": "test", - }, - DescriptorStr, - ], - ), - ids=["DescriptorBool", "DescriptorNumber", "DescriptorStr"], -) -def test_item_from_dict(reference, constructor): - d = constructor.from_dict(reference) - for key, item in reference.items(): - if key.startswith("@"): - continue - obtained = getattr(d, key) - assert obtained == item - - -@pytest.mark.parametrize("value", ("This is ", "a fun ", "test")) -def test_parameter_display_name(value): - p = DescriptorNumber("test", 1, display_name=value) - assert p.display_name == value - - -def test_item_boolean_value(): - item = DescriptorBool("test", True) - assert item.value is True - item.value = False - assert item.value is False - - item = DescriptorBool("test", False) - assert item.value is False - item.value = True - assert item.value is True diff --git a/tests/unit_tests/base_classes/__init__.py b/tests/unit_tests/base_classes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/base_classes/test_base_obj.py similarity index 98% rename from tests/unit_tests/Objects/test_BaseObj.py rename to tests/unit_tests/base_classes/test_base_obj.py index da67cc33..9ba645d9 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/base_classes/test_base_obj.py @@ -16,9 +16,9 @@ import pytest import easyscience -from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.variable import DescriptorNumber -from easyscience.Objects.variable import Parameter +from easyscience.base_classes import BaseObj +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter from easyscience.io.dict import DictSerializer from easyscience import global_object @@ -153,7 +153,7 @@ def test_baseobj_as_dict(clear, setup_pars: dict): obtained = obj.as_dict() assert isinstance(obtained, dict) expected = { - "@module": "easyscience.Objects.ObjectClasses", + "@module": "easyscience.base_classes.base_obj", "@class": "BaseObj", "@version": easyscience.__version__, "name": "test", diff --git a/tests/unit_tests/global_object/test_global_object.py b/tests/unit_tests/global_object/test_global_object.py index 2997b523..6f0c463b 100644 --- a/tests/unit_tests/global_object/test_global_object.py +++ b/tests/unit_tests/global_object/test_global_object.py @@ -1,6 +1,6 @@ import easyscience from easyscience.global_object.global_object import GlobalObject -from easyscience.Objects.variable.descriptor_bool import DescriptorBool +from easyscience.variable import DescriptorBool class TestGlobalObject: def test_init(self): diff --git a/tests/unit_tests/global_object/test_map.py b/tests/unit_tests/global_object/test_map.py index b3eae678..2f0151f2 100644 --- a/tests/unit_tests/global_object/test_map.py +++ b/tests/unit_tests/global_object/test_map.py @@ -2,9 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project - diff --git a/tests/unit_tests/Objects/variable/test_descriptor_any_type.py b/tests/unit_tests/variable/test_descriptor_any_type.py similarity index 96% rename from tests/unit_tests/Objects/variable/test_descriptor_any_type.py rename to tests/unit_tests/variable/test_descriptor_any_type.py index 3a906cf8..70c4cc65 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_any_type.py +++ b/tests/unit_tests/variable/test_descriptor_any_type.py @@ -1,7 +1,7 @@ import pytest import numpy as np -from easyscience.Objects.variable.descriptor_any_type import DescriptorAnyType +from easyscience.variable import DescriptorAnyType from easyscience import global_object class TestDescriptorAnyType: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/variable/test_descriptor_array.py similarity index 99% rename from tests/unit_tests/Objects/variable/test_descriptor_array.py rename to tests/unit_tests/variable/test_descriptor_array.py index 7e543ecf..d6ff77bf 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/variable/test_descriptor_array.py @@ -6,8 +6,8 @@ import numpy as np -from easyscience.Objects.variable.descriptor_array import DescriptorArray -from easyscience.Objects.variable.descriptor_number import DescriptorNumber +from easyscience.variable import DescriptorArray +from easyscience.variable import DescriptorNumber from easyscience import global_object class TestDescriptorArray: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_base.py b/tests/unit_tests/variable/test_descriptor_base.py similarity index 98% rename from tests/unit_tests/Objects/variable/test_descriptor_base.py rename to tests/unit_tests/variable/test_descriptor_base.py index 4e444358..aeeb823e 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_base.py +++ b/tests/unit_tests/variable/test_descriptor_base.py @@ -1,7 +1,7 @@ import pytest from easyscience import global_object -from easyscience.Objects.variable.descriptor_base import DescriptorBase +from easyscience.variable.descriptor_base import DescriptorBase class TestDesciptorBase: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_bool.py b/tests/unit_tests/variable/test_descriptor_bool.py similarity index 96% rename from tests/unit_tests/Objects/variable/test_descriptor_bool.py rename to tests/unit_tests/variable/test_descriptor_bool.py index 09440074..4be20657 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_bool.py +++ b/tests/unit_tests/variable/test_descriptor_bool.py @@ -1,6 +1,6 @@ import pytest -from easyscience.Objects.variable.descriptor_bool import DescriptorBool +from easyscience.variable import DescriptorBool from easyscience import global_object class TestDescriptorBool: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_number.py b/tests/unit_tests/variable/test_descriptor_number.py similarity index 99% rename from tests/unit_tests/Objects/variable/test_descriptor_number.py rename to tests/unit_tests/variable/test_descriptor_number.py index ecd52408..8b61bf92 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_number.py +++ b/tests/unit_tests/variable/test_descriptor_number.py @@ -3,7 +3,7 @@ import scipp as sc from scipp import UnitError -from easyscience.Objects.variable.descriptor_number import DescriptorNumber +from easyscience.variable import DescriptorNumber from easyscience import global_object class TestDescriptorNumber: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_str.py b/tests/unit_tests/variable/test_descriptor_str.py similarity index 96% rename from tests/unit_tests/Objects/variable/test_descriptor_str.py rename to tests/unit_tests/variable/test_descriptor_str.py index 5ad903e3..aa593ed9 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_str.py +++ b/tests/unit_tests/variable/test_descriptor_str.py @@ -1,6 +1,6 @@ import pytest -from easyscience.Objects.variable.descriptor_str import DescriptorStr +from easyscience.variable import DescriptorStr from easyscience import global_object class TestDescriptorStr: diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/variable/test_parameter.py similarity index 99% rename from tests/unit_tests/Objects/variable/test_parameter.py rename to tests/unit_tests/variable/test_parameter.py index 0deefb29..28896ac4 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/variable/test_parameter.py @@ -5,10 +5,10 @@ from scipp import UnitError -from easyscience.Objects.variable.parameter import Parameter -from easyscience.Objects.variable.descriptor_number import DescriptorNumber +from easyscience.variable import Parameter +from easyscience.variable import DescriptorNumber from easyscience import global_object -from easyscience.Objects.ObjectClasses import BaseObj +from easyscience.base_classes import BaseObj class TestParameter: @pytest.fixture From 0ec2c315e9cd3ccfdb6b282aae012325b8a278e6 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 5 May 2025 14:53:59 +0200 Subject: [PATCH 32/58] move BaseCollection into base_classes and InterfaceFactoryTemplate into fitting/calculators. --- src/easyscience/__init__.py | 2 -- src/easyscience/base_classes/__init__.py | 2 ++ src/easyscience/{ => base_classes}/base_collection.py | 6 +++--- src/easyscience/base_classes/based_base.py | 2 +- src/easyscience/fitting/calculators/__init__.py | 7 +++++++ .../{ => fitting/calculators}/interface_factory.py | 2 +- src/easyscience/fitting/multi_fitter.py | 3 +-- src/easyscience/models/polynomial.py | 4 ++-- tests/unit_tests/global_object/test_undo_redo.py | 2 +- tests/unit_tests/io/test_dict.py | 4 ++-- tests/unit_tests/test_base_collection.py | 6 ++---- 11 files changed, 22 insertions(+), 18 deletions(-) rename src/easyscience/{ => base_classes}/base_collection.py (98%) create mode 100644 src/easyscience/fitting/calculators/__init__.py rename src/easyscience/{ => fitting/calculators}/interface_factory.py (99%) diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index d120ddda..588ffb81 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -7,12 +7,10 @@ from .__version__ import __version__ as __version__ # noqa: E402 -from .base_collection import BaseCollection # noqa: E402 from .fitting.available_minimizers import AvailableMinimizers # noqa: E402 __all__ = [ __version__, AvailableMinimizers, global_object, - BaseCollection, ] diff --git a/src/easyscience/base_classes/__init__.py b/src/easyscience/base_classes/__init__.py index dd9bf3aa..8c79346f 100644 --- a/src/easyscience/base_classes/__init__.py +++ b/src/easyscience/base_classes/__init__.py @@ -1,7 +1,9 @@ +from .base_collection import BaseCollection from .base_obj import BaseObj from .based_base import BasedBase __all__ = [ BaseObj, BasedBase, + BaseCollection, ] diff --git a/src/easyscience/base_collection.py b/src/easyscience/base_classes/base_collection.py similarity index 98% rename from src/easyscience/base_collection.py rename to src/easyscience/base_classes/base_collection.py index 0341f359..b2fa07ea 100644 --- a/src/easyscience/base_collection.py +++ b/src/easyscience/base_classes/base_collection.py @@ -16,11 +16,11 @@ from easyscience.global_object.undo_redo import NotarizedDict -from .base_classes import BasedBase -from .variable.descriptor_base import DescriptorBase +from ..variable.descriptor_base import DescriptorBase +from .based_base import BasedBase if TYPE_CHECKING: - from .interface_factory import InterfaceFactoryTemplate + from ..fitting.calculators import InterfaceFactoryTemplate diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index 4a7de21e..faccb686 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -16,7 +16,7 @@ from ..variable import Parameter if TYPE_CHECKING: - from ..interface_factory import InterfaceFactoryTemplate + from ..fitting.calculators import InterfaceFactoryTemplate from ..variable.descriptor_base import DescriptorBase diff --git a/src/easyscience/fitting/calculators/__init__.py b/src/easyscience/fitting/calculators/__init__.py new file mode 100644 index 00000000..63793dd5 --- /dev/null +++ b/src/easyscience/fitting/calculators/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project Date: Mon, 5 May 2025 15:13:37 +0200 Subject: [PATCH 33/58] Push changes to lowercase in filenames --- src/easyscience/utils/Exceptions.py | 11 + src/easyscience/utils/__init__.py | 3 + src/easyscience/utils/classTools.py | 67 ++++ src/easyscience/utils/classUtils.py | 94 +++++ src/easyscience/utils/decorators.py | 100 ++++++ src/easyscience/utils/string.py | 44 +++ .../integration_tests/fitting/test_fitter.py | 323 ++++++++++++++++++ .../fitting/test_multi_fitter.py | 251 ++++++++++++++ tests/unit_tests/fitting/__init__.py | 4 + .../fitting/minimizers/test_factory.py | 62 ++++ .../fitting/minimizers/test_minimizer_base.py | 206 +++++++++++ .../minimizers/test_minimizer_bumps.py | 166 +++++++++ .../fitting/minimizers/test_minimizer_dfo.py | 265 ++++++++++++++ .../minimizers/test_minimizer_lmfit.py | 308 +++++++++++++++++ tests/unit_tests/fitting/test_fitter.py | 219 ++++++++++++ 15 files changed, 2123 insertions(+) create mode 100644 src/easyscience/utils/Exceptions.py create mode 100644 src/easyscience/utils/__init__.py create mode 100644 src/easyscience/utils/classTools.py create mode 100644 src/easyscience/utils/classUtils.py create mode 100644 src/easyscience/utils/decorators.py create mode 100644 src/easyscience/utils/string.py create mode 100644 tests/integration_tests/fitting/test_fitter.py create mode 100644 tests/integration_tests/fitting/test_multi_fitter.py create mode 100644 tests/unit_tests/fitting/__init__.py create mode 100644 tests/unit_tests/fitting/minimizers/test_factory.py create mode 100644 tests/unit_tests/fitting/minimizers/test_minimizer_base.py create mode 100644 tests/unit_tests/fitting/minimizers/test_minimizer_bumps.py create mode 100644 tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py create mode 100644 tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py create mode 100644 tests/unit_tests/fitting/test_fitter.py diff --git a/src/easyscience/utils/Exceptions.py b/src/easyscience/utils/Exceptions.py new file mode 100644 index 00000000..8fd54ac0 --- /dev/null +++ b/src/easyscience/utils/Exceptions.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project None: + cls = type(inst) + annotations = getattr(cls, '__annotations__', False) + if not hasattr(cls, '__perinstance'): + cls = type(cls.__name__, (cls,), {'__module__': inst.__module__}) + cls.__perinstance = True + if annotations: + cls.__annotations__ = annotations + inst.__old_class__ = inst.__class__ + inst.__class__ = cls + setattr(cls, name, LoggedProperty(*args, **kwargs)) + + +def addProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: + cls = type(inst) + annotations = getattr(cls, '__annotations__', False) + if not hasattr(cls, '__perinstance'): + cls = type(cls.__name__, (cls,), {'__module__': __name__}) + cls.__perinstance = True + if annotations: + cls.__annotations__ = annotations + inst.__old_class__ = inst.__class__ + inst.__class__ = cls + + setattr(cls, name, property(*args, **kwargs)) + + +def removeProp(inst: ComponentSerializer, name: str) -> None: + cls = type(inst) + if not hasattr(cls, '__perinstance'): + cls = type(cls.__name__, (cls,), {'__module__': __name__}) + cls.__perinstance = True + inst.__old_class__ = inst.__class__ + inst.__class__ = cls + delattr(cls, name) + + +def generatePath(model_obj: BasedBase, skip_first: bool = False) -> Tuple[List[int], List[str]]: + pars = model_obj.get_parameters() + start_idx = 0 + int(skip_first) + unique_names = [] + names = [] + for par in pars: + route = global_object.map.reverse_route(par.unique_name, model_obj.unique_name) + objs = [getattr(global_object.map.get_item_by_key(r), 'name') for r in route] + objs.reverse() + names.append('.'.join(objs[start_idx:])) + unique_names.append(par.unique_name) + return unique_names, names diff --git a/src/easyscience/utils/classUtils.py b/src/easyscience/utils/classUtils.py new file mode 100644 index 00000000..df00895c --- /dev/null +++ b/src/easyscience/utils/classUtils.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project str: + """Return the function's docstring.""" + return self.func.__doc__ + + def __get__(self, obj, objtype): + """Support instance methods.""" + return functools.partial(self.__call__, obj) + + +def counted(func): + """ + Counts how many times a function has been called and adds a `func.calls` to it's properties + :param func: Function to be counted + :return: Results from function call + """ + + @functools.wraps(func) + def wrapped(*args, **kwargs): + wrapped.n_calls += 1 + return func(*args, **kwargs) + + wrapped.n_calls = 0 + return wrapped + + +def time_it(func): + """ + Times a function and reports the time either to the class' log or the base logger + :param func: function to be timed + :return: callable function with timer + """ + name = func.__module__ + "." + func.__name__ + time_logger = global_object.log.getLogger("timer." + name) + + @functools.wraps(func) + def _time_it(*args, **kwargs): + start = int(round(time() * 1000)) + try: + return func(*args, **kwargs) + finally: + end_ = int(round(time() * 1000)) - start + time_logger.debug( + f"\033[1;34;49mExecution time: {end_ if end_ > 0 else 0} ms\033[0m" + ) + + return _time_it + + +def deprecated(func): + """ + This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + """ + + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.warn_explicit( + "Call to deprecated function {}.".format(func.__name__), + category=DeprecationWarning, + filename=func.__code__.co_filename, + lineno=func.__code__.co_firstlineno + 1, + ) + return func(*args, **kwargs) + + return new_func diff --git a/src/easyscience/utils/string.py b/src/easyscience/utils/string.py new file mode 100644 index 00000000..b99cf41e --- /dev/null +++ b/src/easyscience/utils/string.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project = 0: + s += '+' + if abs(f.numerator) != 1: + s += str(f.numerator) + elif f < 0: + s += '-' + s += c + dim + if f.denominator != 1: + s += '/' + str(f.denominator) + if t != 0: + s += ('+' if (t > 0 and s != '') else '') + str(Fraction(t).limit_denominator()) + if s == '': + s += '0' + parts.append(s) + return delim.join(parts) diff --git a/tests/integration_tests/fitting/test_fitter.py b/tests/integration_tests/fitting/test_fitter.py new file mode 100644 index 00000000..780dfb21 --- /dev/null +++ b/tests/integration_tests/fitting/test_fitter.py @@ -0,0 +1,323 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project 0 % This does not work as some methods don't calculate error + assert item1.error == pytest.approx(0, abs=1e-1) + assert item1.value == pytest.approx(item2.value, abs=5e-3) + y_calc_ref = ref_sin(x) + assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) + assert result.residual == pytest.approx(sp_sin(x) - y_calc_ref, abs=1e-2) + + +@pytest.mark.parametrize("with_errors", [False, True]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_basic_fit(fit_engine: AvailableMinimizers, with_errors): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + args = [x, y] + kwargs = {} + if with_errors: + kwargs["weights"] = 1 / np.sqrt(y) + result = f.fit(*args, **kwargs) + + if fit_engine is not None: + assert result.minimizer_engine.package == fit_engine.name.lower() # Special case where minimizer matches package + assert sp_sin.phase.value == pytest.approx(ref_sin.phase.value, rel=1e-3) + assert sp_sin.offset.value == pytest.approx(ref_sin.offset.value, rel=1e-3) + + +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_fit_result(fit_engine): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + sp_ref1 = { + f"p{item1.unique_name}": item1.value + for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) + } + sp_ref2 = { + f"p{item1.unique_name}": item2.value + for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) + } + + f = Fitter(sp_sin, sp_sin) + + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + + result = f.fit(x, y) + check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) + + +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_basic_max_evaluations(fit_engine): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + args = [x, y] + kwargs = {} + f.max_evaluations = 3 + try: + result = f.fit(*args, **kwargs) + # Result should not be the same as the reference + assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3) + assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3) + except FitError as e: + # DFO throws a different error + assert "Objective has been called MAXFUN times" in str(e) + + +@pytest.mark.parametrize("fit_engine,tolerance", [(None, 10), (AvailableMinimizers.LMFit, 10), (AvailableMinimizers.Bumps, 10), (AvailableMinimizers.DFO, 0.1)]) +def test_basic_tolerance(fit_engine, tolerance): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + args = [x, y] + kwargs = {} + f.tolerance = tolerance + result = f.fit(*args, **kwargs) + # Result should not be the same as the reference + assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3) + assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3) + + +@pytest.mark.parametrize("fit_method", ["leastsq", "powell", "cobyla"]) +def test_lmfit_methods(fit_method): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + assert fit_method in f._minimizer.supported_methods() + result = f.fit(x, y, method=fit_method) + check_fit_results(result, sp_sin, ref_sin, x) + + +#@pytest.mark.xfail(reason="known bumps issue") +@pytest.mark.parametrize("fit_method", ["newton", "lm"]) +def test_bumps_methods(fit_method): + ref_sin = AbsSin(0.2, np.pi) + sp_sin = AbsSin(0.354, 3.05) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + sp_sin.offset.fixed = False + sp_sin.phase.fixed = False + + f = Fitter(sp_sin, sp_sin) + f.switch_minimizer("Bumps") + assert fit_method in f._minimizer.supported_methods() + result = f.fit(x, y, method=fit_method) + check_fit_results(result, sp_sin, ref_sin, x) + + +@pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_dependent_parameter(fit_engine): + ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) + sp_sin = AbsSin(1, 0.5) + + x = np.linspace(0, 5, 200) + y = ref_sin(x) + + f = Fitter(sp_sin, sp_sin) + + sp_sin.offset.make_dependent_on(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) + + if fit_engine is not None: + try: + f.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + + result = f.fit(x, y) + check_fit_results(result, sp_sin, ref_sin, x) + +@pytest.mark.parametrize("with_errors", [False, True]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_2D_vectorized(fit_engine, with_errors): + x = np.linspace(0, 5, 200) + mm = AbsSin2D(0.3, 1.6) + m2 = AbsSin2D( + 0.1, 1.8 + ) # The fit is quite sensitive to the initial values :-( + X, Y = np.meshgrid(x, x) + XY = np.stack((X, Y), axis=2) + ff = Fitter(m2, m2) + if fit_engine is not None: + try: + ff.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + try: + args = [XY, mm(XY)] + kwargs = {"vectorized": True} + if with_errors: + kwargs["weights"] = 1 / np.sqrt(args[1]) + result = ff.fit(*args, **kwargs) + except FitError as e: + if "Unable to allocate" in str(e): + pytest.skip(msg="MemoryError - Matrix too large") + else: + raise e + assert result.n_pars == len(m2.get_fit_parameters()) + assert result.reduced_chi == pytest.approx(0, abs=1.5e-3) + assert result.success + assert np.all(result.x == XY) + y_calc_ref = m2(XY) + assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) + assert result.residual == pytest.approx(mm(XY) - y_calc_ref, abs=1e-2) + + +@pytest.mark.parametrize("with_errors", [False, True]) +@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) +def test_2D_non_vectorized(fit_engine, with_errors): + x = np.linspace(0, 5, 200) + mm = AbsSin2DL(0.3, 1.6) + m2 = AbsSin2DL( + 0.1, 1.8 + ) # The fit is quite sensitive to the initial values :-( + X, Y = np.meshgrid(x, x) + XY = np.stack((X, Y), axis=2) + ff = Fitter(m2, m2) + if fit_engine is not None: + try: + ff.switch_minimizer(fit_engine) + except AttributeError: + pytest.skip(msg=f"{fit_engine} is not installed") + try: + args = [XY, mm(XY.reshape(-1, 2))] + kwargs = {"vectorized": False} + if with_errors: + kwargs["weights"] = 1 / np.sqrt(args[1]) + result = ff.fit(*args, **kwargs) + except FitError as e: + if "Unable to allocate" in str(e): + pytest.skip(msg="MemoryError - Matrix too large") + else: + raise e + assert result.n_pars == len(m2.get_fit_parameters()) + assert result.reduced_chi == pytest.approx(0, abs=1.5e-3) + assert result.success + assert np.all(result.x == XY) + y_calc_ref = m2(XY.reshape(-1, 2)) + assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) + assert result.residual == pytest.approx( + mm(XY.reshape(-1, 2)) - y_calc_ref, abs=1e-2 + ) diff --git a/tests/integration_tests/fitting/test_multi_fitter.py b/tests/integration_tests/fitting/test_multi_fitter.py new file mode 100644 index 00000000..1c3178de --- /dev/null +++ b/tests/integration_tests/fitting/test_multi_fitter.py @@ -0,0 +1,251 @@ +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project MinimizerBase: + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + minimizer = factory(minimizer, mock_fit_object, mock_fit_function) + return minimizer + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.LMFit), ('leastsq', AvailableMinimizers.LMFit_leastsq), ('powell', AvailableMinimizers.LMFit_powell), ('cobyla', AvailableMinimizers.LMFit_cobyla), ('differential_evolution', AvailableMinimizers.LMFit_differential_evolution), ('least_squares', AvailableMinimizers.LMFit_scipy_least_squares)]) + def test_factory_lm_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + assert minimizer.package == 'lmfit' + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('amoeba', AvailableMinimizers.Bumps), ('amoeba', AvailableMinimizers.Bumps_simplex), ('newton', AvailableMinimizers.Bumps_newton), ('lm', AvailableMinimizers.Bumps_lm)]) + def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + assert minimizer.package == 'bumps' + + @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.DFO), ('leastsq', AvailableMinimizers.DFO_leastsq)]) + def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): + minimizer = self.pull_minminizer(minimizer_enum) + assert minimizer._method == minimizer_method + assert minimizer.package == 'dfo' + + +@pytest.mark.parametrize('minimizer_name,expected', [('LMFit', AvailableMinimizers.LMFit), ('LMFit_leastsq', AvailableMinimizers.LMFit_leastsq), ('LMFit_powell', AvailableMinimizers.LMFit_powell), ('LMFit_cobyla', AvailableMinimizers.LMFit_cobyla), ('LMFit_differential_evolution', AvailableMinimizers.LMFit_differential_evolution), ('LMFit_scipy_least_squares', AvailableMinimizers.LMFit_scipy_least_squares) ]) +def test_from_string_to_enum_lmfit(minimizer_name, expected): + assert from_string_to_enum(minimizer_name) == expected + + +@pytest.mark.parametrize('minimizer_name,expected', [('Bumps', AvailableMinimizers.Bumps), ('Bumps_simplex', AvailableMinimizers.Bumps_simplex), ('Bumps_newton', AvailableMinimizers.Bumps_newton), ('Bumps_lm', AvailableMinimizers.Bumps_lm)]) +def test_from_string_to_enum_bumps(minimizer_name, expected): + assert from_string_to_enum(minimizer_name) == expected + + +@pytest.mark.parametrize('minimizer_name,expected', [('DFO', AvailableMinimizers.DFO), ('DFO_leastsq', AvailableMinimizers.DFO_leastsq)]) +def test_from_string_to_enum_dfo(minimizer_name, expected): + assert from_string_to_enum(minimizer_name) == expected + + +def test_available_minimizers(): + assert AvailableMinimizers.LMFit + assert AvailableMinimizers.LMFit_leastsq + assert AvailableMinimizers.LMFit_powell + assert AvailableMinimizers.LMFit_cobyla + assert AvailableMinimizers.LMFit_differential_evolution + assert AvailableMinimizers.LMFit_scipy_least_squares + assert AvailableMinimizers.Bumps + assert AvailableMinimizers.Bumps_simplex + assert AvailableMinimizers.Bumps_newton + assert AvailableMinimizers.Bumps_lm + assert AvailableMinimizers.DFO + assert AvailableMinimizers.DFO_leastsq + assert len(AvailableMinimizers) == 12 \ No newline at end of file diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/fitting/minimizers/test_minimizer_base.py new file mode 100644 index 00000000..1af9ae99 --- /dev/null +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_base.py @@ -0,0 +1,206 @@ +import pytest + +from unittest.mock import MagicMock + +from inspect import Parameter as InspectParameter +from inspect import Signature +from inspect import _empty + +from easyscience.fitting.minimizers.minimizer_base import MinimizerBase +from easyscience.fitting.minimizers.utils import FitError +from easyscience.variable import Parameter + +class TestMinimizerBase(): + @pytest.fixture + def minimizer(self): + # This avoids the error: TypeError: Can't instantiate abstract class with abstract methods __init__ + MinimizerBase.__abstractmethods__ = set() + MinimizerBase.supported_methods = MagicMock(return_value=['method']) + + self._mock_minimizer_enum = MagicMock(package='package', method='method') + minimizer = MinimizerBase( + obj='obj', + fit_function='fit_function', + minimizer_enum=self._mock_minimizer_enum + ) + return minimizer + + def test_init_exception(self): + # When Then + MinimizerBase.__abstractmethods__ = set() + MinimizerBase.supported_methods = MagicMock(return_value=['method']) + + # Expect + with pytest.raises(FitError): + MinimizerBase( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='package', method='not_a_method') + ) + + def test_init(self, minimizer: MinimizerBase): + assert minimizer._object == 'obj' + assert minimizer._original_fit_function == 'fit_function' + assert minimizer._method == 'method' + assert minimizer._cached_pars == {} + assert minimizer._cached_pars_vals == {} + assert minimizer._cached_model == None + assert minimizer._fit_function == None + + def test_enum(self, minimizer: MinimizerBase): + assert minimizer.enum == self._mock_minimizer_enum + + def test_evaluate(self, minimizer: MinimizerBase): + # When + minimizer._fit_function = MagicMock(return_value='fit_function_return') + minimizer._prepare_parameters = MagicMock(return_value={'prepared_parms_key': 'prepared_parms_val'}) + + # Then + result = minimizer.evaluate('x', minimizer_parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}) + + # Expect + assert result == 'fit_function_return' + minimizer._fit_function.assert_called_once_with('x', prepared_parms_key='prepared_parms_val', kwargs={'kwargs_key': 'kwargs_val'}) + minimizer._prepare_parameters.assert_called_once_with({'parms_key': 'parms_val'}) + + def test_evaluate_no_fit_function(self, minimizer: MinimizerBase): + # When + mock_fit_function = MagicMock() + minimizer._fit_function = None + minimizer._prepare_parameters = MagicMock(return_value={'prepared_parms_key': 'prepared_parms_val'}) + minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) + + # Then + minimizer.evaluate('x', minimizer_parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}) + + # Expect + mock_fit_function.assert_called_once_with('x', prepared_parms_key='prepared_parms_val', kwargs={'kwargs_key': 'kwargs_val'}) + minimizer._prepare_parameters.assert_called_once_with({'parms_key': 'parms_val'}) + + def test_evaluate_no_parameters(self, minimizer: MinimizerBase): + # When + minimizer._fit_function = MagicMock(return_value='fit_function_return') + minimizer._prepare_parameters = MagicMock(return_value={'parms_key': 'parms_val'}) + + # Then + minimizer.evaluate('x') + + # Expect + minimizer._prepare_parameters.assert_called_once_with({}) + minimizer._fit_function.assert_called_once_with('x', parms_key='parms_val') + + def test_evaluate_exception(self, minimizer: MinimizerBase): + # When + minimizer_parameters = 'not dict type' + + # Then Expect + with pytest.raises(TypeError): + minimizer.evaluate('x', minimizer_parameters=minimizer_parameters) + + def test_prepare_parameters(self, minimizer: MinimizerBase): + # When + parameters = { + 'pa': 1, + 'pb': 2 + } + + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + 'c': MagicMock() + } + minimizer._cached_pars['a'].value = 3 + minimizer._cached_pars['b'].value = 4 + minimizer._cached_pars['c'].value = 5 + + # Then + parameters = minimizer._prepare_parameters(parameters) + + # Expect + assert parameters == { + 'pa': 1, + 'pb': 2, + 'pc': 5 + } + + def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: + # When + minimizer._original_fit_function = MagicMock(return_value='fit_function_result') + + minimizer._object = MagicMock() + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.value = 1.0 + mock_parm_1.error = 0.1 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.value = 2.0 + mock_parm_2.error = 0.2 + minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + + # Then + fit_function = minimizer._generate_fit_function() + fit_function_result = fit_function([10.0]) + + # Expect + assert 'fit_function_result' == fit_function_result + minimizer._original_fit_function.assert_called_once_with([10.0]) + assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 + assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 + assert str(fit_function.__signature__) == '(x, pmock_parm_1=1.0, pmock_parm_2=2.0)' + + def test_create_signature(self, minimizer: MinimizerBase) -> None: + # When + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.value = 1.0 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.value = 2.0 + pars = {1: mock_parm_1, 2: mock_parm_2} + + # Then + signature = minimizer._create_signature(pars) + + # Expect + wrapped_parameters = [ + InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty), + InspectParameter('p1', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=1.0), + InspectParameter('p2', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=2.0) + ] + expected_signature = Signature(wrapped_parameters) + assert signature == expected_signature + + def test_get_method_dict(self, minimizer: MinimizerBase) -> None: + # When Then + result = minimizer._get_method_kwargs() + + # Expect + assert result == {'method': 'method'} + + def test_get_method_dict_no_self(self, minimizer: MinimizerBase) -> None: + # When + minimizer._method = None + + # Then + result = minimizer._get_method_kwargs() + + # Expect + assert result == {} + + def test_get_method_dict_supported_method(self, minimizer: MinimizerBase) -> None: + # When + minimizer.supported_methods = MagicMock(return_value=['supported_method']) + + # Then + result = minimizer._get_method_kwargs('supported_method') + + # Expect + assert result == {'method': 'supported_method'} + + def test_get_method_dict_not_supported_method(self, minimizer: MinimizerBase) -> None: + # When + minimizer.supported_methods = MagicMock(return_value=['supported_method']) + + # Then Expect + with pytest.raises(FitError): + result = minimizer._get_method_kwargs('not_supported_method') + diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_bumps.py b/tests/unit_tests/fitting/minimizers/test_minimizer_bumps.py new file mode 100644 index 00000000..7feaf8e0 --- /dev/null +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_bumps.py @@ -0,0 +1,166 @@ +import pytest + +from unittest.mock import MagicMock +import numpy as np + +import easyscience.fitting.minimizers.minimizer_bumps + +from easyscience.fitting.minimizers.minimizer_bumps import Bumps +from easyscience.fitting.minimizers.utils import FitError + + +class TestBumpsFit(): + @pytest.fixture + def minimizer(self) -> Bumps: + minimizer = Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='amoeba') + ) + return minimizer + + def test_init(self, minimizer: Bumps) -> None: + assert minimizer._p_0 == {} + assert minimizer.package == 'bumps' + + def test_init_exception(self) -> None: + with pytest.raises(FitError): + Bumps( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='bumps', method='not_amoeba') + ) + + def test_all_methods(self, minimizer: Bumps) -> None: + # When Then Expect + assert minimizer.all_methods() == ['amoeba', 'de', 'dream', 'newton', 'scipy.leastsq', 'lm'] + + def test_supported_methods(self, minimizer: Bumps) -> None: + # When Then Expect + assert set(minimizer.supported_methods()) == set(['scipy.leastsq','newton', 'lm', 'amoeba']) + + def test_fit(self, minimizer: Bumps, monkeypatch) -> None: + # When + from easyscience import global_object + global_object.stack.enabled = False + + mock_bumps_fit = MagicMock(return_value='fit') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "bumps_fit", mock_bumps_fit) + + mock_FitProblem = MagicMock(return_value='fit_problem') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitProblem", mock_FitProblem) + + mock_model = MagicMock() + mock_model_function = MagicMock(return_value=mock_model) + minimizer._make_model = MagicMock(return_value=mock_model_function) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + cached_par = MagicMock() + cached_par.value = 1 + cached_pars = {'mock_parm_1': cached_par} + minimizer._cached_pars = cached_pars + + # Then + result = minimizer.fit(x=1.0, y=2.0) + + # Expect + assert result == 'gen_fit_results' + mock_bumps_fit.assert_called_once_with('fit_problem', method='amoeba') + minimizer._make_model.assert_called_once_with(parameters=None) + minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._gen_fit_results.assert_called_once_with('fit') + mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) + mock_FitProblem.assert_called_once_with(mock_model) + + + def test_make_model(self, minimizer: Bumps, monkeypatch) -> None: + # When + mock_fit_function = MagicMock(return_value=np.array([11, 22])) + minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) + + mock_parm_1 = MagicMock() + mock_parm_1.unique_name = 'mock_parm_1' + minimizer.convert_to_par_object = MagicMock(return_value='converted_parm_1') + + mock_Curve = MagicMock(return_value='curve') + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "Curve", mock_Curve) + + # Then + model = minimizer._make_model(parameters=[mock_parm_1]) + curve_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) + + # Expect + minimizer._generate_fit_function.assert_called_once_with() + assert mock_Curve.call_args[0][0] == mock_fit_function + assert all(mock_Curve.call_args[0][1] == np.array([1,2])) + assert all(mock_Curve.call_args[0][2] == np.array([10,20])) + assert curve_for_model == 'curve' + + def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): + # When + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + } + minimizer._cached_pars['a'].value = 'a' + minimizer._cached_pars['b'].value = 'b' + + mock_cached_model = MagicMock() + mock_cached_model._pnames = ['pa', 'pb'] + minimizer._cached_model = mock_cached_model + + mock_fit_result = MagicMock() + mock_fit_result.x = [1.0, 2.0] + mock_fit_result.dx = [0.1, 0.2] + + # Then + minimizer._set_parameter_fit_result(mock_fit_result, False) + + # Expect + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.1 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.2 + + def test_gen_fit_results(self, minimizer: Bumps, monkeypatch): + # When + mock_domain_fit_results = MagicMock() + mock_FitResults = MagicMock(return_value=mock_domain_fit_results) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitResults", mock_FitResults) + + mock_fit_result = MagicMock() + mock_fit_result.success = True + + mock_cached_model = MagicMock() + mock_cached_model.x = 'x' + mock_cached_model.y = 'y' + mock_cached_model.dy = 'dy' + mock_cached_model._pnames = ['ppar_1', 'ppar_2'] + minimizer._cached_model = mock_cached_model + + mock_cached_par_1 = MagicMock() + mock_cached_par_1.value = 'par_value_1' + mock_cached_par_2 = MagicMock() + mock_cached_par_2.value = 'par_value_2' + minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} + + minimizer._p_0 = 'p_0' + minimizer.evaluate = MagicMock(return_value='evaluate') + + # Then + domain_fit_results = minimizer._gen_fit_results(mock_fit_result, **{'kwargs_set_key': 'kwargs_set_val'}) + + # Expect + assert domain_fit_results == mock_domain_fit_results + assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' + assert domain_fit_results.success == True + assert domain_fit_results.y_obs == 'y' + assert domain_fit_results.x == 'x' + assert domain_fit_results.p == {'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} + assert domain_fit_results.p0 == 'p_0' + assert domain_fit_results.y_calc == 'evaluate' + assert domain_fit_results.y_err == 'dy' + assert str(domain_fit_results.minimizer_engine) == "" + assert domain_fit_results.fit_args is None + minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'}) diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py b/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py new file mode 100644 index 00000000..7d8ca3fe --- /dev/null +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_dfo.py @@ -0,0 +1,265 @@ +import pytest + +from unittest.mock import MagicMock +import numpy as np + +import easyscience.fitting.minimizers.minimizer_dfo +from easyscience.variable import Parameter + +from easyscience.fitting.minimizers.minimizer_dfo import DFO +from easyscience.fitting.minimizers.utils import FitError + + +class TestDFOFit(): + @pytest.fixture + def minimizer(self) -> DFO: + minimizer = DFO( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='dfo', method='leastsq') + ) + return minimizer + + def test_init(self, minimizer: DFO) -> None: + assert minimizer._p_0 == {} + assert minimizer.package == 'dfo' + + def test_init_exception(self) -> None: + with pytest.raises(FitError): + DFO( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='dfo', method='not_leastsq') + ) + + def test_supported_methods(self, minimizer: DFO) -> None: + # When Then Expect + assert minimizer.supported_methods() == ['leastsq'] + + def test_supported_methods(self, minimizer: DFO) -> None: + # When Then Expect + assert minimizer.supported_methods() == ['leastsq'] + + def test_fit(self, minimizer: DFO) -> None: + # When + from easyscience import global_object + global_object.stack.enabled = False + + mock_model = MagicMock() + mock_model_function = MagicMock(return_value=mock_model) + minimizer._make_model = MagicMock(return_value=mock_model_function) + minimizer._dfo_fit = MagicMock(return_value='fit') + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + cached_par = MagicMock() + cached_par.value = 1 + cached_pars = {'mock_parm_1': cached_par} + minimizer._cached_pars = cached_pars + + # Then + result = minimizer.fit(x=1.0, y=2.0) + + # Expect + assert result == 'gen_fit_results' + minimizer._dfo_fit.assert_called_once_with(cached_pars, mock_model) + minimizer._make_model.assert_called_once_with(parameters=None) + minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._gen_fit_results.assert_called_once_with('fit', 1.4142135623730951) + mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) + + def test_generate_fit_function(self, minimizer: DFO) -> None: + # When + minimizer._original_fit_function = MagicMock(return_value='fit_function_result') + + minimizer._object = MagicMock() + mock_parm_1 = MagicMock() + mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.value = 1.0 + mock_parm_1.error = 0.1 + mock_parm_2 = MagicMock() + mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.value = 2.0 + mock_parm_2.error = 0.2 + minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) + + # Then + fit_function = minimizer._generate_fit_function() + fit_function_result = fit_function([10.0]) + + # Expect + assert 'fit_function_result' == fit_function_result + minimizer._original_fit_function.assert_called_once_with([10.0]) + assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 + assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 + + def test_make_model(self, minimizer: DFO) -> None: + # When + mock_fit_function = MagicMock(return_value=np.array([11, 22])) + minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) + + mock_parm_1 = MagicMock() + mock_parm_1.unique_name = 'mock_parm_1' + mock_parm_1.value = 1000.0 + mock_parm_2 = MagicMock() + mock_parm_2.unique_name = 'mock_parm_2' + mock_parm_2.value = 2000.0 + + # Then + model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2]) + residuals_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) + + # Expect + minimizer._generate_fit_function.assert_called_once_with() + assert all(np.array([-0.01, -0.01]) == residuals_for_model(np.array([1111, 2222]))) + assert all(mock_fit_function.call_args[0][0] == np.array([1, 2])) + assert mock_fit_function.call_args[1] == {'pmock_parm_1': 1111, 'pmock_parm_2': 2222} + + def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): + # When + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + } + minimizer._cached_pars['a'].value = 'a' + minimizer._cached_pars['b'].value = 'b' + + mock_fit_result = MagicMock() + mock_fit_result.x = [1.0, 2.0] + mock_fit_result.jacobian = 'jacobian' + mock_fit_result.resid = 'resid' + + minimizer._error_from_jacobian = MagicMock(return_value=np.array([[0.1, 0.0], [0.0, 0.2]])) + + # Then + minimizer._set_parameter_fit_result(mock_fit_result, False) + + # Expect + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.1 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.2 + minimizer._error_from_jacobian.assert_called_once_with('jacobian', 'resid', 0.95) + + def test_gen_fit_results(self, minimizer: DFO, monkeypatch): + # When + mock_domain_fit_results = MagicMock() + mock_FitResults = MagicMock(return_value=mock_domain_fit_results) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "FitResults", mock_FitResults) + + mock_fit_result = MagicMock() + mock_fit_result.flag = False + + mock_cached_model = MagicMock() + mock_cached_model.x = 'x' + mock_cached_model.y = 'y' + minimizer._cached_model = mock_cached_model + + mock_cached_par_1 = MagicMock() + mock_cached_par_1.value = 'par_value_1' + mock_cached_par_2 = MagicMock() + mock_cached_par_2.value = 'par_value_2' + minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} + + minimizer._p_0 = 'p_0' + minimizer.evaluate = MagicMock(return_value='evaluate') + + # Then + domain_fit_results = minimizer._gen_fit_results(mock_fit_result, 'weights', **{'kwargs_set_key': 'kwargs_set_val'}) + + # Expect + assert domain_fit_results == mock_domain_fit_results + assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' + assert domain_fit_results.success == True + assert domain_fit_results.y_obs == 'y' + assert domain_fit_results.x == 'x' + assert domain_fit_results.p == {'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} + assert domain_fit_results.p0 == 'p_0' + assert domain_fit_results.y_calc == 'evaluate' + assert domain_fit_results.y_err == 'weights' + assert str(domain_fit_results.minimizer_engine) == "" + assert domain_fit_results.fit_args is None + minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'}) + + def test_dfo_fit(self, minimizer: DFO, monkeypatch): + # When + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.value = 1.0 + mock_parm_1.min = 0.1 + mock_parm_1.max = 10.0 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.value = 2.0 + mock_parm_2.min = 0.2 + mock_parm_2.max = 20.0 + pars = {1: mock_parm_1, 2: mock_parm_2} + + kwargs = {'kwargs_set_key': 'kwargs_set_val'} + + mock_dfols = MagicMock() + mock_results = MagicMock() + mock_results.msg = 'Success' + mock_dfols.solve = MagicMock(return_value=mock_results) + + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + + # Then + results = minimizer._dfo_fit(pars, 'model', **kwargs) + + # Expect + assert results == mock_results + assert mock_dfols.solve.call_args[0][0] == 'model' + assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) + assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([0.1, 0.2])) + assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) + assert mock_dfols.solve.call_args[1]['scaling_within_bounds'] is True + assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' + + def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): + # When + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.value = 1.0 + mock_parm_1.min = -np.inf + mock_parm_1.max = 10.0 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.value = 2.0 + mock_parm_2.min = 0.2 + mock_parm_2.max = 20.0 + pars = {1: mock_parm_1, 2: mock_parm_2} + + kwargs = {'kwargs_set_key': 'kwargs_set_val'} + + mock_dfols = MagicMock() + mock_results = MagicMock() + mock_results.msg = 'Success' + mock_dfols.solve = MagicMock(return_value=mock_results) + + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + + # Then + results = minimizer._dfo_fit(pars, 'model', **kwargs) + + # Expect + assert results == mock_results + assert mock_dfols.solve.call_args[0][0] == 'model' + assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) + assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([-np.inf, 0.2])) + assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) + assert not 'scaling_within_bounds' in list(mock_dfols.solve.call_args[1].keys()) + assert 'kwargs_set_key' in list(mock_dfols.solve.call_args[1].keys()) + assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' + + def test_dfo_fit_exception(self, minimizer: DFO, monkeypatch): + # When + pars = {1: MagicMock(Parameter)} + kwargs = {'kwargs_set_key': 'kwargs_set_val'} + + mock_dfols = MagicMock() + mock_results = MagicMock() + mock_results.msg = 'Failed' + mock_dfols.solve = MagicMock(return_value=mock_results) + + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) + + # Then Expect + with pytest.raises(FitError): + minimizer._dfo_fit(pars, 'model', **kwargs) \ No newline at end of file diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py new file mode 100644 index 00000000..92316bc6 --- /dev/null +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py @@ -0,0 +1,308 @@ +import pytest + +from unittest.mock import MagicMock + +import easyscience.fitting.minimizers.minimizer_lmfit +from easyscience.fitting.minimizers.minimizer_lmfit import LMFit +from easyscience.variable import Parameter +from lmfit import Parameter as LMParameter +from easyscience.fitting.minimizers.utils import FitError + + +class TestLMFit(): + @pytest.fixture + def minimizer(self) -> LMFit: + minimizer = LMFit( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='lm', method='leastsq') + ) + return minimizer + + def test_init(self, minimizer: LMFit) -> None: + assert minimizer.package == 'lmfit' + + def test_init_exception(self) -> None: + with pytest.raises(FitError): + LMFit( + obj='obj', + fit_function='fit_function', + minimizer_enum=MagicMock(package='dfo', method='not_leastsq') + ) + + def test_make_model(self, minimizer: LMFit, monkeypatch) -> None: + # When + mock_lm_model = MagicMock() + mock_LMModel = MagicMock(return_value=mock_lm_model) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMModel", mock_LMModel) + minimizer._generate_fit_function = MagicMock(return_value='model') + mock_parm_1 = MagicMock(LMParameter) + mock_parm_1.value = 1.0 + mock_parm_1.min = -10.0 + mock_parm_1.max = 10.0 + mock_parm_2 = MagicMock(LMParameter) + mock_parm_2.value = 2.0 + mock_parm_2.min = -20.0 + mock_parm_2.max = 20.0 + pars = {'key_1': mock_parm_1, 'key_2': mock_parm_2} + + # Then + model = minimizer._make_model(pars=pars) + + # Expect + minimizer._generate_fit_function.assert_called_once_with() + mock_LMModel.assert_called_once_with('model', independent_vars=['x'], param_names=['pkey_1', 'pkey_2']) + mock_lm_model.set_param_hint.assert_called_with('pkey_2', value=2.0, min=-20.0, max=20.0) + assert mock_lm_model.set_param_hint.call_count == 2 + assert model == mock_lm_model + + def test_make_model_no_pars(self, minimizer: LMFit, monkeypatch) -> None: + # When + mock_lm_model = MagicMock() + mock_LMModel = MagicMock(return_value=mock_lm_model) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMModel", mock_LMModel) + minimizer._generate_fit_function = MagicMock(return_value='model') + mock_parm_1 = MagicMock(Parameter) + mock_parm_1.value = 1.0 + mock_parm_1.min = -10.0 + mock_parm_1.max = 10.0 + mock_parm_2 = MagicMock(Parameter) + mock_parm_2.value = 2.0 + mock_parm_2.min = -20.0 + mock_parm_2.max = 20.0 + minimizer._cached_pars = {'key_1': mock_parm_1, 'key_2': mock_parm_2} + + # Then + model = minimizer._make_model() + + # Expect + minimizer._generate_fit_function.assert_called_once_with() + mock_LMModel.assert_called_once_with('model', independent_vars=['x'], param_names=['pkey_1', 'pkey_2']) + mock_lm_model.set_param_hint.assert_called_with('pkey_2', value=2.0, min=-20.0, max=20.0) + assert mock_lm_model.set_param_hint.call_count == 2 + assert model == mock_lm_model + + def test_fit(self, minimizer: LMFit) -> None: + # When + from easyscience import global_object + global_object.stack.enabled = False + + mock_model = MagicMock() + mock_model.fit = MagicMock(return_value='fit') + minimizer._make_model = MagicMock(return_value=mock_model) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + # Then + result = minimizer.fit(x=1.0, y=2.0) + + # Expect + assert result == 'gen_fit_results' + mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='leastsq') + minimizer._make_model.assert_called_once_with() + minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) + minimizer._gen_fit_results.assert_called_once_with('fit') + + def test_fit_model(self, minimizer: LMFit) -> None: + # When + mock_model = MagicMock() + mock_model.fit = MagicMock(return_value='fit') + minimizer._make_model = MagicMock(return_value=mock_model) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + # Then + minimizer.fit(x=1.0, y=2.0, model=mock_model) + + # Expect + mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='leastsq') + minimizer._make_model.assert_not_called() + + def test_fit_method(self, minimizer: LMFit) -> None: + # When + mock_model = MagicMock() + mock_model.fit = MagicMock(return_value='fit') + minimizer._make_model = MagicMock(return_value=mock_model) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + minimizer.supported_methods = MagicMock(return_value=['method_passed']) + minimizer.all_methods = MagicMock(return_value=['method_passed']) + + # Then + minimizer.fit(x=1.0, y=2.0, method='method_passed') + + # Expect + mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='method_passed') + minimizer.supported_methods.assert_called_once_with() + + def test_fit_kwargs(self, minimizer: LMFit) -> None: + # When + mock_model = MagicMock() + mock_model.fit = MagicMock(return_value='fit') + minimizer._make_model = MagicMock(return_value=mock_model) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + # Then + minimizer.fit(x=1.0, y=2.0, minimizer_kwargs={'minimizer_key': 'minimizer_val'}, engine_kwargs={'engine_key': 'engine_val'}) + + # Expect + mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={'minimizer_key': 'minimizer_val'}, method='leastsq', engine_key='engine_val') + + def test_fit_exception(self, minimizer: LMFit) -> None: + # When + minimizer._make_model = MagicMock(side_effect=Exception('Exception')) + minimizer._set_parameter_fit_result = MagicMock() + minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') + + # Then Expect + with pytest.raises(FitError): + minimizer.fit(x=1.0, y=2.0) + + def test_convert_to_pars_obj(self, minimizer: LMFit, monkeypatch) -> None: + # When + minimizer._object = MagicMock() + minimizer._object.get_fit_parameters = MagicMock(return_value = ['parm_1', 'parm_2']) + + minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') + + mock_lm_parameter = MagicMock() + mock_lm_parameter.add_many = MagicMock(return_value='add_many') + mock_LMParameters = MagicMock(return_value=mock_lm_parameter) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameters", mock_LMParameters) + + # Then + pars = minimizer.convert_to_pars_obj() + + # Expect + assert pars == 'add_many' + assert minimizer.convert_to_par_object.call_count == 2 + minimizer._object.get_fit_parameters.assert_called_once_with() + minimizer.convert_to_par_object.assert_called_with('parm_2') + mock_lm_parameter.add_many.assert_called_once_with(['convert_to_par_object', 'convert_to_par_object']) + + def test_convert_to_pars_obj_with_parameters(self, minimizer: LMFit, monkeypatch) -> None: + # When + minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') + + mock_lm_parameter = MagicMock() + mock_lm_parameter.add_many = MagicMock(return_value='add_many') + mock_LMParameters = MagicMock(return_value=mock_lm_parameter) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameters", mock_LMParameters) + + # Then + pars = minimizer.convert_to_pars_obj(['parm_1', 'parm_2']) + + # Expect + assert pars == 'add_many' + assert minimizer.convert_to_par_object.call_count == 2 + minimizer.convert_to_par_object.assert_called_with('parm_2') + mock_lm_parameter.add_many.assert_called_once_with(['convert_to_par_object', 'convert_to_par_object']) + + def test_convert_to_par_object(self, minimizer: LMFit, monkeypatch) -> None: + # When + mock_lm_parameter = MagicMock() + mock_LMParameter = MagicMock(return_value=mock_lm_parameter) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameter", mock_LMParameter) + + mock_parm = MagicMock(Parameter) + mock_parm.value = 1.0 + mock_parm.fixed = True + mock_parm.min = -10.0 + mock_parm.max = 10.0 + mock_parm.unique_name = 'key_converted' + + # Then + par = minimizer.convert_to_par_object(mock_parm) + + # Expect + assert par == mock_lm_parameter + mock_LMParameter.assert_called_once_with('pkey_converted', value=1.0, vary=False, min=-10.0, max=10.0, expr=None, brute_step=None) + + def test_set_parameter_fit_result_no_stack_status(self, minimizer: LMFit) -> None: + # When + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + } + minimizer._cached_pars['a'].value = 'a' + minimizer._cached_pars['b'].value = 'b' + + mock_param_a = MagicMock() + mock_param_a.value = 1.0 + mock_param_a.stderr = 0.1 + mock_param_b = MagicMock + mock_param_b.value = 2.0 + mock_param_b.stderr = 0.2 + mock_fit_result = MagicMock() + mock_fit_result.params = {'pa': mock_param_a, 'pb': mock_param_b} + mock_fit_result.errorbars = True + + # Then + minimizer._set_parameter_fit_result(mock_fit_result, False) + + # Expect + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.1 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.2 + + def test_set_parameter_fit_result_no_stack_status_no_error(self, minimizer: LMFit) -> None: + # When + minimizer._cached_pars = { + 'a': MagicMock(), + 'b': MagicMock(), + } + minimizer._cached_pars['a'].value = 'a' + minimizer._cached_pars['b'].value = 'b' + + mock_param_a = MagicMock() + mock_param_a.value = 1.0 + mock_param_a.stderr = 0.1 + mock_param_b = MagicMock + mock_param_b.value = 2.0 + mock_param_b.stderr = 0.2 + mock_fit_result = MagicMock() + mock_fit_result.params = {'pa': mock_param_a, 'pb': mock_param_b} + mock_fit_result.errorbars = False + + # Then + minimizer._set_parameter_fit_result(mock_fit_result, False) + + # Expect + assert minimizer._cached_pars['a'].value == 1.0 + assert minimizer._cached_pars['a'].error == 0.0 + assert minimizer._cached_pars['b'].value == 2.0 + assert minimizer._cached_pars['b'].error == 0.0 + + def test_gen_fit_results(self, minimizer: LMFit, monkeypatch) -> None: + # When + mock_domain_fit_results = MagicMock() + mock_FitResults = MagicMock(return_value=mock_domain_fit_results) + monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "FitResults", mock_FitResults) + + mock_fit_result = MagicMock() + mock_fit_result.success ='success' + mock_fit_result.data = 'data' + mock_fit_result.userkws = {'x': 'x_val'} + mock_fit_result.values = 'values' + mock_fit_result.init_values = 'init_values' + mock_fit_result.best_fit = 'best_fit' + mock_fit_result.weights = 10 + + # Then + domain_fit_results = minimizer._gen_fit_results(mock_fit_result, **{'kwargs_set_key': 'kwargs_set_val'}) + + # Expect + assert domain_fit_results == mock_domain_fit_results + assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' + assert domain_fit_results.success == 'success' + assert domain_fit_results.y_obs == 'data' + assert domain_fit_results.x == 'x_val' + assert domain_fit_results.p == 'values' + assert domain_fit_results.p0 == 'init_values' + assert domain_fit_results.y_calc == 'best_fit' + assert domain_fit_results.y_err == 0.1 + assert str(domain_fit_results.minimizer_engine) == "" + assert domain_fit_results.fit_args is None + diff --git a/tests/unit_tests/fitting/test_fitter.py b/tests/unit_tests/fitting/test_fitter.py new file mode 100644 index 00000000..992225ce --- /dev/null +++ b/tests/unit_tests/fitting/test_fitter.py @@ -0,0 +1,219 @@ +from unittest.mock import MagicMock + +import pytest +import numpy as np +import easyscience.fitting.fitter +from easyscience.fitting.fitter import Fitter +from easyscience.fitting.available_minimizers import AvailableMinimizers + + +class TestFitter(): + @pytest.fixture + def fitter(self, monkeypatch): + monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) + self.mock_fit_object = MagicMock() + self.mock_fit_function = MagicMock() + return Fitter(self.mock_fit_object, self.mock_fit_function) + + def test_constructor(self, fitter: Fitter): + # When Then Expect + assert fitter._fit_object == self.mock_fit_object + assert fitter._fit_function == self.mock_fit_function + assert fitter._dependent_dims is None + assert fitter._enum_current_minimizer is None #== AvailableMinimizers.LMFit_leastsq + assert fitter._minimizer is None + fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) + + def test_make_model(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.make_model = MagicMock(return_value='model') + fitter._minimizer = mock_minimizer + + # Then + model = fitter.make_model('pars') + + # Expect + assert model == 'model' + mock_minimizer.make_model.assert_called_once_with('pars') + + def test_evaluate(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.evaluate = MagicMock(return_value='result') + fitter._minimizer = mock_minimizer + + # Then + result = fitter.evaluate('pars') + + # Expect + assert result == 'result' + mock_minimizer.evaluate.assert_called_once_with('pars') + + def test_convert_to_pars_obj(self, fitter: Fitter): + # When + mock_minimizer = MagicMock() + mock_minimizer.convert_to_pars_obj = MagicMock(return_value='obj') + fitter._minimizer = mock_minimizer + + # Then + obj = fitter.convert_to_pars_obj('pars') + + # Expect + assert obj == 'obj' + mock_minimizer.convert_to_pars_obj.assert_called_once_with('pars') + + def test_initialize(self, fitter: Fitter): + # When + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + + # Then + fitter.initialize(mock_fit_object, mock_fit_function) + + # Expect + assert fitter._fit_object == mock_fit_object + assert fitter._fit_function == mock_fit_function + fitter._update_minimizer.count(2) + + def test_create(self, fitter: Fitter, monkeypatch): + # When + fitter._update_minimizer = MagicMock() + mock_string_to_enum = MagicMock(return_value=10) + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) + + # Then + fitter.create('great-minimizer') + + # Expect + mock_string_to_enum.assert_called_once_with('great-minimizer') + fitter._update_minimizer.assert_called_once_with(10) + + def test_switch_minimizer(self, fitter: Fitter, monkeypatch): + # When + mock_minimizer = MagicMock() + fitter._minimizer = mock_minimizer + mock_string_to_enum = MagicMock(return_value=10) + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) + + # Then + fitter.switch_minimizer('great-minimizer') + + # Expect + fitter._update_minimizer.count(2) + mock_string_to_enum.assert_called_once_with('great-minimizer') + + def test_update_minimizer(self, monkeypatch): + # When + mock_fit_object = MagicMock() + mock_fit_function = MagicMock() + + mock_string_to_enum = MagicMock(return_value=10) + mock_factory = MagicMock(return_value='minimizer') + monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) + monkeypatch.setattr(easyscience.fitting.fitter, 'factory', mock_factory) + fitter = Fitter(mock_fit_object, mock_fit_function) + + # Then + fitter._update_minimizer('great-minimizer') + + # Expect + assert fitter._enum_current_minimizer == 'great-minimizer' + assert fitter._minimizer == 'minimizer' + + def test_available_minimizers(self, fitter: Fitter): + # When + minimizers = fitter.available_minimizers + + # Then Expect + assert minimizers == [ + 'LMFit', 'LMFit_leastsq', 'LMFit_powell', 'LMFit_cobyla', 'LMFit_differential_evolution', 'LMFit_scipy_least_squares', + 'Bumps', 'Bumps_simplex', 'Bumps_newton', 'Bumps_lm', + 'DFO', 'DFO_leastsq' + ] + + def test_minimizer(self, fitter: Fitter): + # When + fitter._minimizer = 'minimizer' + + # Then + minimizer = fitter.minimizer + + # Expect + assert minimizer == 'minimizer' + + def test_fit_function(self, fitter: Fitter): + # When Then + fit_function = fitter.fit_function + + # Expect + assert fit_function == self.mock_fit_function + + def test_set_fit_function(self, fitter: Fitter): + # When + fitter._enum_current_minimizer = 'current_minimizer' + + # Then + fitter.fit_function = 'new-fit-function' + + # Expect + assert fitter._fit_function == 'new-fit-function' + fitter._update_minimizer.assert_called_with('current_minimizer') + + def test_fit_object(self, fitter: Fitter): + # When Then + fit_object = fitter.fit_object + + # Expect + assert fit_object == self.mock_fit_object + + def test_set_fit_object(self, fitter: Fitter): + # When + fitter._enum_current_minimizer = 'current_minimizer' + + # Then + fitter.fit_object = 'new-fit-object' + + # Expect + assert fitter.fit_object == 'new-fit-object' + fitter._update_minimizer.assert_called_with('current_minimizer') + + def test_fit(self, fitter: Fitter): + # When + fitter._precompute_reshaping = MagicMock(return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims')) + fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') + fitter._post_compute_reshaping = MagicMock(return_value='fit_result') + fitter._minimizer = MagicMock() + fitter._minimizer.fit = MagicMock(return_value='result') + + # Then + result = fitter.fit('x', 'y', 'weights', 'vectorized') + + # Expect + fitter._precompute_reshaping.assert_called_once_with('x', 'y', 'weights', 'vectorized') + fitter._fit_function_wrapper.assert_called_once_with('x_new', flatten=True) + fitter._post_compute_reshaping.assert_called_once_with('result', 'x', 'y') + assert result == 'fit_result' + assert fitter._dependent_dims == 'dims' + assert fitter._fit_function == self.mock_fit_function + + def test_post_compute_reshaping(self, fitter: Fitter): + # When + fit_result = MagicMock() + fit_result.y_calc = np.array([[10], [20], [30]]) + fit_result.y_err = np.array([[40], [50], [60]]) + x = np.array([1, 2, 3]) + y = np.array([4, 5, 6]) + + # Then + result = fitter._post_compute_reshaping(fit_result, x, y) + + # Expect + assert np.array_equal(result.y_calc, np.array([10, 20, 30])) + assert np.array_equal(result.y_err, np.array([40, 50, 60])) + assert np.array_equal(result.x, x) + assert np.array_equal(result.y_obs, y) + +# TODO +# def test_fit_function_wrapper() +# def test_precompute_reshaping() From 2e8abb1b55f472f4627841e52f6c5fd7b426bd2c Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 5 May 2025 15:35:36 +0200 Subject: [PATCH 34/58] Missed the BaseCollections test for the move --- tests/integration_tests/fitting/__init__.py | 0 tests/unit_tests/{ => base_classes}/test_base_collection.py | 0 tests/unit_tests/fitting/minimizers/__init__.py | 0 tests/unit_tests/global_object/__init__.py | 0 tests/unit_tests/variable/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/integration_tests/fitting/__init__.py rename tests/unit_tests/{ => base_classes}/test_base_collection.py (100%) create mode 100644 tests/unit_tests/fitting/minimizers/__init__.py create mode 100644 tests/unit_tests/global_object/__init__.py create mode 100644 tests/unit_tests/variable/__init__.py diff --git a/tests/integration_tests/fitting/__init__.py b/tests/integration_tests/fitting/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/test_base_collection.py b/tests/unit_tests/base_classes/test_base_collection.py similarity index 100% rename from tests/unit_tests/test_base_collection.py rename to tests/unit_tests/base_classes/test_base_collection.py diff --git a/tests/unit_tests/fitting/minimizers/__init__.py b/tests/unit_tests/fitting/minimizers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/global_object/__init__.py b/tests/unit_tests/global_object/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/variable/__init__.py b/tests/unit_tests/variable/__init__.py new file mode 100644 index 00000000..e69de29b From 06cac5dd7c3f239f6f714876a595a25273bbad98 Mon Sep 17 00:00:00 2001 From: Christian Dam Vedel <158568093+damskii9992@users.noreply.github.com> Date: Mon, 5 May 2025 15:58:51 +0200 Subject: [PATCH 35/58] Delete src/easyscience/Utils directory --- src/easyscience/Utils/Exceptions.py | 11 --- src/easyscience/Utils/__init__.py | 3 - src/easyscience/Utils/classTools.py | 67 ------------------- src/easyscience/Utils/classUtils.py | 94 -------------------------- src/easyscience/Utils/decorators.py | 100 ---------------------------- src/easyscience/Utils/string.py | 44 ------------ 6 files changed, 319 deletions(-) delete mode 100644 src/easyscience/Utils/Exceptions.py delete mode 100644 src/easyscience/Utils/__init__.py delete mode 100644 src/easyscience/Utils/classTools.py delete mode 100644 src/easyscience/Utils/classUtils.py delete mode 100644 src/easyscience/Utils/decorators.py delete mode 100644 src/easyscience/Utils/string.py diff --git a/src/easyscience/Utils/Exceptions.py b/src/easyscience/Utils/Exceptions.py deleted file mode 100644 index 8fd54ac0..00000000 --- a/src/easyscience/Utils/Exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project None: - cls = type(inst) - annotations = getattr(cls, '__annotations__', False) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': inst.__module__}) - cls.__perinstance = True - if annotations: - cls.__annotations__ = annotations - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - setattr(cls, name, LoggedProperty(*args, **kwargs)) - - -def addProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: - cls = type(inst) - annotations = getattr(cls, '__annotations__', False) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': __name__}) - cls.__perinstance = True - if annotations: - cls.__annotations__ = annotations - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - - setattr(cls, name, property(*args, **kwargs)) - - -def removeProp(inst: ComponentSerializer, name: str) -> None: - cls = type(inst) - if not hasattr(cls, '__perinstance'): - cls = type(cls.__name__, (cls,), {'__module__': __name__}) - cls.__perinstance = True - inst.__old_class__ = inst.__class__ - inst.__class__ = cls - delattr(cls, name) - - -def generatePath(model_obj: BasedBase, skip_first: bool = False) -> Tuple[List[int], List[str]]: - pars = model_obj.get_parameters() - start_idx = 0 + int(skip_first) - unique_names = [] - names = [] - for par in pars: - route = global_object.map.reverse_route(par.unique_name, model_obj.unique_name) - objs = [getattr(global_object.map.get_item_by_key(r), 'name') for r in route] - objs.reverse() - names.append('.'.join(objs[start_idx:])) - unique_names.append(par.unique_name) - return unique_names, names diff --git a/src/easyscience/Utils/classUtils.py b/src/easyscience/Utils/classUtils.py deleted file mode 100644 index df00895c..00000000 --- a/src/easyscience/Utils/classUtils.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project str: - """Return the function's docstring.""" - return self.func.__doc__ - - def __get__(self, obj, objtype): - """Support instance methods.""" - return functools.partial(self.__call__, obj) - - -def counted(func): - """ - Counts how many times a function has been called and adds a `func.calls` to it's properties - :param func: Function to be counted - :return: Results from function call - """ - - @functools.wraps(func) - def wrapped(*args, **kwargs): - wrapped.n_calls += 1 - return func(*args, **kwargs) - - wrapped.n_calls = 0 - return wrapped - - -def time_it(func): - """ - Times a function and reports the time either to the class' log or the base logger - :param func: function to be timed - :return: callable function with timer - """ - name = func.__module__ + "." + func.__name__ - time_logger = global_object.log.getLogger("timer." + name) - - @functools.wraps(func) - def _time_it(*args, **kwargs): - start = int(round(time() * 1000)) - try: - return func(*args, **kwargs) - finally: - end_ = int(round(time() * 1000)) - start - time_logger.debug( - f"\033[1;34;49mExecution time: {end_ if end_ > 0 else 0} ms\033[0m" - ) - - return _time_it - - -def deprecated(func): - """ - This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used. - """ - - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.warn_explicit( - "Call to deprecated function {}.".format(func.__name__), - category=DeprecationWarning, - filename=func.__code__.co_filename, - lineno=func.__code__.co_firstlineno + 1, - ) - return func(*args, **kwargs) - - return new_func diff --git a/src/easyscience/Utils/string.py b/src/easyscience/Utils/string.py deleted file mode 100644 index b99cf41e..00000000 --- a/src/easyscience/Utils/string.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project = 0: - s += '+' - if abs(f.numerator) != 1: - s += str(f.numerator) - elif f < 0: - s += '-' - s += c + dim - if f.denominator != 1: - s += '/' + str(f.denominator) - if t != 0: - s += ('+' if (t > 0 and s != '') else '') + str(Fraction(t).limit_denominator()) - if s == '': - s += '0' - parts.append(s) - return delim.join(parts) From 7522f14883bbb5db584149fc8ab2106b051cb302 Mon Sep 17 00:00:00 2001 From: Christian Dam Vedel <158568093+damskii9992@users.noreply.github.com> Date: Mon, 5 May 2025 15:59:31 +0200 Subject: [PATCH 36/58] Delete tests/integration_tests/Fitting directory --- .../integration_tests/Fitting/test_fitter.py | 323 ------------------ .../Fitting/test_multi_fitter.py | 251 -------------- 2 files changed, 574 deletions(-) delete mode 100644 tests/integration_tests/Fitting/test_fitter.py delete mode 100644 tests/integration_tests/Fitting/test_multi_fitter.py diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py deleted file mode 100644 index 780dfb21..00000000 --- a/tests/integration_tests/Fitting/test_fitter.py +++ /dev/null @@ -1,323 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project 0 % This does not work as some methods don't calculate error - assert item1.error == pytest.approx(0, abs=1e-1) - assert item1.value == pytest.approx(item2.value, abs=5e-3) - y_calc_ref = ref_sin(x) - assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) - assert result.residual == pytest.approx(sp_sin(x) - y_calc_ref, abs=1e-2) - - -@pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_basic_fit(fit_engine: AvailableMinimizers, with_errors): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - f = Fitter(sp_sin, sp_sin) - if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - args = [x, y] - kwargs = {} - if with_errors: - kwargs["weights"] = 1 / np.sqrt(y) - result = f.fit(*args, **kwargs) - - if fit_engine is not None: - assert result.minimizer_engine.package == fit_engine.name.lower() # Special case where minimizer matches package - assert sp_sin.phase.value == pytest.approx(ref_sin.phase.value, rel=1e-3) - assert sp_sin.offset.value == pytest.approx(ref_sin.offset.value, rel=1e-3) - - -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_fit_result(fit_engine): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - sp_ref1 = { - f"p{item1.unique_name}": item1.value - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) - } - sp_ref2 = { - f"p{item1.unique_name}": item2.value - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) - } - - f = Fitter(sp_sin, sp_sin) - - if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - - result = f.fit(x, y) - check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2) - - -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_basic_max_evaluations(fit_engine): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - f = Fitter(sp_sin, sp_sin) - if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - args = [x, y] - kwargs = {} - f.max_evaluations = 3 - try: - result = f.fit(*args, **kwargs) - # Result should not be the same as the reference - assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3) - assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3) - except FitError as e: - # DFO throws a different error - assert "Objective has been called MAXFUN times" in str(e) - - -@pytest.mark.parametrize("fit_engine,tolerance", [(None, 10), (AvailableMinimizers.LMFit, 10), (AvailableMinimizers.Bumps, 10), (AvailableMinimizers.DFO, 0.1)]) -def test_basic_tolerance(fit_engine, tolerance): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - f = Fitter(sp_sin, sp_sin) - if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - args = [x, y] - kwargs = {} - f.tolerance = tolerance - result = f.fit(*args, **kwargs) - # Result should not be the same as the reference - assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3) - assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3) - - -@pytest.mark.parametrize("fit_method", ["leastsq", "powell", "cobyla"]) -def test_lmfit_methods(fit_method): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - f = Fitter(sp_sin, sp_sin) - assert fit_method in f._minimizer.supported_methods() - result = f.fit(x, y, method=fit_method) - check_fit_results(result, sp_sin, ref_sin, x) - - -#@pytest.mark.xfail(reason="known bumps issue") -@pytest.mark.parametrize("fit_method", ["newton", "lm"]) -def test_bumps_methods(fit_method): - ref_sin = AbsSin(0.2, np.pi) - sp_sin = AbsSin(0.354, 3.05) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - sp_sin.offset.fixed = False - sp_sin.phase.fixed = False - - f = Fitter(sp_sin, sp_sin) - f.switch_minimizer("Bumps") - assert fit_method in f._minimizer.supported_methods() - result = f.fit(x, y, method=fit_method) - check_fit_results(result, sp_sin, ref_sin, x) - - -@pytest.mark.parametrize("fit_engine", [AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_dependent_parameter(fit_engine): - ref_sin = AbsSin(np.pi * 0.45, 0.45 * np.pi * 0.5) - sp_sin = AbsSin(1, 0.5) - - x = np.linspace(0, 5, 200) - y = ref_sin(x) - - f = Fitter(sp_sin, sp_sin) - - sp_sin.offset.make_dependent_on(dependency_expression='2*phase', dependency_map={"phase": sp_sin.phase}) - - if fit_engine is not None: - try: - f.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - - result = f.fit(x, y) - check_fit_results(result, sp_sin, ref_sin, x) - -@pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_2D_vectorized(fit_engine, with_errors): - x = np.linspace(0, 5, 200) - mm = AbsSin2D(0.3, 1.6) - m2 = AbsSin2D( - 0.1, 1.8 - ) # The fit is quite sensitive to the initial values :-( - X, Y = np.meshgrid(x, x) - XY = np.stack((X, Y), axis=2) - ff = Fitter(m2, m2) - if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - try: - args = [XY, mm(XY)] - kwargs = {"vectorized": True} - if with_errors: - kwargs["weights"] = 1 / np.sqrt(args[1]) - result = ff.fit(*args, **kwargs) - except FitError as e: - if "Unable to allocate" in str(e): - pytest.skip(msg="MemoryError - Matrix too large") - else: - raise e - assert result.n_pars == len(m2.get_fit_parameters()) - assert result.reduced_chi == pytest.approx(0, abs=1.5e-3) - assert result.success - assert np.all(result.x == XY) - y_calc_ref = m2(XY) - assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) - assert result.residual == pytest.approx(mm(XY) - y_calc_ref, abs=1e-2) - - -@pytest.mark.parametrize("with_errors", [False, True]) -@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO]) -def test_2D_non_vectorized(fit_engine, with_errors): - x = np.linspace(0, 5, 200) - mm = AbsSin2DL(0.3, 1.6) - m2 = AbsSin2DL( - 0.1, 1.8 - ) # The fit is quite sensitive to the initial values :-( - X, Y = np.meshgrid(x, x) - XY = np.stack((X, Y), axis=2) - ff = Fitter(m2, m2) - if fit_engine is not None: - try: - ff.switch_minimizer(fit_engine) - except AttributeError: - pytest.skip(msg=f"{fit_engine} is not installed") - try: - args = [XY, mm(XY.reshape(-1, 2))] - kwargs = {"vectorized": False} - if with_errors: - kwargs["weights"] = 1 / np.sqrt(args[1]) - result = ff.fit(*args, **kwargs) - except FitError as e: - if "Unable to allocate" in str(e): - pytest.skip(msg="MemoryError - Matrix too large") - else: - raise e - assert result.n_pars == len(m2.get_fit_parameters()) - assert result.reduced_chi == pytest.approx(0, abs=1.5e-3) - assert result.success - assert np.all(result.x == XY) - y_calc_ref = m2(XY.reshape(-1, 2)) - assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2) - assert result.residual == pytest.approx( - mm(XY.reshape(-1, 2)) - y_calc_ref, abs=1e-2 - ) diff --git a/tests/integration_tests/Fitting/test_multi_fitter.py b/tests/integration_tests/Fitting/test_multi_fitter.py deleted file mode 100644 index 1c3178de..00000000 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ /dev/null @@ -1,251 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Date: Mon, 5 May 2025 16:00:01 +0200 Subject: [PATCH 37/58] Delete tests/unit_tests/Fitting directory --- tests/unit_tests/Fitting/__init__.py | 4 - .../Fitting/minimizers/test_factory.py | 62 ---- .../Fitting/minimizers/test_minimizer_base.py | 206 ------------ .../minimizers/test_minimizer_bumps.py | 166 ---------- .../Fitting/minimizers/test_minimizer_dfo.py | 265 --------------- .../minimizers/test_minimizer_lmfit.py | 308 ------------------ tests/unit_tests/Fitting/test_fitter.py | 219 ------------- 7 files changed, 1230 deletions(-) delete mode 100644 tests/unit_tests/Fitting/__init__.py delete mode 100644 tests/unit_tests/Fitting/minimizers/test_factory.py delete mode 100644 tests/unit_tests/Fitting/minimizers/test_minimizer_base.py delete mode 100644 tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py delete mode 100644 tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py delete mode 100644 tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py delete mode 100644 tests/unit_tests/Fitting/test_fitter.py diff --git a/tests/unit_tests/Fitting/__init__.py b/tests/unit_tests/Fitting/__init__.py deleted file mode 100644 index 2f639849..00000000 --- a/tests/unit_tests/Fitting/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project MinimizerBase: - mock_fit_object = MagicMock() - mock_fit_function = MagicMock() - minimizer = factory(minimizer, mock_fit_object, mock_fit_function) - return minimizer - - @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.LMFit), ('leastsq', AvailableMinimizers.LMFit_leastsq), ('powell', AvailableMinimizers.LMFit_powell), ('cobyla', AvailableMinimizers.LMFit_cobyla), ('differential_evolution', AvailableMinimizers.LMFit_differential_evolution), ('least_squares', AvailableMinimizers.LMFit_scipy_least_squares)]) - def test_factory_lm_fit(self, minimizer_method, minimizer_enum): - minimizer = self.pull_minminizer(minimizer_enum) - assert minimizer._method == minimizer_method - assert minimizer.package == 'lmfit' - - @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('amoeba', AvailableMinimizers.Bumps), ('amoeba', AvailableMinimizers.Bumps_simplex), ('newton', AvailableMinimizers.Bumps_newton), ('lm', AvailableMinimizers.Bumps_lm)]) - def test_factory_bumps_fit(self, minimizer_method, minimizer_enum): - minimizer = self.pull_minminizer(minimizer_enum) - assert minimizer._method == minimizer_method - assert minimizer.package == 'bumps' - - @pytest.mark.parametrize('minimizer_method,minimizer_enum', [('leastsq', AvailableMinimizers.DFO), ('leastsq', AvailableMinimizers.DFO_leastsq)]) - def test_factory_dfo_fit(self, minimizer_method, minimizer_enum): - minimizer = self.pull_minminizer(minimizer_enum) - assert minimizer._method == minimizer_method - assert minimizer.package == 'dfo' - - -@pytest.mark.parametrize('minimizer_name,expected', [('LMFit', AvailableMinimizers.LMFit), ('LMFit_leastsq', AvailableMinimizers.LMFit_leastsq), ('LMFit_powell', AvailableMinimizers.LMFit_powell), ('LMFit_cobyla', AvailableMinimizers.LMFit_cobyla), ('LMFit_differential_evolution', AvailableMinimizers.LMFit_differential_evolution), ('LMFit_scipy_least_squares', AvailableMinimizers.LMFit_scipy_least_squares) ]) -def test_from_string_to_enum_lmfit(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -@pytest.mark.parametrize('minimizer_name,expected', [('Bumps', AvailableMinimizers.Bumps), ('Bumps_simplex', AvailableMinimizers.Bumps_simplex), ('Bumps_newton', AvailableMinimizers.Bumps_newton), ('Bumps_lm', AvailableMinimizers.Bumps_lm)]) -def test_from_string_to_enum_bumps(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -@pytest.mark.parametrize('minimizer_name,expected', [('DFO', AvailableMinimizers.DFO), ('DFO_leastsq', AvailableMinimizers.DFO_leastsq)]) -def test_from_string_to_enum_dfo(minimizer_name, expected): - assert from_string_to_enum(minimizer_name) == expected - - -def test_available_minimizers(): - assert AvailableMinimizers.LMFit - assert AvailableMinimizers.LMFit_leastsq - assert AvailableMinimizers.LMFit_powell - assert AvailableMinimizers.LMFit_cobyla - assert AvailableMinimizers.LMFit_differential_evolution - assert AvailableMinimizers.LMFit_scipy_least_squares - assert AvailableMinimizers.Bumps - assert AvailableMinimizers.Bumps_simplex - assert AvailableMinimizers.Bumps_newton - assert AvailableMinimizers.Bumps_lm - assert AvailableMinimizers.DFO - assert AvailableMinimizers.DFO_leastsq - assert len(AvailableMinimizers) == 12 \ No newline at end of file diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py deleted file mode 100644 index 1af9ae99..00000000 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_base.py +++ /dev/null @@ -1,206 +0,0 @@ -import pytest - -from unittest.mock import MagicMock - -from inspect import Parameter as InspectParameter -from inspect import Signature -from inspect import _empty - -from easyscience.fitting.minimizers.minimizer_base import MinimizerBase -from easyscience.fitting.minimizers.utils import FitError -from easyscience.variable import Parameter - -class TestMinimizerBase(): - @pytest.fixture - def minimizer(self): - # This avoids the error: TypeError: Can't instantiate abstract class with abstract methods __init__ - MinimizerBase.__abstractmethods__ = set() - MinimizerBase.supported_methods = MagicMock(return_value=['method']) - - self._mock_minimizer_enum = MagicMock(package='package', method='method') - minimizer = MinimizerBase( - obj='obj', - fit_function='fit_function', - minimizer_enum=self._mock_minimizer_enum - ) - return minimizer - - def test_init_exception(self): - # When Then - MinimizerBase.__abstractmethods__ = set() - MinimizerBase.supported_methods = MagicMock(return_value=['method']) - - # Expect - with pytest.raises(FitError): - MinimizerBase( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='package', method='not_a_method') - ) - - def test_init(self, minimizer: MinimizerBase): - assert minimizer._object == 'obj' - assert minimizer._original_fit_function == 'fit_function' - assert minimizer._method == 'method' - assert minimizer._cached_pars == {} - assert minimizer._cached_pars_vals == {} - assert minimizer._cached_model == None - assert minimizer._fit_function == None - - def test_enum(self, minimizer: MinimizerBase): - assert minimizer.enum == self._mock_minimizer_enum - - def test_evaluate(self, minimizer: MinimizerBase): - # When - minimizer._fit_function = MagicMock(return_value='fit_function_return') - minimizer._prepare_parameters = MagicMock(return_value={'prepared_parms_key': 'prepared_parms_val'}) - - # Then - result = minimizer.evaluate('x', minimizer_parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}) - - # Expect - assert result == 'fit_function_return' - minimizer._fit_function.assert_called_once_with('x', prepared_parms_key='prepared_parms_val', kwargs={'kwargs_key': 'kwargs_val'}) - minimizer._prepare_parameters.assert_called_once_with({'parms_key': 'parms_val'}) - - def test_evaluate_no_fit_function(self, minimizer: MinimizerBase): - # When - mock_fit_function = MagicMock() - minimizer._fit_function = None - minimizer._prepare_parameters = MagicMock(return_value={'prepared_parms_key': 'prepared_parms_val'}) - minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) - - # Then - minimizer.evaluate('x', minimizer_parameters={'parms_key': 'parms_val'}, kwargs={'kwargs_key': 'kwargs_val'}) - - # Expect - mock_fit_function.assert_called_once_with('x', prepared_parms_key='prepared_parms_val', kwargs={'kwargs_key': 'kwargs_val'}) - minimizer._prepare_parameters.assert_called_once_with({'parms_key': 'parms_val'}) - - def test_evaluate_no_parameters(self, minimizer: MinimizerBase): - # When - minimizer._fit_function = MagicMock(return_value='fit_function_return') - minimizer._prepare_parameters = MagicMock(return_value={'parms_key': 'parms_val'}) - - # Then - minimizer.evaluate('x') - - # Expect - minimizer._prepare_parameters.assert_called_once_with({}) - minimizer._fit_function.assert_called_once_with('x', parms_key='parms_val') - - def test_evaluate_exception(self, minimizer: MinimizerBase): - # When - minimizer_parameters = 'not dict type' - - # Then Expect - with pytest.raises(TypeError): - minimizer.evaluate('x', minimizer_parameters=minimizer_parameters) - - def test_prepare_parameters(self, minimizer: MinimizerBase): - # When - parameters = { - 'pa': 1, - 'pb': 2 - } - - minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), - 'c': MagicMock() - } - minimizer._cached_pars['a'].value = 3 - minimizer._cached_pars['b'].value = 4 - minimizer._cached_pars['c'].value = 5 - - # Then - parameters = minimizer._prepare_parameters(parameters) - - # Expect - assert parameters == { - 'pa': 1, - 'pb': 2, - 'pc': 5 - } - - def test_generate_fit_function(self, minimizer: MinimizerBase) -> None: - # When - minimizer._original_fit_function = MagicMock(return_value='fit_function_result') - - minimizer._object = MagicMock() - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.unique_name = 'mock_parm_1' - mock_parm_1.value = 1.0 - mock_parm_1.error = 0.1 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.unique_name = 'mock_parm_2' - mock_parm_2.value = 2.0 - mock_parm_2.error = 0.2 - minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) - - # Then - fit_function = minimizer._generate_fit_function() - fit_function_result = fit_function([10.0]) - - # Expect - assert 'fit_function_result' == fit_function_result - minimizer._original_fit_function.assert_called_once_with([10.0]) - assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 - assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 - assert str(fit_function.__signature__) == '(x, pmock_parm_1=1.0, pmock_parm_2=2.0)' - - def test_create_signature(self, minimizer: MinimizerBase) -> None: - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - pars = {1: mock_parm_1, 2: mock_parm_2} - - # Then - signature = minimizer._create_signature(pars) - - # Expect - wrapped_parameters = [ - InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty), - InspectParameter('p1', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=1.0), - InspectParameter('p2', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty, default=2.0) - ] - expected_signature = Signature(wrapped_parameters) - assert signature == expected_signature - - def test_get_method_dict(self, minimizer: MinimizerBase) -> None: - # When Then - result = minimizer._get_method_kwargs() - - # Expect - assert result == {'method': 'method'} - - def test_get_method_dict_no_self(self, minimizer: MinimizerBase) -> None: - # When - minimizer._method = None - - # Then - result = minimizer._get_method_kwargs() - - # Expect - assert result == {} - - def test_get_method_dict_supported_method(self, minimizer: MinimizerBase) -> None: - # When - minimizer.supported_methods = MagicMock(return_value=['supported_method']) - - # Then - result = minimizer._get_method_kwargs('supported_method') - - # Expect - assert result == {'method': 'supported_method'} - - def test_get_method_dict_not_supported_method(self, minimizer: MinimizerBase) -> None: - # When - minimizer.supported_methods = MagicMock(return_value=['supported_method']) - - # Then Expect - with pytest.raises(FitError): - result = minimizer._get_method_kwargs('not_supported_method') - diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py deleted file mode 100644 index 7feaf8e0..00000000 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_bumps.py +++ /dev/null @@ -1,166 +0,0 @@ -import pytest - -from unittest.mock import MagicMock -import numpy as np - -import easyscience.fitting.minimizers.minimizer_bumps - -from easyscience.fitting.minimizers.minimizer_bumps import Bumps -from easyscience.fitting.minimizers.utils import FitError - - -class TestBumpsFit(): - @pytest.fixture - def minimizer(self) -> Bumps: - minimizer = Bumps( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='bumps', method='amoeba') - ) - return minimizer - - def test_init(self, minimizer: Bumps) -> None: - assert minimizer._p_0 == {} - assert minimizer.package == 'bumps' - - def test_init_exception(self) -> None: - with pytest.raises(FitError): - Bumps( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='bumps', method='not_amoeba') - ) - - def test_all_methods(self, minimizer: Bumps) -> None: - # When Then Expect - assert minimizer.all_methods() == ['amoeba', 'de', 'dream', 'newton', 'scipy.leastsq', 'lm'] - - def test_supported_methods(self, minimizer: Bumps) -> None: - # When Then Expect - assert set(minimizer.supported_methods()) == set(['scipy.leastsq','newton', 'lm', 'amoeba']) - - def test_fit(self, minimizer: Bumps, monkeypatch) -> None: - # When - from easyscience import global_object - global_object.stack.enabled = False - - mock_bumps_fit = MagicMock(return_value='fit') - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "bumps_fit", mock_bumps_fit) - - mock_FitProblem = MagicMock(return_value='fit_problem') - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitProblem", mock_FitProblem) - - mock_model = MagicMock() - mock_model_function = MagicMock(return_value=mock_model) - minimizer._make_model = MagicMock(return_value=mock_model_function) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - cached_par = MagicMock() - cached_par.value = 1 - cached_pars = {'mock_parm_1': cached_par} - minimizer._cached_pars = cached_pars - - # Then - result = minimizer.fit(x=1.0, y=2.0) - - # Expect - assert result == 'gen_fit_results' - mock_bumps_fit.assert_called_once_with('fit_problem', method='amoeba') - minimizer._make_model.assert_called_once_with(parameters=None) - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) - minimizer._gen_fit_results.assert_called_once_with('fit') - mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) - mock_FitProblem.assert_called_once_with(mock_model) - - - def test_make_model(self, minimizer: Bumps, monkeypatch) -> None: - # When - mock_fit_function = MagicMock(return_value=np.array([11, 22])) - minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) - - mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' - minimizer.convert_to_par_object = MagicMock(return_value='converted_parm_1') - - mock_Curve = MagicMock(return_value='curve') - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "Curve", mock_Curve) - - # Then - model = minimizer._make_model(parameters=[mock_parm_1]) - curve_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) - - # Expect - minimizer._generate_fit_function.assert_called_once_with() - assert mock_Curve.call_args[0][0] == mock_fit_function - assert all(mock_Curve.call_args[0][1] == np.array([1,2])) - assert all(mock_Curve.call_args[0][2] == np.array([10,20])) - assert curve_for_model == 'curve' - - def test_set_parameter_fit_result_no_stack_status(self, minimizer: Bumps): - # When - minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), - } - minimizer._cached_pars['a'].value = 'a' - minimizer._cached_pars['b'].value = 'b' - - mock_cached_model = MagicMock() - mock_cached_model._pnames = ['pa', 'pb'] - minimizer._cached_model = mock_cached_model - - mock_fit_result = MagicMock() - mock_fit_result.x = [1.0, 2.0] - mock_fit_result.dx = [0.1, 0.2] - - # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) - - # Expect - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.1 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.2 - - def test_gen_fit_results(self, minimizer: Bumps, monkeypatch): - # When - mock_domain_fit_results = MagicMock() - mock_FitResults = MagicMock(return_value=mock_domain_fit_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_bumps, "FitResults", mock_FitResults) - - mock_fit_result = MagicMock() - mock_fit_result.success = True - - mock_cached_model = MagicMock() - mock_cached_model.x = 'x' - mock_cached_model.y = 'y' - mock_cached_model.dy = 'dy' - mock_cached_model._pnames = ['ppar_1', 'ppar_2'] - minimizer._cached_model = mock_cached_model - - mock_cached_par_1 = MagicMock() - mock_cached_par_1.value = 'par_value_1' - mock_cached_par_2 = MagicMock() - mock_cached_par_2.value = 'par_value_2' - minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} - - minimizer._p_0 = 'p_0' - minimizer.evaluate = MagicMock(return_value='evaluate') - - # Then - domain_fit_results = minimizer._gen_fit_results(mock_fit_result, **{'kwargs_set_key': 'kwargs_set_val'}) - - # Expect - assert domain_fit_results == mock_domain_fit_results - assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' - assert domain_fit_results.success == True - assert domain_fit_results.y_obs == 'y' - assert domain_fit_results.x == 'x' - assert domain_fit_results.p == {'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} - assert domain_fit_results.p0 == 'p_0' - assert domain_fit_results.y_calc == 'evaluate' - assert domain_fit_results.y_err == 'dy' - assert str(domain_fit_results.minimizer_engine) == "" - assert domain_fit_results.fit_args is None - minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'}) diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py deleted file mode 100644 index 7d8ca3fe..00000000 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py +++ /dev/null @@ -1,265 +0,0 @@ -import pytest - -from unittest.mock import MagicMock -import numpy as np - -import easyscience.fitting.minimizers.minimizer_dfo -from easyscience.variable import Parameter - -from easyscience.fitting.minimizers.minimizer_dfo import DFO -from easyscience.fitting.minimizers.utils import FitError - - -class TestDFOFit(): - @pytest.fixture - def minimizer(self) -> DFO: - minimizer = DFO( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='dfo', method='leastsq') - ) - return minimizer - - def test_init(self, minimizer: DFO) -> None: - assert minimizer._p_0 == {} - assert minimizer.package == 'dfo' - - def test_init_exception(self) -> None: - with pytest.raises(FitError): - DFO( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='dfo', method='not_leastsq') - ) - - def test_supported_methods(self, minimizer: DFO) -> None: - # When Then Expect - assert minimizer.supported_methods() == ['leastsq'] - - def test_supported_methods(self, minimizer: DFO) -> None: - # When Then Expect - assert minimizer.supported_methods() == ['leastsq'] - - def test_fit(self, minimizer: DFO) -> None: - # When - from easyscience import global_object - global_object.stack.enabled = False - - mock_model = MagicMock() - mock_model_function = MagicMock(return_value=mock_model) - minimizer._make_model = MagicMock(return_value=mock_model_function) - minimizer._dfo_fit = MagicMock(return_value='fit') - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - cached_par = MagicMock() - cached_par.value = 1 - cached_pars = {'mock_parm_1': cached_par} - minimizer._cached_pars = cached_pars - - # Then - result = minimizer.fit(x=1.0, y=2.0) - - # Expect - assert result == 'gen_fit_results' - minimizer._dfo_fit.assert_called_once_with(cached_pars, mock_model) - minimizer._make_model.assert_called_once_with(parameters=None) - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) - minimizer._gen_fit_results.assert_called_once_with('fit', 1.4142135623730951) - mock_model_function.assert_called_once_with(1.0, 2.0, 1.4142135623730951) - - def test_generate_fit_function(self, minimizer: DFO) -> None: - # When - minimizer._original_fit_function = MagicMock(return_value='fit_function_result') - - minimizer._object = MagicMock() - mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' - mock_parm_1.value = 1.0 - mock_parm_1.error = 0.1 - mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' - mock_parm_2.value = 2.0 - mock_parm_2.error = 0.2 - minimizer._object.get_fit_parameters = MagicMock(return_value=[mock_parm_1, mock_parm_2]) - - # Then - fit_function = minimizer._generate_fit_function() - fit_function_result = fit_function([10.0]) - - # Expect - assert 'fit_function_result' == fit_function_result - minimizer._original_fit_function.assert_called_once_with([10.0]) - assert minimizer._cached_pars['mock_parm_1'] == mock_parm_1 - assert minimizer._cached_pars['mock_parm_2'] == mock_parm_2 - - def test_make_model(self, minimizer: DFO) -> None: - # When - mock_fit_function = MagicMock(return_value=np.array([11, 22])) - minimizer._generate_fit_function = MagicMock(return_value=mock_fit_function) - - mock_parm_1 = MagicMock() - mock_parm_1.unique_name = 'mock_parm_1' - mock_parm_1.value = 1000.0 - mock_parm_2 = MagicMock() - mock_parm_2.unique_name = 'mock_parm_2' - mock_parm_2.value = 2000.0 - - # Then - model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2]) - residuals_for_model = model(x=np.array([1, 2]), y=np.array([10, 20]), weights=np.array([100, 200])) - - # Expect - minimizer._generate_fit_function.assert_called_once_with() - assert all(np.array([-0.01, -0.01]) == residuals_for_model(np.array([1111, 2222]))) - assert all(mock_fit_function.call_args[0][0] == np.array([1, 2])) - assert mock_fit_function.call_args[1] == {'pmock_parm_1': 1111, 'pmock_parm_2': 2222} - - def test_set_parameter_fit_result_no_stack_status(self, minimizer: DFO): - # When - minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), - } - minimizer._cached_pars['a'].value = 'a' - minimizer._cached_pars['b'].value = 'b' - - mock_fit_result = MagicMock() - mock_fit_result.x = [1.0, 2.0] - mock_fit_result.jacobian = 'jacobian' - mock_fit_result.resid = 'resid' - - minimizer._error_from_jacobian = MagicMock(return_value=np.array([[0.1, 0.0], [0.0, 0.2]])) - - # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) - - # Expect - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.1 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.2 - minimizer._error_from_jacobian.assert_called_once_with('jacobian', 'resid', 0.95) - - def test_gen_fit_results(self, minimizer: DFO, monkeypatch): - # When - mock_domain_fit_results = MagicMock() - mock_FitResults = MagicMock(return_value=mock_domain_fit_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "FitResults", mock_FitResults) - - mock_fit_result = MagicMock() - mock_fit_result.flag = False - - mock_cached_model = MagicMock() - mock_cached_model.x = 'x' - mock_cached_model.y = 'y' - minimizer._cached_model = mock_cached_model - - mock_cached_par_1 = MagicMock() - mock_cached_par_1.value = 'par_value_1' - mock_cached_par_2 = MagicMock() - mock_cached_par_2.value = 'par_value_2' - minimizer._cached_pars = {'par_1': mock_cached_par_1, 'par_2': mock_cached_par_2} - - minimizer._p_0 = 'p_0' - minimizer.evaluate = MagicMock(return_value='evaluate') - - # Then - domain_fit_results = minimizer._gen_fit_results(mock_fit_result, 'weights', **{'kwargs_set_key': 'kwargs_set_val'}) - - # Expect - assert domain_fit_results == mock_domain_fit_results - assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' - assert domain_fit_results.success == True - assert domain_fit_results.y_obs == 'y' - assert domain_fit_results.x == 'x' - assert domain_fit_results.p == {'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'} - assert domain_fit_results.p0 == 'p_0' - assert domain_fit_results.y_calc == 'evaluate' - assert domain_fit_results.y_err == 'weights' - assert str(domain_fit_results.minimizer_engine) == "" - assert domain_fit_results.fit_args is None - minimizer.evaluate.assert_called_once_with('x', minimizer_parameters={'ppar_1': 'par_value_1', 'ppar_2': 'par_value_2'}) - - def test_dfo_fit(self, minimizer: DFO, monkeypatch): - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_1.min = 0.1 - mock_parm_1.max = 10.0 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - mock_parm_2.min = 0.2 - mock_parm_2.max = 20.0 - pars = {1: mock_parm_1, 2: mock_parm_2} - - kwargs = {'kwargs_set_key': 'kwargs_set_val'} - - mock_dfols = MagicMock() - mock_results = MagicMock() - mock_results.msg = 'Success' - mock_dfols.solve = MagicMock(return_value=mock_results) - - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) - - # Then - results = minimizer._dfo_fit(pars, 'model', **kwargs) - - # Expect - assert results == mock_results - assert mock_dfols.solve.call_args[0][0] == 'model' - assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([0.1, 0.2])) - assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) - assert mock_dfols.solve.call_args[1]['scaling_within_bounds'] is True - assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' - - def test_dfo_fit_no_scaling(self, minimizer: DFO, monkeypatch): - # When - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_1.min = -np.inf - mock_parm_1.max = 10.0 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - mock_parm_2.min = 0.2 - mock_parm_2.max = 20.0 - pars = {1: mock_parm_1, 2: mock_parm_2} - - kwargs = {'kwargs_set_key': 'kwargs_set_val'} - - mock_dfols = MagicMock() - mock_results = MagicMock() - mock_results.msg = 'Success' - mock_dfols.solve = MagicMock(return_value=mock_results) - - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) - - # Then - results = minimizer._dfo_fit(pars, 'model', **kwargs) - - # Expect - assert results == mock_results - assert mock_dfols.solve.call_args[0][0] == 'model' - assert all(mock_dfols.solve.call_args[0][1] == np.array([1., 2.])) - assert all(mock_dfols.solve.call_args[1]['bounds'][0] == np.array([-np.inf, 0.2])) - assert all(mock_dfols.solve.call_args[1]['bounds'][1] == np.array([10., 20.])) - assert not 'scaling_within_bounds' in list(mock_dfols.solve.call_args[1].keys()) - assert 'kwargs_set_key' in list(mock_dfols.solve.call_args[1].keys()) - assert mock_dfols.solve.call_args[1]['kwargs_set_key'] == 'kwargs_set_val' - - def test_dfo_fit_exception(self, minimizer: DFO, monkeypatch): - # When - pars = {1: MagicMock(Parameter)} - kwargs = {'kwargs_set_key': 'kwargs_set_val'} - - mock_dfols = MagicMock() - mock_results = MagicMock() - mock_results.msg = 'Failed' - mock_dfols.solve = MagicMock(return_value=mock_results) - - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_dfo, "dfols", mock_dfols) - - # Then Expect - with pytest.raises(FitError): - minimizer._dfo_fit(pars, 'model', **kwargs) \ No newline at end of file diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py deleted file mode 100644 index 92316bc6..00000000 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py +++ /dev/null @@ -1,308 +0,0 @@ -import pytest - -from unittest.mock import MagicMock - -import easyscience.fitting.minimizers.minimizer_lmfit -from easyscience.fitting.minimizers.minimizer_lmfit import LMFit -from easyscience.variable import Parameter -from lmfit import Parameter as LMParameter -from easyscience.fitting.minimizers.utils import FitError - - -class TestLMFit(): - @pytest.fixture - def minimizer(self) -> LMFit: - minimizer = LMFit( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='lm', method='leastsq') - ) - return minimizer - - def test_init(self, minimizer: LMFit) -> None: - assert minimizer.package == 'lmfit' - - def test_init_exception(self) -> None: - with pytest.raises(FitError): - LMFit( - obj='obj', - fit_function='fit_function', - minimizer_enum=MagicMock(package='dfo', method='not_leastsq') - ) - - def test_make_model(self, minimizer: LMFit, monkeypatch) -> None: - # When - mock_lm_model = MagicMock() - mock_LMModel = MagicMock(return_value=mock_lm_model) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMModel", mock_LMModel) - minimizer._generate_fit_function = MagicMock(return_value='model') - mock_parm_1 = MagicMock(LMParameter) - mock_parm_1.value = 1.0 - mock_parm_1.min = -10.0 - mock_parm_1.max = 10.0 - mock_parm_2 = MagicMock(LMParameter) - mock_parm_2.value = 2.0 - mock_parm_2.min = -20.0 - mock_parm_2.max = 20.0 - pars = {'key_1': mock_parm_1, 'key_2': mock_parm_2} - - # Then - model = minimizer._make_model(pars=pars) - - # Expect - minimizer._generate_fit_function.assert_called_once_with() - mock_LMModel.assert_called_once_with('model', independent_vars=['x'], param_names=['pkey_1', 'pkey_2']) - mock_lm_model.set_param_hint.assert_called_with('pkey_2', value=2.0, min=-20.0, max=20.0) - assert mock_lm_model.set_param_hint.call_count == 2 - assert model == mock_lm_model - - def test_make_model_no_pars(self, minimizer: LMFit, monkeypatch) -> None: - # When - mock_lm_model = MagicMock() - mock_LMModel = MagicMock(return_value=mock_lm_model) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMModel", mock_LMModel) - minimizer._generate_fit_function = MagicMock(return_value='model') - mock_parm_1 = MagicMock(Parameter) - mock_parm_1.value = 1.0 - mock_parm_1.min = -10.0 - mock_parm_1.max = 10.0 - mock_parm_2 = MagicMock(Parameter) - mock_parm_2.value = 2.0 - mock_parm_2.min = -20.0 - mock_parm_2.max = 20.0 - minimizer._cached_pars = {'key_1': mock_parm_1, 'key_2': mock_parm_2} - - # Then - model = minimizer._make_model() - - # Expect - minimizer._generate_fit_function.assert_called_once_with() - mock_LMModel.assert_called_once_with('model', independent_vars=['x'], param_names=['pkey_1', 'pkey_2']) - mock_lm_model.set_param_hint.assert_called_with('pkey_2', value=2.0, min=-20.0, max=20.0) - assert mock_lm_model.set_param_hint.call_count == 2 - assert model == mock_lm_model - - def test_fit(self, minimizer: LMFit) -> None: - # When - from easyscience import global_object - global_object.stack.enabled = False - - mock_model = MagicMock() - mock_model.fit = MagicMock(return_value='fit') - minimizer._make_model = MagicMock(return_value=mock_model) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - # Then - result = minimizer.fit(x=1.0, y=2.0) - - # Expect - assert result == 'gen_fit_results' - mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='leastsq') - minimizer._make_model.assert_called_once_with() - minimizer._set_parameter_fit_result.assert_called_once_with('fit', False) - minimizer._gen_fit_results.assert_called_once_with('fit') - - def test_fit_model(self, minimizer: LMFit) -> None: - # When - mock_model = MagicMock() - mock_model.fit = MagicMock(return_value='fit') - minimizer._make_model = MagicMock(return_value=mock_model) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - # Then - minimizer.fit(x=1.0, y=2.0, model=mock_model) - - # Expect - mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='leastsq') - minimizer._make_model.assert_not_called() - - def test_fit_method(self, minimizer: LMFit) -> None: - # When - mock_model = MagicMock() - mock_model.fit = MagicMock(return_value='fit') - minimizer._make_model = MagicMock(return_value=mock_model) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - minimizer.supported_methods = MagicMock(return_value=['method_passed']) - minimizer.all_methods = MagicMock(return_value=['method_passed']) - - # Then - minimizer.fit(x=1.0, y=2.0, method='method_passed') - - # Expect - mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={}, method='method_passed') - minimizer.supported_methods.assert_called_once_with() - - def test_fit_kwargs(self, minimizer: LMFit) -> None: - # When - mock_model = MagicMock() - mock_model.fit = MagicMock(return_value='fit') - minimizer._make_model = MagicMock(return_value=mock_model) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - # Then - minimizer.fit(x=1.0, y=2.0, minimizer_kwargs={'minimizer_key': 'minimizer_val'}, engine_kwargs={'engine_key': 'engine_val'}) - - # Expect - mock_model.fit.assert_called_once_with(2.0, x=1.0, weights=0.7071067811865475, max_nfev=None, fit_kws={'minimizer_key': 'minimizer_val'}, method='leastsq', engine_key='engine_val') - - def test_fit_exception(self, minimizer: LMFit) -> None: - # When - minimizer._make_model = MagicMock(side_effect=Exception('Exception')) - minimizer._set_parameter_fit_result = MagicMock() - minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') - - # Then Expect - with pytest.raises(FitError): - minimizer.fit(x=1.0, y=2.0) - - def test_convert_to_pars_obj(self, minimizer: LMFit, monkeypatch) -> None: - # When - minimizer._object = MagicMock() - minimizer._object.get_fit_parameters = MagicMock(return_value = ['parm_1', 'parm_2']) - - minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') - - mock_lm_parameter = MagicMock() - mock_lm_parameter.add_many = MagicMock(return_value='add_many') - mock_LMParameters = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameters", mock_LMParameters) - - # Then - pars = minimizer.convert_to_pars_obj() - - # Expect - assert pars == 'add_many' - assert minimizer.convert_to_par_object.call_count == 2 - minimizer._object.get_fit_parameters.assert_called_once_with() - minimizer.convert_to_par_object.assert_called_with('parm_2') - mock_lm_parameter.add_many.assert_called_once_with(['convert_to_par_object', 'convert_to_par_object']) - - def test_convert_to_pars_obj_with_parameters(self, minimizer: LMFit, monkeypatch) -> None: - # When - minimizer.convert_to_par_object = MagicMock(return_value='convert_to_par_object') - - mock_lm_parameter = MagicMock() - mock_lm_parameter.add_many = MagicMock(return_value='add_many') - mock_LMParameters = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameters", mock_LMParameters) - - # Then - pars = minimizer.convert_to_pars_obj(['parm_1', 'parm_2']) - - # Expect - assert pars == 'add_many' - assert minimizer.convert_to_par_object.call_count == 2 - minimizer.convert_to_par_object.assert_called_with('parm_2') - mock_lm_parameter.add_many.assert_called_once_with(['convert_to_par_object', 'convert_to_par_object']) - - def test_convert_to_par_object(self, minimizer: LMFit, monkeypatch) -> None: - # When - mock_lm_parameter = MagicMock() - mock_LMParameter = MagicMock(return_value=mock_lm_parameter) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "LMParameter", mock_LMParameter) - - mock_parm = MagicMock(Parameter) - mock_parm.value = 1.0 - mock_parm.fixed = True - mock_parm.min = -10.0 - mock_parm.max = 10.0 - mock_parm.unique_name = 'key_converted' - - # Then - par = minimizer.convert_to_par_object(mock_parm) - - # Expect - assert par == mock_lm_parameter - mock_LMParameter.assert_called_once_with('pkey_converted', value=1.0, vary=False, min=-10.0, max=10.0, expr=None, brute_step=None) - - def test_set_parameter_fit_result_no_stack_status(self, minimizer: LMFit) -> None: - # When - minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), - } - minimizer._cached_pars['a'].value = 'a' - minimizer._cached_pars['b'].value = 'b' - - mock_param_a = MagicMock() - mock_param_a.value = 1.0 - mock_param_a.stderr = 0.1 - mock_param_b = MagicMock - mock_param_b.value = 2.0 - mock_param_b.stderr = 0.2 - mock_fit_result = MagicMock() - mock_fit_result.params = {'pa': mock_param_a, 'pb': mock_param_b} - mock_fit_result.errorbars = True - - # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) - - # Expect - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.1 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.2 - - def test_set_parameter_fit_result_no_stack_status_no_error(self, minimizer: LMFit) -> None: - # When - minimizer._cached_pars = { - 'a': MagicMock(), - 'b': MagicMock(), - } - minimizer._cached_pars['a'].value = 'a' - minimizer._cached_pars['b'].value = 'b' - - mock_param_a = MagicMock() - mock_param_a.value = 1.0 - mock_param_a.stderr = 0.1 - mock_param_b = MagicMock - mock_param_b.value = 2.0 - mock_param_b.stderr = 0.2 - mock_fit_result = MagicMock() - mock_fit_result.params = {'pa': mock_param_a, 'pb': mock_param_b} - mock_fit_result.errorbars = False - - # Then - minimizer._set_parameter_fit_result(mock_fit_result, False) - - # Expect - assert minimizer._cached_pars['a'].value == 1.0 - assert minimizer._cached_pars['a'].error == 0.0 - assert minimizer._cached_pars['b'].value == 2.0 - assert minimizer._cached_pars['b'].error == 0.0 - - def test_gen_fit_results(self, minimizer: LMFit, monkeypatch) -> None: - # When - mock_domain_fit_results = MagicMock() - mock_FitResults = MagicMock(return_value=mock_domain_fit_results) - monkeypatch.setattr(easyscience.fitting.minimizers.minimizer_lmfit, "FitResults", mock_FitResults) - - mock_fit_result = MagicMock() - mock_fit_result.success ='success' - mock_fit_result.data = 'data' - mock_fit_result.userkws = {'x': 'x_val'} - mock_fit_result.values = 'values' - mock_fit_result.init_values = 'init_values' - mock_fit_result.best_fit = 'best_fit' - mock_fit_result.weights = 10 - - # Then - domain_fit_results = minimizer._gen_fit_results(mock_fit_result, **{'kwargs_set_key': 'kwargs_set_val'}) - - # Expect - assert domain_fit_results == mock_domain_fit_results - assert domain_fit_results.kwargs_set_key == 'kwargs_set_val' - assert domain_fit_results.success == 'success' - assert domain_fit_results.y_obs == 'data' - assert domain_fit_results.x == 'x_val' - assert domain_fit_results.p == 'values' - assert domain_fit_results.p0 == 'init_values' - assert domain_fit_results.y_calc == 'best_fit' - assert domain_fit_results.y_err == 0.1 - assert str(domain_fit_results.minimizer_engine) == "" - assert domain_fit_results.fit_args is None - diff --git a/tests/unit_tests/Fitting/test_fitter.py b/tests/unit_tests/Fitting/test_fitter.py deleted file mode 100644 index 992225ce..00000000 --- a/tests/unit_tests/Fitting/test_fitter.py +++ /dev/null @@ -1,219 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -import numpy as np -import easyscience.fitting.fitter -from easyscience.fitting.fitter import Fitter -from easyscience.fitting.available_minimizers import AvailableMinimizers - - -class TestFitter(): - @pytest.fixture - def fitter(self, monkeypatch): - monkeypatch.setattr(Fitter, '_update_minimizer', MagicMock()) - self.mock_fit_object = MagicMock() - self.mock_fit_function = MagicMock() - return Fitter(self.mock_fit_object, self.mock_fit_function) - - def test_constructor(self, fitter: Fitter): - # When Then Expect - assert fitter._fit_object == self.mock_fit_object - assert fitter._fit_function == self.mock_fit_function - assert fitter._dependent_dims is None - assert fitter._enum_current_minimizer is None #== AvailableMinimizers.LMFit_leastsq - assert fitter._minimizer is None - fitter._update_minimizer.assert_called_once_with(AvailableMinimizers.LMFit_leastsq) - - def test_make_model(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.make_model = MagicMock(return_value='model') - fitter._minimizer = mock_minimizer - - # Then - model = fitter.make_model('pars') - - # Expect - assert model == 'model' - mock_minimizer.make_model.assert_called_once_with('pars') - - def test_evaluate(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.evaluate = MagicMock(return_value='result') - fitter._minimizer = mock_minimizer - - # Then - result = fitter.evaluate('pars') - - # Expect - assert result == 'result' - mock_minimizer.evaluate.assert_called_once_with('pars') - - def test_convert_to_pars_obj(self, fitter: Fitter): - # When - mock_minimizer = MagicMock() - mock_minimizer.convert_to_pars_obj = MagicMock(return_value='obj') - fitter._minimizer = mock_minimizer - - # Then - obj = fitter.convert_to_pars_obj('pars') - - # Expect - assert obj == 'obj' - mock_minimizer.convert_to_pars_obj.assert_called_once_with('pars') - - def test_initialize(self, fitter: Fitter): - # When - mock_fit_object = MagicMock() - mock_fit_function = MagicMock() - - # Then - fitter.initialize(mock_fit_object, mock_fit_function) - - # Expect - assert fitter._fit_object == mock_fit_object - assert fitter._fit_function == mock_fit_function - fitter._update_minimizer.count(2) - - def test_create(self, fitter: Fitter, monkeypatch): - # When - fitter._update_minimizer = MagicMock() - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) - - # Then - fitter.create('great-minimizer') - - # Expect - mock_string_to_enum.assert_called_once_with('great-minimizer') - fitter._update_minimizer.assert_called_once_with(10) - - def test_switch_minimizer(self, fitter: Fitter, monkeypatch): - # When - mock_minimizer = MagicMock() - fitter._minimizer = mock_minimizer - mock_string_to_enum = MagicMock(return_value=10) - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) - - # Then - fitter.switch_minimizer('great-minimizer') - - # Expect - fitter._update_minimizer.count(2) - mock_string_to_enum.assert_called_once_with('great-minimizer') - - def test_update_minimizer(self, monkeypatch): - # When - mock_fit_object = MagicMock() - mock_fit_function = MagicMock() - - mock_string_to_enum = MagicMock(return_value=10) - mock_factory = MagicMock(return_value='minimizer') - monkeypatch.setattr(easyscience.fitting.fitter, 'from_string_to_enum', mock_string_to_enum) - monkeypatch.setattr(easyscience.fitting.fitter, 'factory', mock_factory) - fitter = Fitter(mock_fit_object, mock_fit_function) - - # Then - fitter._update_minimizer('great-minimizer') - - # Expect - assert fitter._enum_current_minimizer == 'great-minimizer' - assert fitter._minimizer == 'minimizer' - - def test_available_minimizers(self, fitter: Fitter): - # When - minimizers = fitter.available_minimizers - - # Then Expect - assert minimizers == [ - 'LMFit', 'LMFit_leastsq', 'LMFit_powell', 'LMFit_cobyla', 'LMFit_differential_evolution', 'LMFit_scipy_least_squares', - 'Bumps', 'Bumps_simplex', 'Bumps_newton', 'Bumps_lm', - 'DFO', 'DFO_leastsq' - ] - - def test_minimizer(self, fitter: Fitter): - # When - fitter._minimizer = 'minimizer' - - # Then - minimizer = fitter.minimizer - - # Expect - assert minimizer == 'minimizer' - - def test_fit_function(self, fitter: Fitter): - # When Then - fit_function = fitter.fit_function - - # Expect - assert fit_function == self.mock_fit_function - - def test_set_fit_function(self, fitter: Fitter): - # When - fitter._enum_current_minimizer = 'current_minimizer' - - # Then - fitter.fit_function = 'new-fit-function' - - # Expect - assert fitter._fit_function == 'new-fit-function' - fitter._update_minimizer.assert_called_with('current_minimizer') - - def test_fit_object(self, fitter: Fitter): - # When Then - fit_object = fitter.fit_object - - # Expect - assert fit_object == self.mock_fit_object - - def test_set_fit_object(self, fitter: Fitter): - # When - fitter._enum_current_minimizer = 'current_minimizer' - - # Then - fitter.fit_object = 'new-fit-object' - - # Expect - assert fitter.fit_object == 'new-fit-object' - fitter._update_minimizer.assert_called_with('current_minimizer') - - def test_fit(self, fitter: Fitter): - # When - fitter._precompute_reshaping = MagicMock(return_value=('x_fit', 'x_new', 'y_new', 'weights', 'dims')) - fitter._fit_function_wrapper = MagicMock(return_value='wrapped_fit_function') - fitter._post_compute_reshaping = MagicMock(return_value='fit_result') - fitter._minimizer = MagicMock() - fitter._minimizer.fit = MagicMock(return_value='result') - - # Then - result = fitter.fit('x', 'y', 'weights', 'vectorized') - - # Expect - fitter._precompute_reshaping.assert_called_once_with('x', 'y', 'weights', 'vectorized') - fitter._fit_function_wrapper.assert_called_once_with('x_new', flatten=True) - fitter._post_compute_reshaping.assert_called_once_with('result', 'x', 'y') - assert result == 'fit_result' - assert fitter._dependent_dims == 'dims' - assert fitter._fit_function == self.mock_fit_function - - def test_post_compute_reshaping(self, fitter: Fitter): - # When - fit_result = MagicMock() - fit_result.y_calc = np.array([[10], [20], [30]]) - fit_result.y_err = np.array([[40], [50], [60]]) - x = np.array([1, 2, 3]) - y = np.array([4, 5, 6]) - - # Then - result = fitter._post_compute_reshaping(fit_result, x, y) - - # Expect - assert np.array_equal(result.y_calc, np.array([10, 20, 30])) - assert np.array_equal(result.y_err, np.array([40, 50, 60])) - assert np.array_equal(result.x, x) - assert np.array_equal(result.y_obs, y) - -# TODO -# def test_fit_function_wrapper() -# def test_precompute_reshaping() From b2f3d13a31814c06ae3076ced4a99454e7f576f0 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 5 May 2025 16:21:36 +0200 Subject: [PATCH 38/58] move functions into class in BaseEncoderDecoder --- src/easyscience/io/template.py | 54 ++++++++++++++--------------- src/easyscience/utils/Exceptions.py | 11 ------ 2 files changed, 27 insertions(+), 38 deletions(-) delete mode 100644 src/easyscience/utils/Exceptions.py diff --git a/src/easyscience/io/template.py b/src/easyscience/io/template.py index b20246d9..16afcb06 100644 --- a/src/easyscience/io/template.py +++ b/src/easyscience/io/template.py @@ -128,10 +128,10 @@ def _convert_to_dict( if new_obj is not obj: return new_obj - d = {'@module': get_class_module(obj), '@class': obj.__class__.__name__} + d = {'@module': self._get_class_module(obj), '@class': obj.__class__.__name__} try: - parent_module = get_class_module(obj).split('.')[0] + parent_module = self._get_class_module(obj).split('.')[0] module_version = import_module(parent_module).__version__ # type: ignore d['@version'] = '{}'.format(module_version) except (AttributeError, ImportError): @@ -189,7 +189,7 @@ def runner(o): 'determine the dict format. Alternatively, ' 'you can implement both as_dict and from_dict.' ) - d[c] = recursive_encoder(a, skip=skip, encoder=self, full_encode=full_encode, **kwargs) + d[c] = self._recursive_encoder(a, skip=skip, encoder=self, full_encode=full_encode, **kwargs) if spec.varargs is not None and getattr(obj, spec.varargs, None) is not None: d.update({spec.varargs: getattr(obj, spec.varargs)}) if hasattr(obj, '_kwargs'): @@ -206,7 +206,7 @@ def runner(o): continue vv = redirect[k](obj) v_ = runner(vv) - d[k] = recursive_encoder( + d[k] = self._recursive_encoder( v_, skip=skip, encoder=self, @@ -263,29 +263,29 @@ def _convert_from_dict(d): return [BaseEncoderDecoder._convert_from_dict(x) for x in d] return d -def recursive_encoder(obj, skip: List[str] = [], encoder=None, full_encode=False, **kwargs): - """ - Walk through an object encoding it - """ - if encoder is None: - encoder = BaseEncoderDecoder() - T_ = type(obj) - if issubclass(T_, (list, tuple, MutableSequence)): - # Is it a core MutableSequence? + def _get_class_module(self, obj): + """ + Returns the REAL module of the class of the object. + """ + c = getattr(obj, '__old_class__', obj.__class__) + return c.__module__ + + def _recursive_encoder(self, obj, skip: List[str] = [], encoder=None, full_encode=False, **kwargs): + """ + Walk through an object encoding it + """ + if encoder is None: + encoder = BaseEncoderDecoder() + T_ = type(obj) + if issubclass(T_, (list, tuple, MutableSequence)): + # Is it a core MutableSequence? + if hasattr(obj, 'encode') and obj.__class__.__module__ != 'builtins': # strings have encode + return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) + else: + return [self._recursive_encoder(it, skip, encoder, full_encode, **kwargs) for it in obj] + if isinstance(obj, dict): + return {kk: self._recursive_encoder(vv, skip, encoder, full_encode, **kwargs) for kk, vv in obj.items()} if hasattr(obj, 'encode') and obj.__class__.__module__ != 'builtins': # strings have encode return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) - else: - return [recursive_encoder(it, skip, encoder, full_encode, **kwargs) for it in obj] - if isinstance(obj, dict): - return {kk: recursive_encoder(vv, skip, encoder, full_encode, **kwargs) for kk, vv in obj.items()} - if hasattr(obj, 'encode') and obj.__class__.__module__ != 'builtins': # strings have encode - return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) - return obj - + return obj -def get_class_module(obj): - """ - Returns the REAL module of the class of the object. - """ - c = getattr(obj, '__old_class__', obj.__class__) - return c.__module__ diff --git a/src/easyscience/utils/Exceptions.py b/src/easyscience/utils/Exceptions.py deleted file mode 100644 index 8fd54ac0..00000000 --- a/src/easyscience/utils/Exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Date: Tue, 6 May 2025 10:20:07 +0200 Subject: [PATCH 39/58] Deprecate the XMLSerializer --- src/easyscience/{io => legacy}/xml.py | 8 +- tests/unit_tests/io/test_xml.py | 111 -------------------------- tests/unit_tests/legacy/test_xml.py | 111 ++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 115 deletions(-) rename src/easyscience/{io => legacy}/xml.py (96%) delete mode 100644 tests/unit_tests/io/test_xml.py create mode 100644 tests/unit_tests/legacy/test_xml.py diff --git a/src/easyscience/io/xml.py b/src/easyscience/legacy/xml.py similarity index 96% rename from src/easyscience/io/xml.py rename to src/easyscience/legacy/xml.py index 5d4d8744..46067cd5 100644 --- a/src/easyscience/io/xml.py +++ b/src/easyscience/legacy/xml.py @@ -14,12 +14,12 @@ import numpy as np -from .dict import DataDictSerializer -from .dict import DictSerializer -from .template import BaseEncoderDecoder +from ..io.dict import DataDictSerializer +from ..io.dict import DictSerializer +from ..io.template import BaseEncoderDecoder if TYPE_CHECKING: - from .component_serializer import ComponentSerializer + from ..io.component_serializer import ComponentSerializer can_intent = (sys.version_info.major > 2) & (sys.version_info.minor > 8) diff --git a/tests/unit_tests/io/test_xml.py b/tests/unit_tests/io/test_xml.py deleted file mode 100644 index 8afc41df..00000000 --- a/tests/unit_tests/io/test_xml.py +++ /dev/null @@ -1,111 +0,0 @@ - -import sys -import xml.etree.ElementTree as ET -from copy import deepcopy -from typing import Type - -import pytest - -from easyscience.io.xml import XMLSerializer -from easyscience.variable import DescriptorNumber - -from .test_core import dp_param_dict -from .test_core import skip_dict -from easyscience import global_object - -def recursive_remove(d, remove_keys: list) -> dict: - """ - Remove keys from a dictionary. - """ - if not isinstance(remove_keys, list): - remove_keys = [remove_keys] - if isinstance(d, dict): - dd = {} - for k in d.keys(): - if k not in remove_keys: - dd[k] = recursive_remove(d[k], remove_keys) - return dd - else: - return d - - -def recursive_test(testing_obj, reference_obj): - for i, (k, v) in enumerate(testing_obj.items()): - if isinstance(v, dict): - recursive_test(v, reference_obj[i]) - else: - assert v == XMLSerializer.string_to_variable(reference_obj[i].text) - - -######################################################################################################################## -# TESTING ENCODING -######################################################################################################################## -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_XMLDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - dp_kwargs = deepcopy(dp_kwargs) - - if isinstance(skip, str): - del dp_kwargs[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc = obj.encode(skip=skip, encoder=XMLSerializer) - ref_encode = obj.encode(skip=skip) - assert isinstance(enc, str) - data_xml = ET.XML(enc) - assert data_xml.tag == "data" - recursive_test(data_xml, ref_encode) - -# ######################################################################################################################## -# # TESTING DECODING -# ######################################################################################################################## -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_XMLDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=XMLSerializer) - assert isinstance(enc, str) - data_xml = ET.XML(enc) - assert data_xml.tag == "data" - global_object.map._clear() - dec = dp_cls.decode(enc, decoder=XMLSerializer) - - for k in data_dict.keys(): - if hasattr(obj, k) and hasattr(dec, k): - assert getattr(obj, k) == getattr(dec, k) - else: - raise AttributeError(f"{k} not found in decoded object") - - -def test_slow_encode(): - - if sys.version_info < (3, 9): - pytest.skip("This test is only for python 3.9+") - - a = {"a": [1, 2, 3]} - slow_xml = XMLSerializer().encode(a, fast=False) - reference = """ - 1 - 2 - 3 -""" - assert slow_xml == reference - - -def test_include_header(): - - if sys.version_info < (3, 9): - pytest.skip("This test is only for python 3.9+") - - a = {"a": [1, 2, 3]} - header_xml = XMLSerializer().encode(a, use_header=True) - reference = '?xml version="1.0" encoding="UTF-8"?\n\n 1\n 2\n 3\n' - assert header_xml == reference diff --git a/tests/unit_tests/legacy/test_xml.py b/tests/unit_tests/legacy/test_xml.py new file mode 100644 index 00000000..7094de85 --- /dev/null +++ b/tests/unit_tests/legacy/test_xml.py @@ -0,0 +1,111 @@ + +# import sys +# import xml.etree.ElementTree as ET +# from copy import deepcopy +# from typing import Type + +# import pytest + +# from easyscience.legacy.xml import XMLSerializer +# from easyscience.variable import DescriptorNumber + +# from ..io.test_core import dp_param_dict +# from ..io.test_core import skip_dict +# from easyscience import global_object + +# def recursive_remove(d, remove_keys: list) -> dict: +# """ +# Remove keys from a dictionary. +# """ +# if not isinstance(remove_keys, list): +# remove_keys = [remove_keys] +# if isinstance(d, dict): +# dd = {} +# for k in d.keys(): +# if k not in remove_keys: +# dd[k] = recursive_remove(d[k], remove_keys) +# return dd +# else: +# return d + + +# def recursive_test(testing_obj, reference_obj): +# for i, (k, v) in enumerate(testing_obj.items()): +# if isinstance(v, dict): +# recursive_test(v, reference_obj[i]) +# else: +# assert v == XMLSerializer.string_to_variable(reference_obj[i].text) + + +# ######################################################################################################################## +# # TESTING ENCODING +# ######################################################################################################################## +# @pytest.mark.parametrize(**skip_dict) +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_XMLDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# dp_kwargs = deepcopy(dp_kwargs) + +# if isinstance(skip, str): +# del dp_kwargs[skip] + +# if not isinstance(skip, list): +# skip = [skip] + +# enc = obj.encode(skip=skip, encoder=XMLSerializer) +# ref_encode = obj.encode(skip=skip) +# assert isinstance(enc, str) +# data_xml = ET.XML(enc) +# assert data_xml.tag == "data" +# recursive_test(data_xml, ref_encode) + +# # ######################################################################################################################## +# # # TESTING DECODING +# # ######################################################################################################################## +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_XMLDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=XMLSerializer) +# assert isinstance(enc, str) +# data_xml = ET.XML(enc) +# assert data_xml.tag == "data" +# global_object.map._clear() +# dec = dp_cls.decode(enc, decoder=XMLSerializer) + +# for k in data_dict.keys(): +# if hasattr(obj, k) and hasattr(dec, k): +# assert getattr(obj, k) == getattr(dec, k) +# else: +# raise AttributeError(f"{k} not found in decoded object") + + +# def test_slow_encode(): + +# if sys.version_info < (3, 9): +# pytest.skip("This test is only for python 3.9+") + +# a = {"a": [1, 2, 3]} +# slow_xml = XMLSerializer().encode(a, fast=False) +# reference = """ +# 1 +# 2 +# 3 +# """ +# assert slow_xml == reference + + +# def test_include_header(): + +# if sys.version_info < (3, 9): +# pytest.skip("This test is only for python 3.9+") + +# a = {"a": [1, 2, 3]} +# header_xml = XMLSerializer().encode(a, use_header=True) +# reference = '?xml version="1.0" encoding="UTF-8"?\n\n 1\n 2\n 3\n' +# assert header_xml == reference From 48e3fee3184398fb6e445343a69dbf7be5e889ba Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 10:37:07 +0200 Subject: [PATCH 40/58] deprecate JsonDataSerializer --- src/easyscience/io/json.py | 27 ------ src/easyscience/legacy/json.py | 119 ++++++++++++++++++++++++++ tests/unit_tests/io/test_json.py | 39 --------- tests/unit_tests/legacy/test_json.py | 123 +++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 66 deletions(-) create mode 100644 src/easyscience/legacy/json.py create mode 100644 tests/unit_tests/legacy/test_json.py diff --git a/src/easyscience/io/json.py b/src/easyscience/io/json.py index 074c524d..1fb412e4 100644 --- a/src/easyscience/io/json.py +++ b/src/easyscience/io/json.py @@ -34,32 +34,6 @@ def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: def decode(cls, data: str) -> ComponentSerializer: return json.loads(data, cls=JsonDecoderTemplate) - -class JsonDataSerializer(BaseEncoderDecoder): - def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: - """ - Returns a json string representation of the ComponentSerializer object. - """ - from .dict import DataDictSerializer - - ENCODER = type( - JsonEncoderTemplate.__name__, - (JsonEncoderTemplate, BaseEncoderDecoder), - { - 'skip': skip, - '_converter': lambda *args, **kwargs: DataDictSerializer._parse_dict( - DataDictSerializer._convert_to_dict(*args, **kwargs) - ), - }, - ) - - return json.dumps(obj, cls=ENCODER) - - @classmethod - def decode(cls, data: str) -> ComponentSerializer: - raise NotImplementedError('It is not possible to reconstitute objects from data only objects.') - - class JsonEncoderTemplate(json.JSONEncoder): """ A Json Encoder which supports the ComponentSerializer API, plus adds support for @@ -90,7 +64,6 @@ def default(self, o) -> dict: # pylint: disable=E0202 """ return self._converter(o, self.skip, full_encode=True) - class JsonDecoderTemplate(json.JSONDecoder): """ A Json Decoder which supports the ComponentSerializer API. By default, the diff --git a/src/easyscience/legacy/json.py b/src/easyscience/legacy/json.py new file mode 100644 index 00000000..074c524d --- /dev/null +++ b/src/easyscience/legacy/json.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +__author__ = 'https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py' +__version__ = '3.0.0' + +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project str: + """ + Returns a json string representation of the ComponentSerializer object. + """ + ENCODER = type( + JsonEncoderTemplate.__name__, + (JsonEncoderTemplate, BaseEncoderDecoder), + {'skip': skip}, + ) + return json.dumps(obj, cls=ENCODER) + + @classmethod + def decode(cls, data: str) -> ComponentSerializer: + return json.loads(data, cls=JsonDecoderTemplate) + + +class JsonDataSerializer(BaseEncoderDecoder): + def encode(self, obj: ComponentSerializer, skip: List[str] = []) -> str: + """ + Returns a json string representation of the ComponentSerializer object. + """ + from .dict import DataDictSerializer + + ENCODER = type( + JsonEncoderTemplate.__name__, + (JsonEncoderTemplate, BaseEncoderDecoder), + { + 'skip': skip, + '_converter': lambda *args, **kwargs: DataDictSerializer._parse_dict( + DataDictSerializer._convert_to_dict(*args, **kwargs) + ), + }, + ) + + return json.dumps(obj, cls=ENCODER) + + @classmethod + def decode(cls, data: str) -> ComponentSerializer: + raise NotImplementedError('It is not possible to reconstitute objects from data only objects.') + + +class JsonEncoderTemplate(json.JSONEncoder): + """ + A Json Encoder which supports the ComponentSerializer API, plus adds support for + numpy arrays, datetime objects, bson ObjectIds (requires bson). + + Usage:: + + # Add it as a *cls* keyword when using json.dump + json.dumps(object, cls=MontyEncoder) + """ + + skip = [] + _converter = BaseEncoderDecoder._convert_to_dict + + def default(self, o) -> dict: # pylint: disable=E0202 + """ + Overriding default method for JSON encoding. This method does two + things: (a) If an object has a to_dict property, return the to_dict + output. (b) If the @module and @class keys are not in the to_dict, + add them to the output automatically. If the object has no to_dict + property, the default Python json encoder default method is called. + + Args: + o: Python object. + + Return: + Python dict representation. + """ + return self._converter(o, self.skip, full_encode=True) + + +class JsonDecoderTemplate(json.JSONDecoder): + """ + A Json Decoder which supports the ComponentSerializer API. By default, the + decoder attempts to find a module and name associated with a dict. If + found, the decoder will generate a Pymatgen as a priority. If that fails, + the original decoded dictionary from the string is returned. Note that + nested lists and dicts containing pymatgen object will be decoded correctly + as well. + + Usage: + + # Add it as a *cls* keyword when using json.load + json.loads(json_string, cls=MontyDecoder) + """ + + _converter = BaseEncoderDecoder._convert_from_dict + + def decode(self, s): + """ + Overrides decode from JSONDecoder. + + :param s: string + :return: Object. + """ + d = json.JSONDecoder.decode(self, s) + return self.__class__._converter(d) diff --git a/tests/unit_tests/io/test_json.py b/tests/unit_tests/io/test_json.py index 238f33d4..036180fd 100644 --- a/tests/unit_tests/io/test_json.py +++ b/tests/unit_tests/io/test_json.py @@ -5,7 +5,6 @@ import pytest -from easyscience.io.json import JsonDataSerializer from easyscience.io.json import JsonSerializer from easyscience.variable import DescriptorNumber @@ -63,33 +62,6 @@ def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber] check_dict(dp_kwargs, dec) - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - if isinstance(skip, str): - del data_dict[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc = obj.encode(skip=skip, encoder=JsonDataSerializer) - assert isinstance(enc, str) - enc_d = json.loads(enc) - - expected_keys = set(data_dict.keys()) - obtained_keys = set(enc_d.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(data_dict, enc_d) - # ######################################################################################################################## # # TESTING DECODING # ######################################################################################################################## @@ -110,14 +82,3 @@ def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[Descriptor else: raise AttributeError(f"{k} not found in decoded object") - -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DataDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=JsonDataSerializer) - global_object.map._clear() - with pytest.raises(NotImplementedError): - dec = obj.decode(enc, decoder=JsonDataSerializer) diff --git a/tests/unit_tests/legacy/test_json.py b/tests/unit_tests/legacy/test_json.py new file mode 100644 index 00000000..651bd950 --- /dev/null +++ b/tests/unit_tests/legacy/test_json.py @@ -0,0 +1,123 @@ + +# import json +# from copy import deepcopy +# from typing import Type + +# import pytest + +# from easyscience.io.json import JsonDataSerializer +# from easyscience.io.json import JsonSerializer +# from easyscience.variable import DescriptorNumber + +# from .test_core import check_dict +# from .test_core import dp_param_dict +# from .test_core import skip_dict +# from easyscience import global_object + + +# def recursive_remove(d, remove_keys: list) -> dict: +# """ +# Remove keys from a dictionary. +# """ +# if not isinstance(remove_keys, list): +# remove_keys = [remove_keys] +# if isinstance(d, dict): +# dd = {} +# for k in d.keys(): +# if k not in remove_keys: +# dd[k] = recursive_remove(d[k], remove_keys) +# return dd +# else: +# return d + + +# ######################################################################################################################## +# # TESTING ENCODING +# ######################################################################################################################## +# @pytest.mark.parametrize(**skip_dict) +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# dp_kwargs = deepcopy(dp_kwargs) + +# if isinstance(skip, str): +# del dp_kwargs[skip] + +# if not isinstance(skip, list): +# skip = [skip] + +# enc = obj.encode(skip=skip, encoder=JsonSerializer) +# assert isinstance(enc, str) + +# # We can test like this as we don't have "complex" objects yet +# dec = json.loads(enc) +# expected_keys = set(dp_kwargs.keys()) +# obtained_keys = set(dec.keys()) + +# dif = expected_keys.difference(obtained_keys) + +# assert len(dif) == 0 + +# check_dict(dp_kwargs, dec) + + +# @pytest.mark.parametrize(**skip_dict) +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# if isinstance(skip, str): +# del data_dict[skip] + +# if not isinstance(skip, list): +# skip = [skip] + +# enc = obj.encode(skip=skip, encoder=JsonDataSerializer) +# assert isinstance(enc, str) +# enc_d = json.loads(enc) + +# expected_keys = set(data_dict.keys()) +# obtained_keys = set(enc_d.keys()) + +# dif = expected_keys.difference(obtained_keys) + +# assert len(dif) == 0 + +# check_dict(data_dict, enc_d) + +# # ######################################################################################################################## +# # # TESTING DECODING +# # ######################################################################################################################## +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=JsonSerializer) +# global_object.map._clear() +# assert isinstance(enc, str) +# dec = obj.decode(enc, decoder=JsonSerializer) + +# for k in data_dict.keys(): +# if hasattr(obj, k) and hasattr(dec, k): +# assert getattr(obj, k) == getattr(dec, k) +# else: +# raise AttributeError(f"{k} not found in decoded object") + + +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DataDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=JsonDataSerializer) +# global_object.map._clear() +# with pytest.raises(NotImplementedError): +# dec = obj.decode(enc, decoder=JsonDataSerializer) From c7e523fd0ee80619b02cf5889fbfd3867858d65b Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 10:48:35 +0200 Subject: [PATCH 41/58] Deprecate DataDictSerializer and exclude legacy folder from wheel --- pyproject.toml | 1 + src/easyscience/io/dict.py | 69 +----------- src/easyscience/legacy/dict.py | 128 ++++++++++++++++++++++ tests/unit_tests/io/test_dict.py | 39 ------- tests/unit_tests/legacy/test_dict.py | 156 +++++++++++++++++++++++++++ 5 files changed, 286 insertions(+), 107 deletions(-) create mode 100644 src/easyscience/legacy/dict.py create mode 100644 tests/unit_tests/legacy/test_dict.py diff --git a/pyproject.toml b/pyproject.toml index bb9103e8..cfa5f735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ packages = ["src"] [tool.hatch.build.targets.wheel] packages = ["src/easyscience"] +exclude = ["src/easyscience/legacy"] [tool.coverage.run] source = ["src/easyscience"] diff --git a/src/easyscience/io/dict.py b/src/easyscience/io/dict.py index 2907f04d..ab2b0b90 100644 --- a/src/easyscience/io/dict.py +++ b/src/easyscience/io/dict.py @@ -18,8 +18,6 @@ if TYPE_CHECKING: from .component_serializer import ComponentSerializer -_KNOWN_CORE_TYPES = ("Descriptor", "Parameter") - class DictSerializer(BaseEncoderDecoder): """ @@ -60,69 +58,4 @@ def from_dict(cls, d: Dict[str, Any]) -> ComponentSerializer: :param d: Dict representation. :return: ComponentSerializer class. """ - return BaseEncoderDecoder._convert_from_dict(d) - - -class DataDictSerializer(DictSerializer): - """ - This is a serializer that can encode the data in an EasyScience object to a JSON encoded dictionary. - """ - - def encode( - self, - obj: ComponentSerializer, - skip: Optional[List[str]] = None, - full_encode: bool = False, - **kwargs, - ) -> Dict[str, Any]: - """ - Convert an EasyScience object to a JSON encoded data dictionary - - :param obj: Object to be encoded. - :param skip: List of field names as strings to skip when forming the encoded object - :param full_encode: Should the data also be JSON encoded (default False) - :param kwargs: Any additional key word arguments to be passed to the encoder - :return: object encoded to data dictionary. - """ - - if skip is None: - skip = [] - elif isinstance(skip, str): - skip = [skip] - if not isinstance(skip, list): - raise ValueError("Skip must be a list of strings.") - encoded = super().encode(obj, skip=skip, full_encode=full_encode, **kwargs) - return self._parse_dict(encoded) - - @classmethod - def decode(cls, d: Dict[str, Any]) -> ComponentSerializer: - """ - This function is not implemented as a data dictionary does not contain the necessary information to re-form an - EasyScience object. - """ - - raise NotImplementedError( - "It is not possible to reconstitute objects from data only dictionary." - ) - - @staticmethod - def _parse_dict(in_dict: Dict[str, Any]) -> Dict[str, Any]: - """ - Strip out any non-data from a dictionary - """ - - out_dict = dict() - for key in in_dict.keys(): - if key[0] == "@": - if key == "@class" and in_dict[key] not in _KNOWN_CORE_TYPES: - out_dict["name"] = in_dict[key] - continue - out_dict[key] = in_dict[key] - if isinstance(in_dict[key], dict): - out_dict[key] = DataDictSerializer._parse_dict(in_dict[key]) - elif isinstance(in_dict[key], list): - out_dict[key] = [ - DataDictSerializer._parse_dict(x) if isinstance(x, dict) else x - for x in in_dict[key] - ] - return out_dict + return BaseEncoderDecoder._convert_from_dict(d) \ No newline at end of file diff --git a/src/easyscience/legacy/dict.py b/src/easyscience/legacy/dict.py new file mode 100644 index 00000000..2907f04d --- /dev/null +++ b/src/easyscience/legacy/dict.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +__author__ = "https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py" +__version__ = "3.0.0" +# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +# © 2021-2023 Contributors to the EasyScience project ComponentSerializer: + """ + :param d: Dict representation. + :return: ComponentSerializer class. + """ + + return BaseEncoderDecoder._convert_from_dict(d) + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> ComponentSerializer: + """ + :param d: Dict representation. + :return: ComponentSerializer class. + """ + return BaseEncoderDecoder._convert_from_dict(d) + + +class DataDictSerializer(DictSerializer): + """ + This is a serializer that can encode the data in an EasyScience object to a JSON encoded dictionary. + """ + + def encode( + self, + obj: ComponentSerializer, + skip: Optional[List[str]] = None, + full_encode: bool = False, + **kwargs, + ) -> Dict[str, Any]: + """ + Convert an EasyScience object to a JSON encoded data dictionary + + :param obj: Object to be encoded. + :param skip: List of field names as strings to skip when forming the encoded object + :param full_encode: Should the data also be JSON encoded (default False) + :param kwargs: Any additional key word arguments to be passed to the encoder + :return: object encoded to data dictionary. + """ + + if skip is None: + skip = [] + elif isinstance(skip, str): + skip = [skip] + if not isinstance(skip, list): + raise ValueError("Skip must be a list of strings.") + encoded = super().encode(obj, skip=skip, full_encode=full_encode, **kwargs) + return self._parse_dict(encoded) + + @classmethod + def decode(cls, d: Dict[str, Any]) -> ComponentSerializer: + """ + This function is not implemented as a data dictionary does not contain the necessary information to re-form an + EasyScience object. + """ + + raise NotImplementedError( + "It is not possible to reconstitute objects from data only dictionary." + ) + + @staticmethod + def _parse_dict(in_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Strip out any non-data from a dictionary + """ + + out_dict = dict() + for key in in_dict.keys(): + if key[0] == "@": + if key == "@class" and in_dict[key] not in _KNOWN_CORE_TYPES: + out_dict["name"] = in_dict[key] + continue + out_dict[key] = in_dict[key] + if isinstance(in_dict[key], dict): + out_dict[key] = DataDictSerializer._parse_dict(in_dict[key]) + elif isinstance(in_dict[key], list): + out_dict[key] = [ + DataDictSerializer._parse_dict(x) if isinstance(x, dict) else x + for x in in_dict[key] + ] + return out_dict diff --git a/tests/unit_tests/io/test_dict.py b/tests/unit_tests/io/test_dict.py index 6a9925f6..9c420c87 100644 --- a/tests/unit_tests/io/test_dict.py +++ b/tests/unit_tests/io/test_dict.py @@ -4,7 +4,6 @@ import pytest -from easyscience.io.dict import DataDictSerializer from easyscience.io.dict import DictSerializer from easyscience.variable import DescriptorNumber from easyscience.base_classes import BaseObj @@ -60,32 +59,6 @@ def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber] check_dict(dp_kwargs, enc) - -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - if isinstance(skip, str): - del data_dict[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc_d = obj.encode(skip=skip, encoder=DataDictSerializer) - - expected_keys = set(data_dict.keys()) - obtained_keys = set(enc_d.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(data_dict, enc_d) - - ######################################################################################################################## # TESTING DECODING ######################################################################################################################## @@ -122,18 +95,6 @@ def test_variable_DictSerializer_from_dict(dp_kwargs: dict, dp_cls: Type[Descrip else: raise AttributeError(f"{k} not found in decoded object") - -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DataDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=DataDictSerializer) - with pytest.raises(NotImplementedError): - dec = obj.decode(enc, decoder=DataDictSerializer) - - def test_group_encode(): d0 = DescriptorNumber("a", 0) d1 = DescriptorNumber("b", 1) diff --git a/tests/unit_tests/legacy/test_dict.py b/tests/unit_tests/legacy/test_dict.py new file mode 100644 index 00000000..70af9d78 --- /dev/null +++ b/tests/unit_tests/legacy/test_dict.py @@ -0,0 +1,156 @@ + +# from copy import deepcopy +# from typing import Type + +# import pytest + +# from easyscience.io.dict import DataDictSerializer +# from easyscience.io.dict import DictSerializer +# from easyscience.variable import DescriptorNumber +# from easyscience.base_classes import BaseObj + +# from .test_core import check_dict +# from .test_core import dp_param_dict +# from .test_core import skip_dict +# from easyscience import global_object + + +# def recursive_remove(d, remove_keys: list) -> dict: +# """ +# Remove keys from a dictionary. +# """ +# if not isinstance(remove_keys, list): +# remove_keys = [remove_keys] +# if isinstance(d, dict): +# dd = {} +# for k in d.keys(): +# if k not in remove_keys: +# dd[k] = recursive_remove(d[k], remove_keys) +# return dd +# else: +# return d + + +# ######################################################################################################################## +# # TESTING ENCODING +# ######################################################################################################################## +# @pytest.mark.parametrize(**skip_dict) +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# dp_kwargs = deepcopy(dp_kwargs) + +# if isinstance(skip, str): +# del dp_kwargs[skip] + +# if not isinstance(skip, list): +# skip = [skip] + +# enc = obj.encode(skip=skip, encoder=DictSerializer) + +# expected_keys = set(dp_kwargs.keys()) +# obtained_keys = set(enc.keys()) + +# dif = expected_keys.difference(obtained_keys) + +# assert len(dif) == 0 + +# check_dict(dp_kwargs, enc) + + +# @pytest.mark.parametrize(**skip_dict) +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DataDictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# if isinstance(skip, str): +# del data_dict[skip] + +# if not isinstance(skip, list): +# skip = [skip] + +# enc_d = obj.encode(skip=skip, encoder=DataDictSerializer) + +# expected_keys = set(data_dict.keys()) +# obtained_keys = set(enc_d.keys()) + +# dif = expected_keys.difference(obtained_keys) + +# assert len(dif) == 0 + +# check_dict(data_dict, enc_d) + + +# ######################################################################################################################## +# # TESTING DECODING +# ######################################################################################################################## +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=DictSerializer) +# global_object.map._clear() +# dec = dp_cls.decode(enc, decoder=DictSerializer) + +# for k in data_dict.keys(): +# if hasattr(obj, k) and hasattr(dec, k): +# assert getattr(obj, k) == getattr(dec, k) +# else: +# raise AttributeError(f"{k} not found in decoded object") + + +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DictSerializer_from_dict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=DictSerializer) +# global_object.map._clear() +# dec = dp_cls.from_dict(enc) + +# for k in data_dict.keys(): +# if hasattr(obj, k) and hasattr(dec, k): +# assert getattr(obj, k) == getattr(dec, k) +# else: +# raise AttributeError(f"{k} not found in decoded object") + + +# @pytest.mark.parametrize(**dp_param_dict) +# def test_variable_DataDictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +# data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} + +# obj = dp_cls(**data_dict) + +# enc = obj.encode(encoder=DataDictSerializer) +# with pytest.raises(NotImplementedError): +# dec = obj.decode(enc, decoder=DataDictSerializer) + + +# def test_group_encode(): +# d0 = DescriptorNumber("a", 0) +# d1 = DescriptorNumber("b", 1) + +# from easyscience.base_classes import BaseCollection + +# b = BaseCollection("test", d0, d1) +# d = b.as_dict() +# assert isinstance(d["data"], list) + + +# def test_group_encode2(): +# d0 = DescriptorNumber("a", 0) +# d1 = DescriptorNumber("b", 1) + +# from easyscience.base_classes import BaseCollection + +# b = BaseObj("outer", b=BaseCollection("test", d0, d1)) +# d = b.as_dict() +# assert isinstance(d["b"], dict) \ No newline at end of file From 4ce9ffef8f4f7a17a0feaf0dc35436ca9570c162 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 13:28:44 +0200 Subject: [PATCH 42/58] Depreacte JsonSerializer --- src/easyscience/io/__init__.py | 7 ++ src/easyscience/io/component_serializer.py | 7 +- src/easyscience/io/dict.py | 21 ++--- src/easyscience/io/json.py | 92 ---------------------- tests/unit_tests/io/test_json.py | 84 -------------------- 5 files changed, 19 insertions(+), 192 deletions(-) delete mode 100644 src/easyscience/io/json.py delete mode 100644 tests/unit_tests/io/test_json.py diff --git a/src/easyscience/io/__init__.py b/src/easyscience/io/__init__.py index a4ab5234..0f05734f 100644 --- a/src/easyscience/io/__init__.py +++ b/src/easyscience/io/__init__.py @@ -1,3 +1,10 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project ComponentSerializer: """ - :param d: Dict representation. - :return: ComponentSerializer class. - """ - - return BaseEncoderDecoder._convert_from_dict(d) + Re-create an EasyScience object from the dictionary representation. - @classmethod - def from_dict(cls, d: Dict[str, Any]) -> ComponentSerializer: - """ - :param d: Dict representation. - :return: ComponentSerializer class. + :param d: Dict representation of an EasyScience object. + :return: EasyScience object. """ + return BaseEncoderDecoder._convert_from_dict(d) \ No newline at end of file diff --git a/src/easyscience/io/json.py b/src/easyscience/io/json.py deleted file mode 100644 index 1fb412e4..00000000 --- a/src/easyscience/io/json.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -__author__ = 'https://github.com/materialsvirtuallab/monty/blob/master/monty/json.py' -__version__ = '3.0.0' - -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project str: - """ - Returns a json string representation of the ComponentSerializer object. - """ - ENCODER = type( - JsonEncoderTemplate.__name__, - (JsonEncoderTemplate, BaseEncoderDecoder), - {'skip': skip}, - ) - return json.dumps(obj, cls=ENCODER) - - @classmethod - def decode(cls, data: str) -> ComponentSerializer: - return json.loads(data, cls=JsonDecoderTemplate) - -class JsonEncoderTemplate(json.JSONEncoder): - """ - A Json Encoder which supports the ComponentSerializer API, plus adds support for - numpy arrays, datetime objects, bson ObjectIds (requires bson). - - Usage:: - - # Add it as a *cls* keyword when using json.dump - json.dumps(object, cls=MontyEncoder) - """ - - skip = [] - _converter = BaseEncoderDecoder._convert_to_dict - - def default(self, o) -> dict: # pylint: disable=E0202 - """ - Overriding default method for JSON encoding. This method does two - things: (a) If an object has a to_dict property, return the to_dict - output. (b) If the @module and @class keys are not in the to_dict, - add them to the output automatically. If the object has no to_dict - property, the default Python json encoder default method is called. - - Args: - o: Python object. - - Return: - Python dict representation. - """ - return self._converter(o, self.skip, full_encode=True) - -class JsonDecoderTemplate(json.JSONDecoder): - """ - A Json Decoder which supports the ComponentSerializer API. By default, the - decoder attempts to find a module and name associated with a dict. If - found, the decoder will generate a Pymatgen as a priority. If that fails, - the original decoded dictionary from the string is returned. Note that - nested lists and dicts containing pymatgen object will be decoded correctly - as well. - - Usage: - - # Add it as a *cls* keyword when using json.load - json.loads(json_string, cls=MontyDecoder) - """ - - _converter = BaseEncoderDecoder._convert_from_dict - - def decode(self, s): - """ - Overrides decode from JSONDecoder. - - :param s: string - :return: Object. - """ - d = json.JSONDecoder.decode(self, s) - return self.__class__._converter(d) diff --git a/tests/unit_tests/io/test_json.py b/tests/unit_tests/io/test_json.py deleted file mode 100644 index 036180fd..00000000 --- a/tests/unit_tests/io/test_json.py +++ /dev/null @@ -1,84 +0,0 @@ - -import json -from copy import deepcopy -from typing import Type - -import pytest - -from easyscience.io.json import JsonSerializer -from easyscience.variable import DescriptorNumber - -from .test_core import check_dict -from .test_core import dp_param_dict -from .test_core import skip_dict -from easyscience import global_object - - -def recursive_remove(d, remove_keys: list) -> dict: - """ - Remove keys from a dictionary. - """ - if not isinstance(remove_keys, list): - remove_keys = [remove_keys] - if isinstance(d, dict): - dd = {} - for k in d.keys(): - if k not in remove_keys: - dd[k] = recursive_remove(d[k], remove_keys) - return dd - else: - return d - - -######################################################################################################################## -# TESTING ENCODING -######################################################################################################################## -@pytest.mark.parametrize(**skip_dict) -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - dp_kwargs = deepcopy(dp_kwargs) - - if isinstance(skip, str): - del dp_kwargs[skip] - - if not isinstance(skip, list): - skip = [skip] - - enc = obj.encode(skip=skip, encoder=JsonSerializer) - assert isinstance(enc, str) - - # We can test like this as we don't have "complex" objects yet - dec = json.loads(enc) - expected_keys = set(dp_kwargs.keys()) - obtained_keys = set(dec.keys()) - - dif = expected_keys.difference(obtained_keys) - - assert len(dif) == 0 - - check_dict(dp_kwargs, dec) - -# ######################################################################################################################## -# # TESTING DECODING -# ######################################################################################################################## -@pytest.mark.parametrize(**dp_param_dict) -def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): - data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} - - obj = dp_cls(**data_dict) - - enc = obj.encode(encoder=JsonSerializer) - global_object.map._clear() - assert isinstance(enc, str) - dec = obj.decode(enc, decoder=JsonSerializer) - - for k in data_dict.keys(): - if hasattr(obj, k) and hasattr(dec, k): - assert getattr(obj, k) == getattr(dec, k) - else: - raise AttributeError(f"{k} not found in decoded object") - From c9aa588c1cc33f0fd63b38b2210d10f024b737d3 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 13:56:57 +0200 Subject: [PATCH 43/58] Re-name BaseEncoderDecover to SerializerBase and re-name files and fix corresponding imports --- src/easyscience/base_classes/base_obj.py | 2 +- src/easyscience/base_classes/based_base.py | 2 +- src/easyscience/io/__init__.py | 4 +++- src/easyscience/io/component_serializer.py | 8 ++++---- .../io/{dict.py => dict_serializer.py} | 6 +++--- .../io/{template.py => serializer_base.py} | 16 ++++++++-------- src/easyscience/utils/classTools.py | 2 +- src/easyscience/variable/descriptor_base.py | 2 +- tests/unit_tests/base_classes/test_base_obj.py | 2 +- tests/unit_tests/io/test_dict.py | 2 +- 10 files changed, 24 insertions(+), 22 deletions(-) rename src/easyscience/io/{dict.py => dict_serializer.py} (92%) rename src/easyscience/io/{template.py => serializer_base.py} (95%) diff --git a/src/easyscience/base_classes/base_obj.py b/src/easyscience/base_classes/base_obj.py index 47b57d18..e047ae77 100644 --- a/src/easyscience/base_classes/base_obj.py +++ b/src/easyscience/base_classes/base_obj.py @@ -12,7 +12,7 @@ from .based_base import BasedBase if TYPE_CHECKING: - from ..io.component_serializer import ComponentSerializer + from ..io import ComponentSerializer diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index faccb686..4c20b7db 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -12,7 +12,7 @@ from easyscience import global_object -from ..io.component_serializer import ComponentSerializer +from ..io import ComponentSerializer from ..variable import Parameter if TYPE_CHECKING: diff --git a/src/easyscience/io/__init__.py b/src/easyscience/io/__init__.py index 0f05734f..8efb44e4 100644 --- a/src/easyscience/io/__init__.py +++ b/src/easyscience/io/__init__.py @@ -2,9 +2,11 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Any: + def encode(self, skip: Optional[List[str]] = None, encoder: Optional[SerializerBase] = None, **kwargs) -> Any: """ Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `DictSerializer`. @@ -45,7 +45,7 @@ def encode(self, skip: Optional[List[str]] = None, encoder: Optional[BaseEncoder return encoder_obj.encode(self, skip=skip, **kwargs) @classmethod - def decode(cls, obj: Any, decoder: Optional[BaseEncoderDecoder] = None) -> Any: + def decode(cls, obj: Any, decoder: Optional[SerializerBase] = None) -> Any: """ Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. diff --git a/src/easyscience/io/dict.py b/src/easyscience/io/dict_serializer.py similarity index 92% rename from src/easyscience/io/dict.py rename to src/easyscience/io/dict_serializer.py index 9d6740a5..092af468 100644 --- a/src/easyscience/io/dict.py +++ b/src/easyscience/io/dict_serializer.py @@ -12,13 +12,13 @@ from typing import List from typing import Optional -from .template import BaseEncoderDecoder +from .serializer_base import SerializerBase if TYPE_CHECKING: from .component_serializer import ComponentSerializer -class DictSerializer(BaseEncoderDecoder): +class DictSerializer(SerializerBase): """ This is a serializer that can encode and decode EasyScience objects to and from a dictionary. """ @@ -51,4 +51,4 @@ def decode(cls, d: Dict) -> ComponentSerializer: :return: EasyScience object. """ - return BaseEncoderDecoder._convert_from_dict(d) \ No newline at end of file + return SerializerBase._convert_from_dict(d) \ No newline at end of file diff --git a/src/easyscience/io/template.py b/src/easyscience/io/serializer_base.py similarity index 95% rename from src/easyscience/io/template.py rename to src/easyscience/io/serializer_base.py index 16afcb06..87fb95fb 100644 --- a/src/easyscience/io/template.py +++ b/src/easyscience/io/serializer_base.py @@ -27,7 +27,7 @@ _e = json.JSONEncoder() -class BaseEncoderDecoder: +class SerializerBase: """ This is the base class for creating an encoder/decoder which can convert EasyScience objects. `encode` and `decode` are abstract methods to be implemented for each serializer. It is expected that the helper function `_convert_to_dict` @@ -78,7 +78,7 @@ def _encode_objs(obj: Any) -> Dict[str, Any]: :param obj: any object to be encoded :param skip: List of field names as strings to skip when forming the encoded object - :param kwargs: Key-words to pass to `BaseEncoderDecoder` + :param kwargs: Key-words to pass to `SerializerBase` :return: JSON encoded dictionary """ @@ -124,7 +124,7 @@ def _convert_to_dict( skip = [] if full_encode: - new_obj = BaseEncoderDecoder._encode_objs(obj) + new_obj = SerializerBase._encode_objs(obj) if new_obj is not obj: return new_obj @@ -137,7 +137,7 @@ def _convert_to_dict( except (AttributeError, ImportError): d['@version'] = None # type: ignore - spec, args = BaseEncoderDecoder.get_arg_spec(obj.__class__.__init__) + spec, args = SerializerBase.get_arg_spec(obj.__class__.__init__) if hasattr(obj, '_arg_spec'): args = obj._arg_spec @@ -145,7 +145,7 @@ def _convert_to_dict( def runner(o): if full_encode: - return BaseEncoderDecoder._encode_objs(o) + return SerializerBase._encode_objs(o) else: return o @@ -252,7 +252,7 @@ def _convert_from_dict(d): mod = __import__(modname, globals(), locals(), [classname], 0) if hasattr(mod, classname): cls_ = getattr(mod, classname) - data = {k: BaseEncoderDecoder._convert_from_dict(v) for k, v in d.items() if not k.startswith('@')} + data = {k: SerializerBase._convert_from_dict(v) for k, v in d.items() if not k.startswith('@')} return cls_(**data) elif np is not None and modname == 'numpy' and classname == 'array': if d['dtype'].startswith('complex'): @@ -260,7 +260,7 @@ def _convert_from_dict(d): return np.array(d['data'], dtype=d['dtype']) if issubclass(T_, (list, MutableSequence)): - return [BaseEncoderDecoder._convert_from_dict(x) for x in d] + return [SerializerBase._convert_from_dict(x) for x in d] return d def _get_class_module(self, obj): @@ -275,7 +275,7 @@ def _recursive_encoder(self, obj, skip: List[str] = [], encoder=None, full_encod Walk through an object encoding it """ if encoder is None: - encoder = BaseEncoderDecoder() + encoder = SerializerBase() T_ = type(obj) if issubclass(T_, (list, tuple, MutableSequence)): # Is it a core MutableSequence? diff --git a/src/easyscience/utils/classTools.py b/src/easyscience/utils/classTools.py index 475d3299..4e65a791 100644 --- a/src/easyscience/utils/classTools.py +++ b/src/easyscience/utils/classTools.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from ..base_classes import BasedBase - from ..io.component_serializer import ComponentSerializer + from ..io import ComponentSerializer def addLoggedProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: diff --git a/src/easyscience/variable/descriptor_base.py b/src/easyscience/variable/descriptor_base.py index 3918d528..9f971de5 100644 --- a/src/easyscience/variable/descriptor_base.py +++ b/src/easyscience/variable/descriptor_base.py @@ -10,7 +10,7 @@ from easyscience import global_object from easyscience.global_object.undo_redo import property_stack -from easyscience.io.component_serializer import ComponentSerializer +from easyscience.io import ComponentSerializer class DescriptorBase(ComponentSerializer, metaclass=abc.ABCMeta): diff --git a/tests/unit_tests/base_classes/test_base_obj.py b/tests/unit_tests/base_classes/test_base_obj.py index 9ba645d9..5b220da8 100644 --- a/tests/unit_tests/base_classes/test_base_obj.py +++ b/tests/unit_tests/base_classes/test_base_obj.py @@ -19,7 +19,7 @@ from easyscience.base_classes import BaseObj from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter -from easyscience.io.dict import DictSerializer +from easyscience.io import DictSerializer from easyscience import global_object @pytest.fixture diff --git a/tests/unit_tests/io/test_dict.py b/tests/unit_tests/io/test_dict.py index 9c420c87..0ed629c8 100644 --- a/tests/unit_tests/io/test_dict.py +++ b/tests/unit_tests/io/test_dict.py @@ -4,7 +4,7 @@ import pytest -from easyscience.io.dict import DictSerializer +from easyscience.io.dict_serializer import DictSerializer from easyscience.variable import DescriptorNumber from easyscience.base_classes import BaseObj From 5467c9fec057e190185671d7ce942571ee0a1631 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 14:12:24 +0200 Subject: [PATCH 44/58] Re-name tests --- .../io/{test_core.py => test_component_serializer.py} | 0 .../unit_tests/io/{test_dict.py => test_dict_serializer.py} | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/unit_tests/io/{test_core.py => test_component_serializer.py} (100%) rename tests/unit_tests/io/{test_dict.py => test_dict_serializer.py} (95%) diff --git a/tests/unit_tests/io/test_core.py b/tests/unit_tests/io/test_component_serializer.py similarity index 100% rename from tests/unit_tests/io/test_core.py rename to tests/unit_tests/io/test_component_serializer.py diff --git a/tests/unit_tests/io/test_dict.py b/tests/unit_tests/io/test_dict_serializer.py similarity index 95% rename from tests/unit_tests/io/test_dict.py rename to tests/unit_tests/io/test_dict_serializer.py index 0ed629c8..42da0a8d 100644 --- a/tests/unit_tests/io/test_dict.py +++ b/tests/unit_tests/io/test_dict_serializer.py @@ -8,9 +8,9 @@ from easyscience.variable import DescriptorNumber from easyscience.base_classes import BaseObj -from .test_core import check_dict -from .test_core import dp_param_dict -from .test_core import skip_dict +from .test_component_serializer import check_dict +from .test_component_serializer import dp_param_dict +from .test_component_serializer import skip_dict from easyscience import global_object From 6b9a7255cfdc9dc952788c12076ec1ce80c9c3b2 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 14:27:17 +0200 Subject: [PATCH 45/58] Remove Line model --- src/easyscience/models/polynomial.py | 33 ---------------------- tests/unit_tests/models/test_polynomial.py | 30 -------------------- 2 files changed, 63 deletions(-) diff --git a/src/easyscience/models/polynomial.py b/src/easyscience/models/polynomial.py index d0b310cc..0955e53d 100644 --- a/src/easyscience/models/polynomial.py +++ b/src/easyscience/models/polynomial.py @@ -3,7 +3,6 @@ # © 2021-2023 Contributors to the EasyScience project np.ndarray: - return self.m.value * x + self.c.value - - def __repr__(self): - return '{}({}, {})'.format(self.__class__.__name__, self.m, self.c) diff --git a/tests/unit_tests/models/test_polynomial.py b/tests/unit_tests/models/test_polynomial.py index ac20db93..e6d0fbd5 100644 --- a/tests/unit_tests/models/test_polynomial.py +++ b/tests/unit_tests/models/test_polynomial.py @@ -6,11 +6,8 @@ import numpy as np import pytest -from easyscience.models.polynomial import Line from easyscience.models.polynomial import Polynomial -from easyscience.variable import Parameter -line_test_cases = ((1, 2), (-1, -2), (0.72, 6.48)) poly_test_cases = ( (1.,), ( @@ -22,33 +19,6 @@ (0.72, 6.48, -0.48), ) - -@pytest.mark.parametrize("m, c", line_test_cases) -def test_Line_pars(m, c): - line = Line(m, c) - - assert line.m.value == m - assert line.c.value == c - - x = np.linspace(0, 10, 100) - y = line.m.value * x + line.c.value - assert np.allclose(line(x), y) - - -@pytest.mark.parametrize("m, c", line_test_cases) -def test_Line_constructor(m, c): - m_ = Parameter("m", m) - c_ = Parameter("c", c) - line = Line(m_, c_) - - assert line.m.value == m - assert line.c.value == c - - x = np.linspace(0, 10, 100) - y = line.m.value * x + line.c.value - assert np.allclose(line(x), y) - - @pytest.mark.parametrize("coo", poly_test_cases) def test_Polynomial_pars(coo): poly = Polynomial(coefficients=coo) From c49bbd3efffe10ed21b9f3bb79e6ed891e74a199 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Tue, 6 May 2025 15:42:38 +0200 Subject: [PATCH 46/58] First changes to Polynomial model --- src/easyscience/models/__init__.py | 8 +- src/easyscience/models/polynomial.py | 85 ++++++++++--------- src/easyscience/variable/descriptor_number.py | 17 ++-- src/easyscience/variable/parameter.py | 1 + 4 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/easyscience/models/__init__.py b/src/easyscience/models/__init__.py index 24aa7884..7c3ac4ee 100644 --- a/src/easyscience/models/__init__.py +++ b/src/easyscience/models/__init__.py @@ -1,3 +1,9 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project np.ndarray: - return np.polyval([c.value for c in self.coefficients], x) + if not isinstance(coefficients, list): + raise TypeError('coefficients must be a list of numbers') + if len(coefficients) == 0: + raise ValueError('list of coefficients cannot be empty') + for coefficient, index in enumerate(coefficients): + if not isinstance(coefficient, numbers.Number): + raise TypeError(f'coefficients must be numbers, found {type(coefficient)} at index {index}') + self._coefficients = [Parameter(name=f'c{i}', value=c) for i, c in enumerate(coefficients)] + + super().__init__( + name=name, + unique_name=unique_name + ) + + + def __call__(self, x: np.ndarray) -> np.ndarray: + return np.polyval([c.value for c in self._coefficients], x) def __repr__(self): - s = [] - if len(self.coefficients) >= 1: - s += [f'{self.coefficients[0].value}'] - if len(self.coefficients) >= 2: - s += [f'{self.coefficients[1].value}x'] - if len(self.coefficients) >= 3: - s += [f'{c.value}x^{i+2}' for i, c in enumerate(self.coefficients[2:]) if c.value != 0] - s.reverse() - s = ' + '.join(s) - return 'Polynomial({}, {})'.format(self.name, s) + string = [] + for i, c in enumerate(self._coefficients): + if i == 0: + string += [f'{c.value}'] + elif i == 1: + string += [f'{c.value}x'] + else: + string += [f'{c.value}x^{i}'] + string.reverse() + string = ' + '.join(string) + return 'Polynomial "{}" : {}'.format(self.name, string) diff --git a/src/easyscience/variable/descriptor_number.py b/src/easyscience/variable/descriptor_number.py index 98631542..09c952ab 100644 --- a/src/easyscience/variable/descriptor_number.py +++ b/src/easyscience/variable/descriptor_number.py @@ -53,14 +53,15 @@ def __init__( ): """Constructor for the DescriptorNumber class - param name: Name of the descriptor - param value: Value of the descriptor - param unit: Unit of the descriptor - param variance: Variance of the descriptor - param description: Description of the descriptor - param url: URL of the descriptor - param display_name: Display name of the descriptor - param parent: Parent of the descriptor + :param name: Name of the descriptor + :param value: Value of the descriptor + :param unit: Unit of the descriptor + :param variance: Variance of the descriptor + :param unique_name: Unique name of this object. This is used to find the object from anywhere in the program. + :param description: Description of the descriptor + :param url: URL of the descriptor + :param display_name: Display name of the descriptor + :param parent: Parent of the descriptor .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ self._observers: List[DescriptorNumber] = [] diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 63746993..07cf5860 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -65,6 +65,7 @@ def __init__( :param min: The minimum value for fitting :param max: The maximum value for fitting :param fixed: If the parameter is free to vary during fitting + :param unique_name: Unique name of this object. This is used to find the object from anywhere in the program. :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed From 671c86c9da59ec65a8b5567608d2e86530c4751c Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 7 May 2025 10:16:37 +0200 Subject: [PATCH 47/58] Revert first changes to Polynomial model --- src/easyscience/models/__init__.py | 8 +- src/easyscience/models/polynomial.py | 85 +++++++++---------- src/easyscience/variable/descriptor_number.py | 17 ++-- src/easyscience/variable/parameter.py | 1 - 4 files changed, 51 insertions(+), 60 deletions(-) diff --git a/src/easyscience/models/__init__.py b/src/easyscience/models/__init__.py index 7c3ac4ee..24aa7884 100644 --- a/src/easyscience/models/__init__.py +++ b/src/easyscience/models/__init__.py @@ -1,9 +1,3 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project np.ndarray: - return np.polyval([c.value for c in self._coefficients], x) + def __call__(self, x: np.ndarray, *args, **kwargs) -> np.ndarray: + return np.polyval([c.value for c in self.coefficients], x) def __repr__(self): - string = [] - for i, c in enumerate(self._coefficients): - if i == 0: - string += [f'{c.value}'] - elif i == 1: - string += [f'{c.value}x'] - else: - string += [f'{c.value}x^{i}'] - string.reverse() - string = ' + '.join(string) - return 'Polynomial "{}" : {}'.format(self.name, string) + s = [] + if len(self.coefficients) >= 1: + s += [f'{self.coefficients[0].value}'] + if len(self.coefficients) >= 2: + s += [f'{self.coefficients[1].value}x'] + if len(self.coefficients) >= 3: + s += [f'{c.value}x^{i+2}' for i, c in enumerate(self.coefficients[2:]) if c.value != 0] + s.reverse() + s = ' + '.join(s) + return 'Polynomial({}, {})'.format(self.name, s) diff --git a/src/easyscience/variable/descriptor_number.py b/src/easyscience/variable/descriptor_number.py index 09c952ab..98631542 100644 --- a/src/easyscience/variable/descriptor_number.py +++ b/src/easyscience/variable/descriptor_number.py @@ -53,15 +53,14 @@ def __init__( ): """Constructor for the DescriptorNumber class - :param name: Name of the descriptor - :param value: Value of the descriptor - :param unit: Unit of the descriptor - :param variance: Variance of the descriptor - :param unique_name: Unique name of this object. This is used to find the object from anywhere in the program. - :param description: Description of the descriptor - :param url: URL of the descriptor - :param display_name: Display name of the descriptor - :param parent: Parent of the descriptor + param name: Name of the descriptor + param value: Value of the descriptor + param unit: Unit of the descriptor + param variance: Variance of the descriptor + param description: Description of the descriptor + param url: URL of the descriptor + param display_name: Display name of the descriptor + param parent: Parent of the descriptor .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ self._observers: List[DescriptorNumber] = [] diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 07cf5860..63746993 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -65,7 +65,6 @@ def __init__( :param min: The minimum value for fitting :param max: The maximum value for fitting :param fixed: If the parameter is free to vary during fitting - :param unique_name: Unique name of this object. This is used to find the object from anywhere in the program. :param description: A brief summary of what this object is :param url: Lookup url for documentation/information :param display_name: The name of the object as it should be displayed From 8559a6c0f63123936002821a8e3a1eccc83eb30b Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 7 May 2025 16:05:04 +0200 Subject: [PATCH 48/58] Rename serializers to suffixes --- src/easyscience/base_classes/base_obj.py | 18 ++++++------- src/easyscience/base_classes/based_base.py | 4 +-- .../fitting/calculators/interface_factory.py | 2 +- src/easyscience/io/__init__.py | 10 +++---- src/easyscience/io/serializer_base.py | 16 ++++++------ ..._serializer.py => serializer_component.py} | 26 +++++++++---------- ...{dict_serializer.py => serializer_dict.py} | 8 +++--- src/easyscience/utils/classTools.py | 8 +++--- src/easyscience/variable/descriptor_base.py | 4 +-- .../unit_tests/base_classes/test_base_obj.py | 4 +-- ...alizer.py => test_serializer_component.py} | 0 ..._serializer.py => test_serializer_dict.py} | 22 ++++++++-------- 12 files changed, 61 insertions(+), 61 deletions(-) rename src/easyscience/io/{component_serializer.py => serializer_component.py} (80%) rename src/easyscience/io/{dict_serializer.py => serializer_dict.py} (89%) rename tests/unit_tests/io/{test_component_serializer.py => test_serializer_component.py} (100%) rename tests/unit_tests/io/{test_dict_serializer.py => test_serializer_dict.py} (83%) diff --git a/src/easyscience/base_classes/base_obj.py b/src/easyscience/base_classes/base_obj.py index e047ae77..771c8e13 100644 --- a/src/easyscience/base_classes/base_obj.py +++ b/src/easyscience/base_classes/base_obj.py @@ -12,7 +12,7 @@ from .based_base import BasedBase if TYPE_CHECKING: - from ..io import ComponentSerializer + from ..io import SerializerComponent @@ -28,8 +28,8 @@ def __init__( self, name: str, unique_name: Optional[str] = None, - *args: Optional[ComponentSerializer], - **kwargs: Optional[ComponentSerializer], + *args: Optional[SerializerComponent], + **kwargs: Optional[SerializerComponent], ): """ Set up the base class. @@ -64,7 +64,7 @@ def __init__( test_class=BaseObj, ) - def _add_component(self, key: str, component: ComponentSerializer) -> None: + def _add_component(self, key: str, component: SerializerComponent) -> None: """ Dynamically add a component to the class. This is an internal method, though can be called remotely. The recommended alternative is to use typing, i.e. @@ -98,7 +98,7 @@ def __init__(self, foo: Parameter, bar: Parameter): test_class=BaseObj, ) - def __setattr__(self, key: str, value: ComponentSerializer) -> None: + def __setattr__(self, key: str, value: SerializerComponent) -> None: # Assume that the annotation is a ClassVar old_obj = None if ( @@ -130,15 +130,15 @@ def __repr__(self) -> str: return f"{self.__class__.__name__} `{getattr(self, 'name')}`" @staticmethod - def __getter(key: str) -> Callable[[ComponentSerializer], ComponentSerializer]: - def getter(obj: ComponentSerializer) -> ComponentSerializer: + def __getter(key: str) -> Callable[[SerializerComponent], SerializerComponent]: + def getter(obj: SerializerComponent) -> SerializerComponent: return obj._kwargs[key] return getter @staticmethod - def __setter(key: str) -> Callable[[ComponentSerializer], None]: - def setter(obj: ComponentSerializer, value: float) -> None: + def __setter(key: str) -> Callable[[SerializerComponent], None]: + def setter(obj: SerializerComponent, value: float) -> None: if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( value.__class__, (DescriptorBase) ): diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index 4c20b7db..ecabce3a 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -12,7 +12,7 @@ from easyscience import global_object -from ..io import ComponentSerializer +from ..io import SerializerComponent from ..variable import Parameter if TYPE_CHECKING: @@ -20,7 +20,7 @@ from ..variable.descriptor_base import DescriptorBase -class BasedBase(ComponentSerializer): +class BasedBase(SerializerComponent): __slots__ = ['_name', '_global_object', 'user_data', '_kwargs'] _REDIRECT = {} diff --git a/src/easyscience/fitting/calculators/interface_factory.py b/src/easyscience/fitting/calculators/interface_factory.py index 0ebb12a1..576ed8fc 100644 --- a/src/easyscience/fitting/calculators/interface_factory.py +++ b/src/easyscience/fitting/calculators/interface_factory.py @@ -54,7 +54,7 @@ def create(self, *args, **kwargs): def switch(self, new_interface: str, fitter: Optional[Type[Fitter]] = None): """ Changes the current interface to a new interface. The current interface is destroyed and - all ComponentSerializer parameters carried over to the new interface. i.e. pick up where you left off. + all SerializerComponent parameters carried over to the new interface. i.e. pick up where you left off. :param new_interface: name of new interface to be created :type new_interface: str diff --git a/src/easyscience/io/__init__.py b/src/easyscience/io/__init__.py index 8efb44e4..a4711e1b 100644 --- a/src/easyscience/io/__init__.py +++ b/src/easyscience/io/__init__.py @@ -1,12 +1,12 @@ # SPDX-FileCopyrightText: 2023 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project any: + def encode(self, obj: SerializerComponent, skip: Optional[List[str]] = None, **kwargs) -> any: """ Abstract implementation of an encoder. @@ -51,7 +51,7 @@ def encode(self, obj: ComponentSerializer, skip: Optional[List[str]] = None, **k @abstractmethod def decode(cls, obj: Any) -> Any: """ - Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. + Re-create an EasyScience object from the output of an encoder. The default decoder is `SerializerDict`. :param obj: encoded EasyScience object :return: Reformed EasyScience object @@ -112,7 +112,7 @@ def _encode_objs(obj: Any) -> Dict[str, Any]: def _convert_to_dict( self, - obj: ComponentSerializer, + obj: SerializerComponent, skip: Optional[List[str]] = None, full_encode: bool = False, **kwargs, @@ -235,9 +235,9 @@ def _convert_from_dict(d): if '@module' in d and '@class' in d: modname = d['@module'] classname = d['@class'] - # if classname in DictSerializer.REDIRECT.get(modname, {}): - # modname = DictSerializer.REDIRECT[modname][classname]["@module"] - # classname = DictSerializer.REDIRECT[modname][classname]["@class"] + # if classname in SerializerDict.REDIRECT.get(modname, {}): + # modname = SerializerDict.REDIRECT[modname][classname]["@module"] + # classname = SerializerDict.REDIRECT[modname][classname]["@class"] else: modname = None classname = None diff --git a/src/easyscience/io/component_serializer.py b/src/easyscience/io/serializer_component.py similarity index 80% rename from src/easyscience/io/component_serializer.py rename to src/easyscience/io/serializer_component.py index 3c89f638..75a02536 100644 --- a/src/easyscience/io/component_serializer.py +++ b/src/easyscience/io/serializer_component.py @@ -10,17 +10,17 @@ from typing import List from typing import Optional -from .dict_serializer import DictSerializer +from .serializer_dict import SerializerDict if TYPE_CHECKING: from .serializer_base import SerializerBase -class ComponentSerializer: +class SerializerComponent: """ This base class adds the capability of saving and loading (encoding/decoding, serializing/deserializing) easyscience objects via the `encode` and `decode` methods. - The default encoder is `DictSerializer`, which converts the object to a dictionary. + The default encoder is `SerializerDict`, which converts the object to a dictionary. Shortcuts for dictionary and encoding is also present. """ @@ -32,22 +32,22 @@ def __deepcopy__(self, memo): def encode(self, skip: Optional[List[str]] = None, encoder: Optional[SerializerBase] = None, **kwargs) -> Any: """ - Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `DictSerializer`. + Use an encoder to covert an EasyScience object into another format. Default is to a dictionary using `SerializerDict`. :param skip: List of field names as strings to skip when forming the encoded object - :param encoder: The encoder to be used for encoding the data. Default is `DictSerializer` + :param encoder: The encoder to be used for encoding the data. Default is `SerializerDict` :param kwargs: Any additional key word arguments to be passed to the encoder :return: encoded object containing all information to reform an EasyScience object. """ if encoder is None: - encoder = DictSerializer + encoder = SerializerDict encoder_obj = encoder() return encoder_obj.encode(self, skip=skip, **kwargs) @classmethod def decode(cls, obj: Any, decoder: Optional[SerializerBase] = None) -> Any: """ - Re-create an EasyScience object from the output of an encoder. The default decoder is `DictSerializer`. + Re-create an EasyScience object from the output of an encoder. The default decoder is `SerializerDict`. :param obj: encoded EasyScience object :param decoder: decoder to be used to reform the EasyScience object @@ -55,27 +55,27 @@ def decode(cls, obj: Any, decoder: Optional[SerializerBase] = None) -> Any: """ if decoder is None: - decoder = DictSerializer + decoder = SerializerDict return decoder.decode(obj) def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: """ - Convert an EasyScience object into a full dictionary using `DictSerializer`. - This is a shortcut for ```obj.encode(encoder=DictSerializer)``` + Convert an EasyScience object into a full dictionary using `SerializerDict`. + This is a shortcut for ```obj.encode(encoder=SerializerDict)``` :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. """ - return self.encode(skip=skip, encoder=DictSerializer) + return self.encode(skip=skip, encoder=SerializerDict) @classmethod def from_dict(cls, obj_dict: Dict[str, Any]) -> None: """ Re-create an EasyScience object from a full encoded dictionary. - :param obj_dict: dictionary containing the serialized contents (from `DictSerializer`) of an EasyScience object + :param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object :return: Reformed EasyScience object """ - return cls.decode(obj_dict, decoder=DictSerializer) + return cls.decode(obj_dict, decoder=SerializerDict) diff --git a/src/easyscience/io/dict_serializer.py b/src/easyscience/io/serializer_dict.py similarity index 89% rename from src/easyscience/io/dict_serializer.py rename to src/easyscience/io/serializer_dict.py index 092af468..aadc7ffc 100644 --- a/src/easyscience/io/dict_serializer.py +++ b/src/easyscience/io/serializer_dict.py @@ -15,17 +15,17 @@ from .serializer_base import SerializerBase if TYPE_CHECKING: - from .component_serializer import ComponentSerializer + from .serializer_component import SerializerComponent -class DictSerializer(SerializerBase): +class SerializerDict(SerializerBase): """ This is a serializer that can encode and decode EasyScience objects to and from a dictionary. """ def encode( self, - obj: ComponentSerializer, + obj: SerializerComponent, skip: Optional[List[str]] = None, full_encode: bool = False, **kwargs, @@ -43,7 +43,7 @@ def encode( return self._convert_to_dict(obj, skip=skip, full_encode=full_encode, **kwargs) @classmethod - def decode(cls, d: Dict) -> ComponentSerializer: + def decode(cls, d: Dict) -> SerializerComponent: """ Re-create an EasyScience object from the dictionary representation. diff --git a/src/easyscience/utils/classTools.py b/src/easyscience/utils/classTools.py index 4e65a791..9231eabd 100644 --- a/src/easyscience/utils/classTools.py +++ b/src/easyscience/utils/classTools.py @@ -13,10 +13,10 @@ if TYPE_CHECKING: from ..base_classes import BasedBase - from ..io import ComponentSerializer + from ..io import SerializerComponent -def addLoggedProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: +def addLoggedProp(inst: SerializerComponent, name: str, *args, **kwargs) -> None: cls = type(inst) annotations = getattr(cls, '__annotations__', False) if not hasattr(cls, '__perinstance'): @@ -29,7 +29,7 @@ def addLoggedProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None setattr(cls, name, LoggedProperty(*args, **kwargs)) -def addProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: +def addProp(inst: SerializerComponent, name: str, *args, **kwargs) -> None: cls = type(inst) annotations = getattr(cls, '__annotations__', False) if not hasattr(cls, '__perinstance'): @@ -43,7 +43,7 @@ def addProp(inst: ComponentSerializer, name: str, *args, **kwargs) -> None: setattr(cls, name, property(*args, **kwargs)) -def removeProp(inst: ComponentSerializer, name: str) -> None: +def removeProp(inst: SerializerComponent, name: str) -> None: cls = type(inst) if not hasattr(cls, '__perinstance'): cls = type(cls.__name__, (cls,), {'__module__': __name__}) diff --git a/src/easyscience/variable/descriptor_base.py b/src/easyscience/variable/descriptor_base.py index 9f971de5..094c10f9 100644 --- a/src/easyscience/variable/descriptor_base.py +++ b/src/easyscience/variable/descriptor_base.py @@ -10,10 +10,10 @@ from easyscience import global_object from easyscience.global_object.undo_redo import property_stack -from easyscience.io import ComponentSerializer +from easyscience.io import SerializerComponent -class DescriptorBase(ComponentSerializer, metaclass=abc.ABCMeta): +class DescriptorBase(SerializerComponent, metaclass=abc.ABCMeta): """ This is the base of all variable descriptions for models. It contains all information to describe a single unique property of an object. This description includes a name and value as well as optionally a unit, description diff --git a/tests/unit_tests/base_classes/test_base_obj.py b/tests/unit_tests/base_classes/test_base_obj.py index 5b220da8..f4d800f3 100644 --- a/tests/unit_tests/base_classes/test_base_obj.py +++ b/tests/unit_tests/base_classes/test_base_obj.py @@ -19,7 +19,7 @@ from easyscience.base_classes import BaseObj from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter -from easyscience.io import DictSerializer +from easyscience.io import SerializerDict from easyscience import global_object @pytest.fixture @@ -227,7 +227,7 @@ def check_dict(check, item): if "@module" in item.keys(): with not_raises([ValueError, AttributeError]): global_object.map._clear() - this_obj = DictSerializer().decode(item) + this_obj = SerializerDict().decode(item) for key in check.keys(): assert key in item.keys() diff --git a/tests/unit_tests/io/test_component_serializer.py b/tests/unit_tests/io/test_serializer_component.py similarity index 100% rename from tests/unit_tests/io/test_component_serializer.py rename to tests/unit_tests/io/test_serializer_component.py diff --git a/tests/unit_tests/io/test_dict_serializer.py b/tests/unit_tests/io/test_serializer_dict.py similarity index 83% rename from tests/unit_tests/io/test_dict_serializer.py rename to tests/unit_tests/io/test_serializer_dict.py index 42da0a8d..64c046d5 100644 --- a/tests/unit_tests/io/test_dict_serializer.py +++ b/tests/unit_tests/io/test_serializer_dict.py @@ -4,13 +4,13 @@ import pytest -from easyscience.io.dict_serializer import DictSerializer +from easyscience.io.serializer_dict import SerializerDict from easyscience.variable import DescriptorNumber from easyscience.base_classes import BaseObj -from .test_component_serializer import check_dict -from .test_component_serializer import dp_param_dict -from .test_component_serializer import skip_dict +from .test_serializer_component import check_dict +from .test_serializer_component import dp_param_dict +from .test_serializer_component import skip_dict from easyscience import global_object @@ -35,7 +35,7 @@ def recursive_remove(d, remove_keys: list) -> dict: ######################################################################################################################## @pytest.mark.parametrize(**skip_dict) @pytest.mark.parametrize(**dp_param_dict) -def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): +def test_variable_SerializerDict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber], skip): data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} obj = dp_cls(**data_dict) @@ -48,7 +48,7 @@ def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber] if not isinstance(skip, list): skip = [skip] - enc = obj.encode(skip=skip, encoder=DictSerializer) + enc = obj.encode(skip=skip, encoder=SerializerDict) expected_keys = set(dp_kwargs.keys()) obtained_keys = set(enc.keys()) @@ -63,14 +63,14 @@ def test_variable_DictSerializer(dp_kwargs: dict, dp_cls: Type[DescriptorNumber] # TESTING DECODING ######################################################################################################################## @pytest.mark.parametrize(**dp_param_dict) -def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +def test_variable_SerializerDict_decode(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} obj = dp_cls(**data_dict) - enc = obj.encode(encoder=DictSerializer) + enc = obj.encode(encoder=SerializerDict) global_object.map._clear() - dec = dp_cls.decode(enc, decoder=DictSerializer) + dec = dp_cls.decode(enc, decoder=SerializerDict) for k in data_dict.keys(): if hasattr(obj, k) and hasattr(dec, k): @@ -80,12 +80,12 @@ def test_variable_DictSerializer_decode(dp_kwargs: dict, dp_cls: Type[Descriptor @pytest.mark.parametrize(**dp_param_dict) -def test_variable_DictSerializer_from_dict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): +def test_variable_SerializerDict_from_dict(dp_kwargs: dict, dp_cls: Type[DescriptorNumber]): data_dict = {k: v for k, v in dp_kwargs.items() if k[0] != "@"} obj = dp_cls(**data_dict) - enc = obj.encode(encoder=DictSerializer) + enc = obj.encode(encoder=SerializerDict) global_object.map._clear() dec = dp_cls.from_dict(enc) From 2b6151fd30c63fc5523baebfd8f0acf86e7e5170 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Wed, 7 May 2025 16:19:10 +0200 Subject: [PATCH 49/58] Rename base_classes to suffixes --- Examples/base/README.rst | 2 +- Examples/base/plot_baseclass1.py | 8 +- docs/src/reference/base.rst | 4 +- src/easyscience/base_classes/__init__.py | 8 +- ...{base_collection.py => collection_base.py} | 8 +- .../base_classes/{base_obj.py => obj_base.py} | 20 ++-- .../fitting/minimizers/minimizer_base.py | 8 +- .../fitting/minimizers/minimizer_bumps.py | 14 +-- .../fitting/minimizers/minimizer_dfo.py | 10 +- .../fitting/minimizers/minimizer_lmfit.py | 14 +-- src/easyscience/fitting/multi_fitter.py | 4 +- .../global_object/hugger/property.py | 4 +- src/easyscience/job/analysis.py | 4 +- src/easyscience/job/experiment.py | 4 +- src/easyscience/job/job.py | 4 +- src/easyscience/job/theoreticalmodel.py | 4 +- src/easyscience/models/polynomial.py | 16 ++-- .../integration_tests/fitting/test_fitter.py | 6 +- .../fitting/test_multi_fitter.py | 8 +- ..._collection.py => test_collection_base.py} | 78 ++++++++-------- .../{test_base_obj.py => test_obj_base.py} | 92 +++++++++---------- tests/unit_tests/global_object/test_map.py | 12 +-- .../global_object/test_undo_redo.py | 14 +-- tests/unit_tests/io/test_serializer_dict.py | 10 +- tests/unit_tests/variable/test_parameter.py | 6 +- 25 files changed, 181 insertions(+), 181 deletions(-) rename src/easyscience/base_classes/{base_collection.py => collection_base.py} (97%) rename src/easyscience/base_classes/{base_obj.py => obj_base.py} (92%) rename tests/unit_tests/base_classes/{test_base_collection.py => test_collection_base.py} (87%) rename tests/unit_tests/base_classes/{test_base_obj.py => test_obj_base.py} (85%) diff --git a/Examples/base/README.rst b/Examples/base/README.rst index a91eda2f..46da573a 100644 --- a/Examples/base/README.rst +++ b/Examples/base/README.rst @@ -3,4 +3,4 @@ Subclassing Examples ------------------------ -This section gathers examples which correspond to subclassing the :class:`easyscience.base_classes.BaseObj` class. +This section gathers examples which correspond to subclassing the :class:`easyscience.base_classes.ObjBase` class. diff --git a/Examples/base/plot_baseclass1.py b/Examples/base/plot_baseclass1.py index 0f4eddfb..b87c559e 100644 --- a/Examples/base/plot_baseclass1.py +++ b/Examples/base/plot_baseclass1.py @@ -1,7 +1,7 @@ """ -Subclassing BaseObj - Simple Pendulum +Subclassing ObjBase - Simple Pendulum ===================================== -This example shows how to subclass :class:`easyscience.base_classes.BaseObj` with parameters from +This example shows how to subclass :class:`easyscience.base_classes.ObjBase` with parameters from :class:`EasyScience.variable.Parameter`. For this example a simple pendulum will be modeled. .. math:: @@ -17,7 +17,7 @@ import matplotlib.pyplot as plt import numpy as np -from easyscience.base_classes import BaseObj +from easyscience.base_classes import ObjBase from easyscience.variable import Parameter # %% @@ -29,7 +29,7 @@ # embedded rST text block: -class Pendulum(BaseObj): +class Pendulum(ObjBase): def __init__(self, A: Parameter, f: Parameter, p: Parameter): super(Pendulum, self).__init__('SimplePendulum', A=A, f=f, p=p) diff --git a/docs/src/reference/base.rst b/docs/src/reference/base.rst index 887500ba..ed3d05de 100644 --- a/docs/src/reference/base.rst +++ b/docs/src/reference/base.rst @@ -26,13 +26,13 @@ Super Classes :members: :inherited-members: -.. autoclass:: easyscience.base_classes.BaseObj +.. autoclass:: easyscience.base_classes.ObjBase :members: +_add_component :inherited-members: Collections =========== -.. autoclass:: easyscience.BaseCollection +.. autoclass:: easyscience.CollectionBase :members: :inherited-members: diff --git a/src/easyscience/base_classes/__init__.py b/src/easyscience/base_classes/__init__.py index 8c79346f..6eb510c2 100644 --- a/src/easyscience/base_classes/__init__.py +++ b/src/easyscience/base_classes/__init__.py @@ -1,9 +1,9 @@ -from .base_collection import BaseCollection -from .base_obj import BaseObj +from .collection_base import CollectionBase +from .obj_base import ObjBase from .based_base import BasedBase __all__ = [ - BaseObj, + ObjBase, BasedBase, - BaseCollection, + CollectionBase, ] diff --git a/src/easyscience/base_classes/base_collection.py b/src/easyscience/base_classes/collection_base.py similarity index 97% rename from src/easyscience/base_classes/base_collection.py rename to src/easyscience/base_classes/collection_base.py index b2fa07ea..96048042 100644 --- a/src/easyscience/base_classes/base_collection.py +++ b/src/easyscience/base_classes/collection_base.py @@ -24,12 +24,12 @@ -class BaseCollection(BasedBase, MutableSequence): +class CollectionBase(BasedBase, MutableSequence): """ This is the base class for which all higher level classes are built off of. NOTE: This object is serializable only if parameters are supplied as: - `BaseObj(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can - cheat with `BaseObj(*[Descriptor(...), Parameter(...), ...])`. + `ObjBase(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can + cheat with `ObjBase(*[Descriptor(...), Parameter(...), ...])`. """ def __init__( @@ -125,7 +125,7 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase :param idx: index or slice of the collection. :type idx: Union[int, slice] :return: Object at index `idx` - :rtype: Union[Parameter, Descriptor, BaseObj, 'BaseCollection'] + :rtype: Union[Parameter, Descriptor, ObjBase, 'CollectionBase'] """ if isinstance(idx, slice): start, stop, step = idx.indices(len(self)) diff --git a/src/easyscience/base_classes/base_obj.py b/src/easyscience/base_classes/obj_base.py similarity index 92% rename from src/easyscience/base_classes/base_obj.py rename to src/easyscience/base_classes/obj_base.py index 771c8e13..579826c5 100644 --- a/src/easyscience/base_classes/base_obj.py +++ b/src/easyscience/base_classes/obj_base.py @@ -16,12 +16,12 @@ -class BaseObj(BasedBase): +class ObjBase(BasedBase): """ This is the base class for which all higher level classes are built off of. NOTE: This object is serializable only if parameters are supplied as: - `BaseObj(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can - cheat with `BaseObj(*[Descriptor(...), Parameter(...), ...])`. + `ObjBase(a=value, b=value)`. For `Parameter` or `Descriptor` objects we can + cheat with `ObjBase(*[Descriptor(...), Parameter(...), ...])`. """ def __init__( @@ -38,18 +38,18 @@ def __init__( :param args: Any arguments? :param kwargs: Fields which this class should contain """ - super(BaseObj, self).__init__(name=name, unique_name=unique_name) + super(ObjBase, self).__init__(name=name, unique_name=unique_name) # If Parameter or Descriptor is given as arguments... for arg in args: - if issubclass(type(arg), (BaseObj, DescriptorBase)): + if issubclass(type(arg), (ObjBase, DescriptorBase)): kwargs[getattr(arg, 'name')] = arg # Set kwargs, also useful for serialization known_keys = self.__dict__.keys() self._kwargs = kwargs for key in kwargs.keys(): if key in known_keys: - raise AttributeError('Kwargs cannot overwrite class attributes in BaseObj.') - if issubclass(type(kwargs[key]), (BasedBase, DescriptorBase)) or 'BaseCollection' in [ + raise AttributeError('Kwargs cannot overwrite class attributes in ObjBase.') + if issubclass(type(kwargs[key]), (BasedBase, DescriptorBase)) or 'CollectionBase' in [ c.__name__ for c in type(kwargs[key]).__bases__ ]: self._global_object.map.add_edge(self, kwargs[key]) @@ -61,7 +61,7 @@ def __init__( self.__setter(key), get_id=key, my_self=self, - test_class=BaseObj, + test_class=ObjBase, ) def _add_component(self, key: str, component: SerializerComponent) -> None: @@ -95,7 +95,7 @@ def __init__(self, foo: Parameter, bar: Parameter): self.__setter(key), get_id=key, my_self=self, - test_class=BaseObj, + test_class=ObjBase, ) def __setattr__(self, key: str, value: SerializerComponent) -> None: @@ -119,7 +119,7 @@ def __setattr__(self, key: str, value: SerializerComponent) -> None: old_obj = self.__getattribute__(key) self._global_object.map.prune_vertex_from_edge(self, old_obj) self._global_object.map.add_edge(self, value) - super(BaseObj, self).__setattr__(key, value) + super(ObjBase, self).__setattr__(key, value) # Update the interface bindings if something changed (BasedBase and Descriptor) if old_obj is not None: old_interface = getattr(self, 'interface', None) diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index fdc3bbde..f24ef94a 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -17,7 +17,7 @@ import numpy as np # causes circular import when Parameter is imported -# from easyscience.base_classes import BaseObj +# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers @@ -36,10 +36,10 @@ class MinimizerBase(metaclass=ABCMeta): def __init__( self, - obj, #: BaseObj, + obj, #: ObjBase, fit_function: Callable, minimizer_enum: AvailableMinimizers, - ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 + ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 if minimizer_enum.method not in self.supported_methods(): raise FitError(f'Method {minimizer_enum.method} not available in {self.__class__}') self._object = obj @@ -159,7 +159,7 @@ def all_methods() -> List[str]: @staticmethod @abstractmethod - def convert_to_par_object(obj): # todo after constraint changes, add type hint: obj: BaseObj + def convert_to_par_object(obj): # todo after constraint changes, add type hint: obj: ObjBase """ Convert an `EasyScience.variable.Parameter` object to an engine Parameter object. """ diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index 157448be..e600d533 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -15,7 +15,7 @@ from bumps.parameter import Parameter as BumpsParameter # causes circular import when Parameter is imported -# from easyscience.base_classes import BaseObj +# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers @@ -32,22 +32,22 @@ class Bumps(MinimizerBase): """ This is a wrapper to Bumps: https://bumps.readthedocs.io/ - It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.base_classes.BaseObj`. + It allows for the Bumps fitting engine to use parameters declared in an `EasyScience.base_classes.ObjBase`. """ package = 'bumps' def __init__( self, - obj, #: BaseObj, + obj, #: ObjBase, fit_function: Callable, minimizer_enum: Optional[AvailableMinimizers] = None, - ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 + ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 """ - Initialize the fitting engine with a `BaseObj` and an arbitrary fitting function. + Initialize the fitting engine with a `ObjBase` and an arbitrary fitting function. :param obj: Object containing elements of the `Parameter` class - :type obj: BaseObj + :type obj: ObjBase :param fit_function: function that when called returns y values. 'x' must be the first and only positional argument. Additional values can be supplied by keyword/value pairs @@ -151,7 +151,7 @@ def convert_to_pars_obj(self, par_list: Optional[List] = None) -> List[BumpsPara :rtype: List[BumpsParameter] """ if par_list is None: - # Assume that we have a BaseObj for which we can obtain a list + # Assume that we have a ObjBase for which we can obtain a list par_list = self._object.get_fit_parameters() pars_obj = [self.__class__.convert_to_par_object(obj) for obj in par_list] return pars_obj diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 867dcb69..44e0186c 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -11,7 +11,7 @@ import numpy as np # causes circular import when Parameter is imported -# from easyscience.base_classes import BaseObj +# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers @@ -30,15 +30,15 @@ class DFO(MinimizerBase): def __init__( self, - obj, #: BaseObj, + obj, #: ObjBase, fit_function: Callable, minimizer_enum: Optional[AvailableMinimizers] = None, - ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 + ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 """ - Initialize the fitting engine with a `BaseObj` and an arbitrary fitting function. + Initialize the fitting engine with a `ObjBase` and an arbitrary fitting function. :param obj: Object containing elements of the `Parameter` class - :type obj: BaseObj + :type obj: ObjBase :param fit_function: function that when called returns y values. 'x' must be the first and only positional argument. Additional values can be supplied by keyword/value pairs diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index bc4de348..fccb5bf0 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -14,7 +14,7 @@ from lmfit.model import ModelResult # causes circular import when Parameter is imported -# from easyscience.base_classes import BaseObj +# from easyscience.base_classes import ObjBase from easyscience.variable import Parameter from ..available_minimizers import AvailableMinimizers @@ -27,22 +27,22 @@ class LMFit(MinimizerBase): # noqa: S101 """ This is a wrapper to the extended Levenberg-Marquardt Fit: https://lmfit.github.io/lmfit-py/ - It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.base_classes.BaseObj`. + It allows for the lmfit fitting engine to use parameters declared in an `EasyScience.base_classes.ObjBase`. """ package = 'lmfit' def __init__( self, - obj, #: BaseObj, + obj, #: ObjBase, fit_function: Callable, minimizer_enum: Optional[AvailableMinimizers] = None, - ): # todo after constraint changes, add type hint: obj: BaseObj # noqa: E501 + ): # todo after constraint changes, add type hint: obj: ObjBase # noqa: E501 """ - Initialize the minimizer with the `BaseObj` and the `fit_function` to be used. + Initialize the minimizer with the `ObjBase` and the `fit_function` to be used. :param obj: Base object which contains the parameters to be fitted - :type obj: BaseObj + :type obj: ObjBase :param fit_function: Function which will be fitted to the data :type fit_function: Callable :param method: Method to be used by the minimizer @@ -167,7 +167,7 @@ def convert_to_pars_obj(self, parameters: Optional[List[Parameter]] = None) -> L :return: lmfit Parameters compatible object """ if parameters is None: - # Assume that we have a BaseObj for which we can obtain a list + # Assume that we have a ObjBase for which we can obtain a list parameters = self._object.get_fit_parameters() lm_parameters = LMParameters().add_many([self.convert_to_par_object(parameter) for parameter in parameters]) return lm_parameters diff --git a/src/easyscience/fitting/multi_fitter.py b/src/easyscience/fitting/multi_fitter.py index 7273be49..c5a47d27 100644 --- a/src/easyscience/fitting/multi_fitter.py +++ b/src/easyscience/fitting/multi_fitter.py @@ -7,7 +7,7 @@ import numpy as np -from ..base_classes import BaseCollection +from ..base_classes import CollectionBase from .fitter import Fitter from .minimizers import FitResults @@ -24,7 +24,7 @@ def __init__( fit_functions: Optional[List[Callable]] = None, ): # Create a dummy core object to hold all the fit objects. - self._fit_objects = BaseCollection('multi', *fit_objects) + self._fit_objects = CollectionBase('multi', *fit_objects) self._fit_functions = fit_functions # Initialize with the first of the fit_functions, without this it is # not possible to change the fitting engine. diff --git a/src/easyscience/global_object/hugger/property.py b/src/easyscience/global_object/hugger/property.py index 6b585dbf..60a978d0 100644 --- a/src/easyscience/global_object/hugger/property.py +++ b/src/easyscience/global_object/hugger/property.py @@ -18,8 +18,8 @@ class LoggedProperty(property): """ Pump up python properties. In this case we can see who has called this property and then do something if a criteria is met. In this case if the caller is not a member of - the `BaseObj` class. Note that all high level `EasyScience` objects should be built from - `BaseObj`. + the `ObjBase` class. Note that all high level `EasyScience` objects should be built from + `ObjBase`. """ _global_object = global_object diff --git a/src/easyscience/job/analysis.py b/src/easyscience/job/analysis.py index 34a37bd4..45bfdd58 100644 --- a/src/easyscience/job/analysis.py +++ b/src/easyscience/job/analysis.py @@ -6,11 +6,11 @@ import numpy as np -from ..base_classes.base_obj import BaseObj +from ..base_classes.obj_base import ObjBase from ..fitting.minimizers import MinimizerBase -class AnalysisBase(BaseObj, metaclass=ABCMeta): +class AnalysisBase(ObjBase, metaclass=ABCMeta): """ This virtual class allows for the creation of technique-specific Analysis objects. """ diff --git a/src/easyscience/job/experiment.py b/src/easyscience/job/experiment.py index ee7158eb..34335a34 100644 --- a/src/easyscience/job/experiment.py +++ b/src/easyscience/job/experiment.py @@ -3,10 +3,10 @@ # © 2021-2023 Contributors to the EasyScience project np.ndarray: return np.polyval([c.value for c in self.coefficients], x) diff --git a/tests/integration_tests/fitting/test_fitter.py b/tests/integration_tests/fitting/test_fitter.py index 780dfb21..386301e4 100644 --- a/tests/integration_tests/fitting/test_fitter.py +++ b/tests/integration_tests/fitting/test_fitter.py @@ -8,11 +8,11 @@ from easyscience.fitting import Fitter from easyscience.fitting.minimizers import FitError from easyscience.fitting import AvailableMinimizers -from easyscience.base_classes import BaseObj +from easyscience.base_classes import ObjBase from easyscience.variable import Parameter # Model and container of parameters for tests -class AbsSin(BaseObj): +class AbsSin(ObjBase): phase: Parameter offset: Parameter @@ -25,7 +25,7 @@ def __call__(self, x): return np.abs(np.sin(self.phase.value * x + self.offset.value)) -class AbsSin2D(BaseObj): +class AbsSin2D(ObjBase): phase: Parameter offset: Parameter diff --git a/tests/integration_tests/fitting/test_multi_fitter.py b/tests/integration_tests/fitting/test_multi_fitter.py index 1c3178de..bfab4ee9 100644 --- a/tests/integration_tests/fitting/test_multi_fitter.py +++ b/tests/integration_tests/fitting/test_multi_fitter.py @@ -7,11 +7,11 @@ import numpy as np from easyscience.fitting.multi_fitter import MultiFitter from easyscience.fitting.minimizers import FitError -from easyscience.base_classes import BaseObj +from easyscience.base_classes import ObjBase from easyscience.variable import Parameter -class Line(BaseObj): +class Line(ObjBase): m: Parameter c: Parameter @@ -24,7 +24,7 @@ def __call__(self, x): return self.m.value * x + self.c.value -class AbsSin(BaseObj): +class AbsSin(ObjBase): phase: Parameter offset: Parameter @@ -37,7 +37,7 @@ def __call__(self, x): return np.abs(np.sin(self.phase.value * x + self.offset.value)) -class AbsSin2D(BaseObj): +class AbsSin2D(ObjBase): phase: Parameter offset: Parameter diff --git a/tests/unit_tests/base_classes/test_base_collection.py b/tests/unit_tests/base_classes/test_collection_base.py similarity index 87% rename from tests/unit_tests/base_classes/test_base_collection.py rename to tests/unit_tests/base_classes/test_collection_base.py index f15179d2..07f8fa42 100644 --- a/tests/unit_tests/base_classes/test_base_collection.py +++ b/tests/unit_tests/base_classes/test_collection_base.py @@ -6,15 +6,15 @@ import pytest import easyscience -from easyscience.base_classes import BaseCollection -from easyscience.base_classes import BaseObj +from easyscience.base_classes import CollectionBase +from easyscience.base_classes import ObjBase from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter from easyscience import global_object test_dict = { "@module": "easyscience.base_classes", - "@class": "BaseCollection", + "@class": "CollectionBase", "@version": easyscience.__version__, "name": "testing", "data": [ @@ -26,7 +26,7 @@ "value": 1.0, "unit": "dimensionless", "variance": None, - "unique_name": "BaseCollection_0", + "unique_name": "CollectionBase_0", "description": "", "url": "", "display_name": "par1", @@ -35,11 +35,11 @@ } -class Alpha(BaseCollection): +class Alpha(CollectionBase): pass -class_constructors = [BaseCollection, Alpha] +class_constructors = [CollectionBase, Alpha] @pytest.fixture @@ -56,7 +56,7 @@ def setup_pars(): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_from_base(cls, setup_pars): +def test_CollectionBase_from_base(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] coll = cls(name, **setup_pars) @@ -72,14 +72,14 @@ def test_baseCollection_from_base(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", range(1, 11)) -def test_baseCollection_from_baseObj(cls, setup_pars: dict, value: int): +def test_CollectionBase_from_ObjBase(cls, setup_pars: dict, value: int): name = setup_pars["name"] del setup_pars["name"] objs = {} prefix = "obj" for idx in range(value): - objs[prefix + str(idx)] = BaseObj(prefix + str(idx), **setup_pars) + objs[prefix + str(idx)] = ObjBase(prefix + str(idx), **setup_pars) coll = cls(name, **objs) @@ -96,7 +96,7 @@ def test_baseCollection_from_baseObj(cls, setup_pars: dict, value: int): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", ("abc", False, (), [])) -def test_baseCollection_create_fail(cls, setup_pars, value): +def test_CollectionBase_create_fail(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] setup_pars["to_fail"] = value @@ -107,7 +107,7 @@ def test_baseCollection_create_fail(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("key", ("user_data", "_kwargs", "interface")) -def test_baseCollection_create_fail2(cls, setup_pars, key): +def test_CollectionBase_create_fail2(cls, setup_pars, key): name = setup_pars["name"] del setup_pars["name"] setup_pars[key] = DescriptorNumber("fail_name", 0) @@ -117,7 +117,7 @@ def test_baseCollection_create_fail2(cls, setup_pars, key): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_append_base(cls, setup_pars): +def test_CollectionBase_append_base(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] @@ -136,7 +136,7 @@ def test_baseCollection_append_base(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", ("abc", False, (), [])) -def test_baseCollection_append_fail(cls, setup_pars, value): +def test_CollectionBase_append_fail(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -147,7 +147,7 @@ def test_baseCollection_append_fail(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", (0, 1, 3, "par1", "des1")) -def test_baseCollection_getItem(cls, setup_pars, value): +def test_CollectionBase_getItem(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -163,7 +163,7 @@ def test_baseCollection_getItem(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", (False, [], (), 100, 100.4)) -def test_baseCollection_getItem_type_fail(cls, setup_pars, value): +def test_CollectionBase_getItem_type_fail(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -174,7 +174,7 @@ def test_baseCollection_getItem_type_fail(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_getItem_slice(cls, setup_pars): +def test_CollectionBase_getItem_slice(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] @@ -186,7 +186,7 @@ def test_baseCollection_getItem_slice(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", (0, 1, 3)) -def test_baseCollection_setItem(cls, setup_pars, value): +def test_CollectionBase_setItem(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -205,7 +205,7 @@ def test_baseCollection_setItem(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", ("abc", (), [])) -def test_baseCollection_setItem_fail(cls, setup_pars, value): +def test_CollectionBase_setItem_fail(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -218,7 +218,7 @@ def test_baseCollection_setItem_fail(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", (0, 1, 3)) -def test_baseCollection_delItem(cls, setup_pars, value): +def test_CollectionBase_delItem(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -237,7 +237,7 @@ def test_baseCollection_delItem(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) @pytest.mark.parametrize("value", (0, 1, 3)) -def test_baseCollection_len(cls, setup_pars, value): +def test_CollectionBase_len(cls, setup_pars, value): name = setup_pars["name"] del setup_pars["name"] @@ -249,7 +249,7 @@ def test_baseCollection_len(cls, setup_pars, value): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_get_parameters(cls, setup_pars): +def test_CollectionBase_get_parameters(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] obj = cls(name, **setup_pars) @@ -258,10 +258,10 @@ def test_baseCollection_get_parameters(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_get_parameters_nested(cls, setup_pars): +def test_CollectionBase_get_parameters_nested(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) name2 = name + "_2" obj2 = cls(name2, obj=obj, **setup_pars) @@ -271,7 +271,7 @@ def test_baseCollection_get_parameters_nested(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_get_fit_parameters(cls, setup_pars): +def test_CollectionBase_get_fit_parameters(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] obj = cls(name, **setup_pars) @@ -280,10 +280,10 @@ def test_baseCollection_get_fit_parameters(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_get_fit_parameters_nested(cls, setup_pars): +def test_CollectionBase_get_fit_parameters_nested(cls, setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) name2 = name + "_2" obj2 = cls(name2, obj=obj, **setup_pars) @@ -293,7 +293,7 @@ def test_baseCollection_get_fit_parameters_nested(cls, setup_pars): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_dir(cls): +def test_CollectionBase_dir(cls): name = "testing" kwargs = {"p1": DescriptorNumber("par1", 1)} obj = cls(name, **kwargs) @@ -329,7 +329,7 @@ def test_baseCollection_dir(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_as_dict(cls): +def test_CollectionBase_as_dict(cls): name = "testing" kwargs = {"p1": DescriptorNumber("par1", 1)} obj = cls(name, **kwargs) @@ -373,7 +373,7 @@ def testit(item1, item2): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_from_dict(cls): +def test_CollectionBase_from_dict(cls): global_object.map._clear() #TODO: figure out why this test fails without this line name = "testing" kwargs = {"p1": DescriptorNumber("par1", 1)} @@ -388,7 +388,7 @@ def test_baseCollection_from_dict(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_repr(cls): +def test_CollectionBase_repr(cls): name = "test" p1 = Parameter("p1", 1) obj = cls(name, p1) @@ -398,7 +398,7 @@ def test_baseCollection_repr(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_iterator(cls): +def test_CollectionBase_iterator(cls): name = "test" p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) @@ -414,7 +414,7 @@ def test_baseCollection_iterator(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_iterator_dict(cls): +def test_CollectionBase_iterator_dict(cls): global_object.map._clear() #TODO: figure out why this test fails without this line name = "test" p1 = Parameter("p1", 1) @@ -434,7 +434,7 @@ def test_baseCollection_iterator_dict(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_sameName(cls): +def test_CollectionBase_sameName(cls): global_object.map._clear() #TODO: figure out why this test fails without this line name = "test" p1 = Parameter("p1", 1) @@ -455,7 +455,7 @@ def test_baseCollection_sameName(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_set_index(cls): +def test_CollectionBase_set_index(cls): name = "test" p1 = Parameter("p1", 1) p2 = Parameter("p1", 2) @@ -477,7 +477,7 @@ def test_baseCollection_set_index(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_set_index_based(cls): +def test_CollectionBase_set_index_based(cls): name = "test" p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) @@ -501,7 +501,7 @@ def test_baseCollection_set_index_based(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_sort(cls): +def test_CollectionBase_sort(cls): name = "test" v = [1, 4, 3, 2, 5] expected = [1, 2, 3, 4, 5] @@ -512,7 +512,7 @@ def test_baseCollection_sort(cls): @pytest.mark.parametrize("cls", class_constructors) -def test_baseCollection_sort_reverse(cls): +def test_CollectionBase_sort_reverse(cls): name = "test" v = [1, 4, 3, 2, 5] expected = [1, 2, 3, 4, 5] @@ -523,12 +523,12 @@ def test_baseCollection_sort_reverse(cls): assert item.value == expected[i] -class Beta(BaseObj): +class Beta(ObjBase): pass @pytest.mark.parametrize("cls", class_constructors) -def test_basecollectionGraph(cls): +def test_CollectionBaseGraph(cls): from easyscience import global_object G = global_object.map diff --git a/tests/unit_tests/base_classes/test_base_obj.py b/tests/unit_tests/base_classes/test_obj_base.py similarity index 85% rename from tests/unit_tests/base_classes/test_base_obj.py rename to tests/unit_tests/base_classes/test_obj_base.py index f4d800f3..13aa6b73 100644 --- a/tests/unit_tests/base_classes/test_base_obj.py +++ b/tests/unit_tests/base_classes/test_obj_base.py @@ -16,7 +16,7 @@ import pytest import easyscience -from easyscience.base_classes import BaseObj +from easyscience.base_classes import ObjBase from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter from easyscience.io import SerializerDict @@ -64,7 +64,7 @@ def not_raises( (["par1", "des1"], ["par2", "des2"]), ], ) -def test_baseobj_create(setup_pars: dict, a: List[str], kw: List[str]): +def test_ObjBase_create(setup_pars: dict, a: List[str], kw: List[str]): name = setup_pars["name"] args = [] for key in a: @@ -72,14 +72,14 @@ def test_baseobj_create(setup_pars: dict, a: List[str], kw: List[str]): kwargs = {} for key in kw: kwargs[key] = setup_pars[key] - base = BaseObj(name, None, *args, **kwargs) + base = ObjBase(name, None, *args, **kwargs) assert base.name == name for key in a: item = getattr(base, setup_pars[key].name) assert isinstance(item, setup_pars[key].__class__) -def test_baseobj_copy(setup_pars: dict): +def test_ObjBase_copy(setup_pars: dict): # When name = setup_pars["name"] args = [] @@ -88,7 +88,7 @@ def test_baseobj_copy(setup_pars: dict): kwargs = {} for key in ["par2", "des2"]: kwargs[key] = setup_pars[key] - base = BaseObj(name, None, *args, **kwargs) + base = ObjBase(name, None, *args, **kwargs) # Then base_copy = copy(base) @@ -102,7 +102,7 @@ def test_baseobj_copy(setup_pars: dict): assert isinstance(item, setup_pars[key].__class__) -def test_baseobj_get(setup_pars: dict): +def test_ObjBase_get(setup_pars: dict): name = setup_pars["name"] explicit_name1 = "par1" explicit_name2 = "par2" @@ -110,30 +110,30 @@ def test_baseobj_get(setup_pars: dict): setup_pars[explicit_name1].name: setup_pars[explicit_name1], setup_pars[explicit_name2].name: setup_pars[explicit_name2], } - obj = BaseObj(name, **kwargs) + obj = ObjBase(name, **kwargs) with not_raises(AttributeError): p1: Parameter = obj.p1 with not_raises(AttributeError): p2: Parameter = obj.p2 -def test_baseobj_set(setup_pars: dict): +def test_ObjBase_set(setup_pars: dict): name = setup_pars["name"] explicit_name1 = "par1" kwargs = { setup_pars[explicit_name1].name: setup_pars[explicit_name1], } - obj = BaseObj(name, **kwargs) + obj = ObjBase(name, **kwargs) new_value = 5.0 with not_raises([AttributeError, ValueError]): obj.p1 = new_value assert obj.p1.value == new_value -def test_baseobj_get_parameters(setup_pars: dict): +def test_ObjBase_get_parameters(setup_pars: dict): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) pars = obj.get_fit_parameters() assert isinstance(pars, list) assert len(pars) == 2 @@ -142,22 +142,22 @@ def test_baseobj_get_parameters(setup_pars: dict): assert "p3" in par_names -def test_baseobj_fit_objects(setup_pars: dict): +def test_ObjBase_fit_objects(setup_pars: dict): pass -def test_baseobj_as_dict(clear, setup_pars: dict): +def test_ObjBase_as_dict(clear, setup_pars: dict): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) obtained = obj.as_dict() assert isinstance(obtained, dict) expected = { - "@module": "easyscience.base_classes.base_obj", - "@class": "BaseObj", + "@module": "easyscience.base_classes.obj_base", + "@class": "ObjBase", "@version": easyscience.__version__, "name": "test", - "unique_name": "BaseObj_0", + "unique_name": "ObjBase_0", "par1": { "@module": Parameter.__module__, "@class": Parameter.__name__, @@ -239,27 +239,27 @@ def check_dict(check, item): check_dict(expected, obtained) -def test_baseobj_dict_roundtrip(clear, setup_pars: dict): +def test_ObjBase_dict_roundtrip(clear, setup_pars: dict): # When name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars, unique_name='special_name') + obj = ObjBase(name, **setup_pars, unique_name='special_name') obj_dict = obj.as_dict() global_object.map._clear() # Then - new_obj = BaseObj.from_dict(obj_dict) + new_obj = ObjBase.from_dict(obj_dict) # Expect new_obj_dict = new_obj.as_dict() assert obj_dict == new_obj_dict -def test_baseobj_dir(setup_pars): +def test_ObjBase_dir(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) expected = [ "encode", "decode", @@ -287,21 +287,21 @@ def test_baseobj_dir(setup_pars): assert len(set(expected).difference(set(obtained))) == 0 -def test_baseobj_get_parameters(setup_pars): +def test_ObjBase_get_parameters(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) pars = obj.get_parameters() assert len(pars) == 3 -def test_baseobj_get_parameters_nested(setup_pars): +def test_ObjBase_get_parameters_nested(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) name2 = name + "_2" - obj2 = BaseObj(name2, obj=obj, **setup_pars) + obj2 = ObjBase(name2, obj=obj, **setup_pars) pars = obj2.get_parameters() assert len(pars) == 6 @@ -310,21 +310,21 @@ def test_baseobj_get_parameters_nested(setup_pars): assert len(pars) == 3 -def test_baseobj_get_fit_parameters(setup_pars): +def test_ObjBase_get_fit_parameters(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) pars = obj.get_fit_parameters() assert len(pars) == 2 -def test_baseobj_get_fit_parameters_nested(setup_pars): +def test_ObjBase_get_fit_parameters_nested(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) name2 = name + "_2" - obj2 = BaseObj(name2, obj=obj, **setup_pars) + obj2 = ObjBase(name2, obj=obj, **setup_pars) pars = obj2.get_fit_parameters() assert len(pars) == 4 @@ -333,10 +333,10 @@ def test_baseobj_get_fit_parameters_nested(setup_pars): assert len(pars) == 2 -def test_baseobj__add_component(setup_pars): +def test_ObjBase__add_component(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) p = Parameter("added_par", 1) new_item_name = "Added" @@ -347,14 +347,14 @@ def test_baseobj__add_component(setup_pars): assert isinstance(a, Parameter) -def test_baseObj_name(setup_pars): +def test_ObjBase_name(setup_pars): name = setup_pars["name"] del setup_pars["name"] - obj = BaseObj(name, **setup_pars) + obj = ObjBase(name, **setup_pars) assert obj.name == name def test_Base_GETSET(): - class A(BaseObj): + class A(ObjBase): def __init__(self, a: Parameter): super(A, self).__init__("a", a=a) @@ -376,7 +376,7 @@ def from_pars(cls, a: float): def test_Base_GETSET(): - class A(BaseObj): + class A(ObjBase): def __init__(self, a: Parameter): super(A, self).__init__("a", a=a) b = 0 @@ -392,7 +392,7 @@ def from_pars(cls, a: float): def test_Base_GETSET_v2(): - class A(BaseObj): + class A(ObjBase): a: ClassVar[Parameter] @@ -417,7 +417,7 @@ def from_pars(cls, a: float): def test_Base_GETSET_v3(): - class A(BaseObj): + class A(ObjBase): a: ClassVar[Parameter] @@ -447,7 +447,7 @@ def from_pars(cls, a: float): def test_BaseCreation(): - class A(BaseObj): + class A(ObjBase): def __init__(self, a: Optional[Union[Parameter, float]] = None): super(A, self).__init__("A", a=Parameter("a", 1.0)) if a is not None: @@ -462,7 +462,7 @@ def __init__(self, a: Optional[Union[Parameter, float]] = None): a.a = 4.0 assert a.a.value == 4.0 - class B(BaseObj): + class B(ObjBase): def __init__(self, b: Optional[Union[A, Parameter, float]] = None): super(B, self).__init__("B", b=A()) if b is not None: @@ -481,13 +481,13 @@ def __init__(self, b: Optional[Union[A, Parameter, float]] = None): def test_unique_name_generator(clear): # When Then - test_obj = BaseObj(name="test") + test_obj = ObjBase(name="test") # Expect - assert test_obj.unique_name == "BaseObj_0" + assert test_obj.unique_name == "ObjBase_0" def test_unique_name_change(clear): # When - test_obj = BaseObj(name="test") + test_obj = ObjBase(name="test") # Then test_obj.unique_name = "test" # Expect @@ -496,7 +496,7 @@ def test_unique_name_change(clear): @pytest.mark.parametrize("input", [2, 2.0, [2], {2}, None]) def test_unique_name_change_exception(input): # When - test_obj = BaseObj(name="test") + test_obj = ObjBase(name="test") # Then Expect with pytest.raises(TypeError): test_obj.unique_name = input \ No newline at end of file diff --git a/tests/unit_tests/global_object/test_map.py b/tests/unit_tests/global_object/test_map.py index 2f0151f2..04f6dc42 100644 --- a/tests/unit_tests/global_object/test_map.py +++ b/tests/unit_tests/global_object/test_map.py @@ -3,7 +3,7 @@ # © 2021-2023 Contributors to the EasyScience project Date: Wed, 7 May 2025 16:30:20 +0200 Subject: [PATCH 50/58] Reorganize imports --- src/easyscience/base_classes/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/easyscience/base_classes/__init__.py b/src/easyscience/base_classes/__init__.py index 6eb510c2..7e4b2819 100644 --- a/src/easyscience/base_classes/__init__.py +++ b/src/easyscience/base_classes/__init__.py @@ -1,9 +1,9 @@ +from .based_base import BasedBase from .collection_base import CollectionBase from .obj_base import ObjBase -from .based_base import BasedBase __all__ = [ - ObjBase, BasedBase, CollectionBase, + ObjBase, ] From 705c8654e57715a3316e2232c0ac92cc838a4712 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 12 May 2025 10:54:36 +0200 Subject: [PATCH 51/58] Small PR reviews --- src/easyscience/global_object/global_object.py | 1 + tests/unit_tests/Objects/variable/test_parameter.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index dd188db2..23d51e9e 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -36,6 +36,7 @@ def __init__(self): # Map. This is the conduit database between all global object species self.map: Map = self.__map + # Unique global ID for each new parameter update. Used by dependent parameters to detect cyclic dependencies. self.update_id_iterator = 0 def instantiate_stack(self): diff --git a/tests/unit_tests/Objects/variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py index 78e19da0..8418a3da 100644 --- a/tests/unit_tests/Objects/variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -133,6 +133,7 @@ def test_make_dependent_on(self, normal_parameter: Parameter): independent_parameter.value = 2 # Expect + normal_parameter.value == 4 self.compare_parameters(normal_parameter, 2*independent_parameter) def test_parameter_from_dependency(self, normal_parameter: Parameter): @@ -207,7 +208,7 @@ def test_process_dependency_unique_names_exception_unique_name_does_not_exist(se normal_parameter._dependency_map = {} # Then Expect - with pytest.raises(ValueError): + with pytest.raises(ValueError, match='A Parameter with unique_name Special_name does not exist. Please check your dependency expression.'): normal_parameter._process_dependency_unique_names(dependency_expression='2*"Special_name"') def test_process_dependency_unique_names_exception_not_a_descriptorNumber(self, clear, normal_parameter: Parameter): From 98f2165bfde60247efabb96dd342bcfbdcc6f4c9 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 12 May 2025 13:48:39 +0200 Subject: [PATCH 52/58] Bring critical classes to the front and utilize these imports in tests --- src/easyscience/__init__.py | 12 ++++++++++-- tests/integration_tests/fitting/test_fitter.py | 8 ++++---- tests/integration_tests/fitting/test_multi_fitter.py | 4 ++-- .../unit_tests/base_classes/test_collection_base.py | 6 +++--- tests/unit_tests/base_classes/test_obj_base.py | 8 ++++---- tests/unit_tests/fitting/minimizers/test_factory.py | 2 +- .../fitting/minimizers/test_minimizer_base.py | 2 +- .../fitting/minimizers/test_minimizer_lmfit.py | 2 +- tests/unit_tests/fitting/test_fitter.py | 4 ++-- tests/unit_tests/global_object/test_map.py | 4 ++-- tests/unit_tests/global_object/test_undo_redo.py | 9 ++++----- tests/unit_tests/io/test_serializer_component.py | 4 ++-- tests/unit_tests/io/test_serializer_dict.py | 4 ++-- tests/unit_tests/variable/test_descriptor_array.py | 2 +- tests/unit_tests/variable/test_descriptor_number.py | 2 +- tests/unit_tests/variable/test_parameter.py | 6 +++--- 16 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/easyscience/__init__.py b/src/easyscience/__init__.py index 588ffb81..e3d4b084 100644 --- a/src/easyscience/__init__.py +++ b/src/easyscience/__init__.py @@ -7,10 +7,18 @@ from .__version__ import __version__ as __version__ # noqa: E402 -from .fitting.available_minimizers import AvailableMinimizers # noqa: E402 +from .base_classes import ObjBase # noqa: E402 +from .fitting import AvailableMinimizers # noqa: E402 +from .fitting import Fitter # noqa: E402 +from .variable import DescriptorNumber # noqa: E402 +from .variable import Parameter # noqa: E402 __all__ = [ __version__, - AvailableMinimizers, global_object, + ObjBase, + AvailableMinimizers, + Fitter, + DescriptorNumber, + Parameter, ] diff --git a/tests/integration_tests/fitting/test_fitter.py b/tests/integration_tests/fitting/test_fitter.py index 386301e4..0ac9a25f 100644 --- a/tests/integration_tests/fitting/test_fitter.py +++ b/tests/integration_tests/fitting/test_fitter.py @@ -5,11 +5,11 @@ import pytest import numpy as np -from easyscience.fitting import Fitter +from easyscience import Fitter +from easyscience import AvailableMinimizers +from easyscience import ObjBase +from easyscience import Parameter from easyscience.fitting.minimizers import FitError -from easyscience.fitting import AvailableMinimizers -from easyscience.base_classes import ObjBase -from easyscience.variable import Parameter # Model and container of parameters for tests class AbsSin(ObjBase): diff --git a/tests/integration_tests/fitting/test_multi_fitter.py b/tests/integration_tests/fitting/test_multi_fitter.py index bfab4ee9..a23111a4 100644 --- a/tests/integration_tests/fitting/test_multi_fitter.py +++ b/tests/integration_tests/fitting/test_multi_fitter.py @@ -7,8 +7,8 @@ import numpy as np from easyscience.fitting.multi_fitter import MultiFitter from easyscience.fitting.minimizers import FitError -from easyscience.base_classes import ObjBase -from easyscience.variable import Parameter +from easyscience import ObjBase +from easyscience import Parameter class Line(ObjBase): diff --git a/tests/unit_tests/base_classes/test_collection_base.py b/tests/unit_tests/base_classes/test_collection_base.py index 07f8fa42..b5983162 100644 --- a/tests/unit_tests/base_classes/test_collection_base.py +++ b/tests/unit_tests/base_classes/test_collection_base.py @@ -7,9 +7,9 @@ import easyscience from easyscience.base_classes import CollectionBase -from easyscience.base_classes import ObjBase -from easyscience.variable import DescriptorNumber -from easyscience.variable import Parameter +from easyscience import ObjBase +from easyscience import DescriptorNumber +from easyscience import Parameter from easyscience import global_object test_dict = { diff --git a/tests/unit_tests/base_classes/test_obj_base.py b/tests/unit_tests/base_classes/test_obj_base.py index 13aa6b73..08322de9 100644 --- a/tests/unit_tests/base_classes/test_obj_base.py +++ b/tests/unit_tests/base_classes/test_obj_base.py @@ -16,11 +16,11 @@ import pytest import easyscience -from easyscience.base_classes import ObjBase -from easyscience.variable import DescriptorNumber -from easyscience.variable import Parameter -from easyscience.io import SerializerDict +from easyscience import ObjBase +from easyscience import DescriptorNumber +from easyscience import Parameter from easyscience import global_object +from easyscience.io import SerializerDict @pytest.fixture def clear(): diff --git a/tests/unit_tests/fitting/minimizers/test_factory.py b/tests/unit_tests/fitting/minimizers/test_factory.py index 9a860955..323ce8b7 100644 --- a/tests/unit_tests/fitting/minimizers/test_factory.py +++ b/tests/unit_tests/fitting/minimizers/test_factory.py @@ -1,6 +1,6 @@ +from easyscience import AvailableMinimizers from easyscience.fitting.minimizers.factory import factory from easyscience.fitting.available_minimizers import from_string_to_enum -from easyscience.fitting.available_minimizers import AvailableMinimizers from easyscience.fitting.minimizers import MinimizerBase from unittest.mock import MagicMock import pytest diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_base.py b/tests/unit_tests/fitting/minimizers/test_minimizer_base.py index 1af9ae99..e4fcb005 100644 --- a/tests/unit_tests/fitting/minimizers/test_minimizer_base.py +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_base.py @@ -8,7 +8,7 @@ from easyscience.fitting.minimizers.minimizer_base import MinimizerBase from easyscience.fitting.minimizers.utils import FitError -from easyscience.variable import Parameter +from easyscience import Parameter class TestMinimizerBase(): @pytest.fixture diff --git a/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py index 92316bc6..8a187709 100644 --- a/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py +++ b/tests/unit_tests/fitting/minimizers/test_minimizer_lmfit.py @@ -4,7 +4,7 @@ import easyscience.fitting.minimizers.minimizer_lmfit from easyscience.fitting.minimizers.minimizer_lmfit import LMFit -from easyscience.variable import Parameter +from easyscience import Parameter from lmfit import Parameter as LMParameter from easyscience.fitting.minimizers.utils import FitError diff --git a/tests/unit_tests/fitting/test_fitter.py b/tests/unit_tests/fitting/test_fitter.py index 992225ce..dede119c 100644 --- a/tests/unit_tests/fitting/test_fitter.py +++ b/tests/unit_tests/fitting/test_fitter.py @@ -3,8 +3,8 @@ import pytest import numpy as np import easyscience.fitting.fitter -from easyscience.fitting.fitter import Fitter -from easyscience.fitting.available_minimizers import AvailableMinimizers +from easyscience import Fitter +from easyscience import AvailableMinimizers class TestFitter(): diff --git a/tests/unit_tests/global_object/test_map.py b/tests/unit_tests/global_object/test_map.py index 04f6dc42..d470080d 100644 --- a/tests/unit_tests/global_object/test_map.py +++ b/tests/unit_tests/global_object/test_map.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause # © 2021-2023 Contributors to the EasyScience project Date: Mon, 12 May 2025 15:11:15 +0200 Subject: [PATCH 53/58] remove _get_class_module from SerializerBase --- src/easyscience/io/serializer_base.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/easyscience/io/serializer_base.py b/src/easyscience/io/serializer_base.py index 604a2eca..0627e8ce 100644 --- a/src/easyscience/io/serializer_base.py +++ b/src/easyscience/io/serializer_base.py @@ -128,10 +128,10 @@ def _convert_to_dict( if new_obj is not obj: return new_obj - d = {'@module': self._get_class_module(obj), '@class': obj.__class__.__name__} + d = {'@module': obj.__module__, '@class': obj.__class__.__name__} try: - parent_module = self._get_class_module(obj).split('.')[0] + parent_module = obj.__module__.split('.')[0] module_version = import_module(parent_module).__version__ # type: ignore d['@version'] = '{}'.format(module_version) except (AttributeError, ImportError): @@ -263,13 +263,6 @@ def _convert_from_dict(d): return [SerializerBase._convert_from_dict(x) for x in d] return d - def _get_class_module(self, obj): - """ - Returns the REAL module of the class of the object. - """ - c = getattr(obj, '__old_class__', obj.__class__) - return c.__module__ - def _recursive_encoder(self, obj, skip: List[str] = [], encoder=None, full_encode=False, **kwargs): """ Walk through an object encoding it From 7a3eccffa6a2ece7741ac2fc161f42a4a9aa6e38 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 16 May 2025 13:46:27 +0200 Subject: [PATCH 54/58] Properly revert Parameters to previous state when make_dependent_on fails --- src/easyscience/variable/parameter.py | 48 ++++++++++++++++++--- tests/unit_tests/variable/test_parameter.py | 45 ++++++++++++++++--- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 63746993..89273ed4 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -185,11 +185,20 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional if not isinstance(value, DescriptorNumber): raise TypeError(f'`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}.') # noqa: E501 - # If we're overwriting the dependency + # If we're overwriting the dependency, store the old attributes + # in case we need to revert back to the old dependency + self._previous_independent = self._independent if not self._independent: - for old_dependency in self._dependency_map.values(): - old_dependency._detach_observer(self) + self._previous_dependency = { + '_dependency_string': self._dependency_string, + '_dependency_map': self._dependency_map, + '_dependency_interpreter': self._dependency_interpreter, + '_clean_dependency_string': self._clean_dependency_string, + } + for dependency in self._dependency_map.values(): + dependency._detach_observer(self) + self._independent = False self._dependency_string = dependency_expression self._dependency_map = dependency_map if dependency_map is not None else {} # List of allowed python constructs for the asteval interpreter @@ -201,8 +210,13 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional 'try': False, 'while': False, 'with': False} self._dependency_interpreter = Interpreter(config=asteval_config) self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies - - self._process_dependency_unique_names(self._dependency_string) + + try: + self._process_dependency_unique_names(self._dependency_string) + except ValueError as error: + self._revert_dependency(skip_detach=True) + raise error + for key, value in self._dependency_map.items(): self._dependency_interpreter.symtable[key] = value self._dependency_interpreter.readonly_symbols.add(key) # Dont allow overwriting of the dependencies in the dependency expression # noqa: E501 @@ -210,15 +224,19 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional try: dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) except NameError as message: + self._revert_dependency() raise NameError('\nUnknown name encountered in dependecy expression:'+ '\n'+'\n'.join(str(message).split("\n")[1:])+ '\nPlease check your expression or add the name to the `dependency_map`') from None except Exception as message: + self._revert_dependency() raise SyntaxError('\nError encountered in dependecy expression:'+ '\n'+'\n'.join(str(message).split("\n")[1:])+ '\nPlease check your expression') from None if not isinstance(dependency_result, DescriptorNumber): - raise TypeError(f'The dependency expression: "{self._dependency_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 + error_string = self._dependency_string + self._revert_dependency() + raise TypeError(f'The dependency expression: "{error_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 self._scalar.value = dependency_result.value self._scalar.unit = dependency_result.unit self._scalar.variance = dependency_result.variance @@ -226,7 +244,6 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._max.value = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value self._min.unit = dependency_result.unit self._max.unit = dependency_result.unit - self._independent = False self._fixed = False self._notify_observers() @@ -511,6 +528,23 @@ def free(self) -> bool: def free(self, value: bool) -> None: self.fixed = not value + def _revert_dependency(self, skip_detach=False) -> None: + """ + Revert the dependency to the old dependency. This is used when an error is raised during setting the dependency. + """ + if self._previous_independent is True: + self.make_independent() + else: + if not skip_detach: + for dependency in self._dependency_map.values(): + dependency._detach_observer(self) + for key, value in self._previous_dependency.items(): + setattr(self, key, value) + for dependency in self._dependency_map.values(): + dependency._attach_observer(self) + del self._previous_dependency + del self._previous_independent + def _process_dependency_unique_names(self, dependency_expression: str): """ Add the unique names of the parameters to the ASTEval interpreter. This is used to evaluate the dependency expression. diff --git a/tests/unit_tests/variable/test_parameter.py b/tests/unit_tests/variable/test_parameter.py index 8c696614..8f6aad6b 100644 --- a/tests/unit_tests/variable/test_parameter.py +++ b/tests/unit_tests/variable/test_parameter.py @@ -225,7 +225,7 @@ def test_process_dependency_unique_names_exception_not_a_descriptorNumber(self, ('2*a', ['a', Parameter(name='a', value=1)]), ('2*a', {4: Parameter(name='a', value=1)}), ('2*a', {'a': ObjBase(name='a')}), - ], ids=["dependecy_expression_not_a_string", "dependency_map_not_a_dict", "dependency_map_keys_not_strings", "dependency_map_values_not_descriptor_number"]) + ], ids=["dependency_expression_not_a_string", "dependency_map_not_a_dict", "dependency_map_keys_not_strings", "dependency_map_values_not_descriptor_number"]) def test_parameter_from_dependency_input_exceptions(self, dependency_expression, dependency_map): # When Then Expect with pytest.raises(TypeError): @@ -239,15 +239,46 @@ def test_parameter_from_dependency_input_exceptions(self, dependency_expression, ('2*a + b', NameError), ('2*a + 3*', SyntaxError), ('2 + 2', TypeError), - ], ids=["parameter_not_in_map", "invalid_dependency_expression", "result_not_a_descriptor_number"]) - def test_parameter_from_dependency_evaluation_exceptions(self, normal_parameter, dependency_expression, error): - # When Then Expect + ('2*"special_name"', ValueError), + ], ids=["parameter_not_in_map", "invalid_dependency_expression", "result_not_a_descriptor_number", "unique_name_does_not_exist"]) + def test_parameter_make_dependent_on_exceptions_cleanup_previously_dependent(self, normal_parameter, dependency_expression, error): + # When + independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + dependent_parameter = Parameter.from_dependency( + name= 'dependent', + dependency_expression='best', + dependency_map={'best': independent_parameter} + ) + # Then Expect + # Check that the correct error is raised with pytest.raises(error): - Parameter.from_dependency( - name = 'dependent', + dependent_parameter.make_dependent_on( dependency_expression=dependency_expression, dependency_map={'a': normal_parameter}, - ) + ) + # Check that everything is properly cleaned up + assert normal_parameter._observers == [] + assert dependent_parameter.independent == False + assert dependent_parameter.dependency_expression == 'best' + assert dependent_parameter.dependency_map == {'best': independent_parameter} + independent_parameter.value = 50 + self.compare_parameters(dependent_parameter, independent_parameter) + + def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent(self, normal_parameter): + # When + independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + # Then Expect + # Check that the correct error is raised + with pytest.raises(NameError): + independent_parameter.make_dependent_on( + dependency_expression='2*a + b', + dependency_map={'a': normal_parameter}, + ) + # Check that everything is properly cleaned up + assert normal_parameter._observers == [] + assert independent_parameter.independent == True + normal_parameter.value = 50 + assert independent_parameter.value == 10 def test_dependent_parameter_updates(self, normal_parameter: Parameter): # When From 7ab41497a8e9181e71bc0b11c886733c0cf6e883 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 16 May 2025 15:23:18 +0200 Subject: [PATCH 55/58] Implement the ping system for detecting cyclic dependencies --- .../global_object/global_object.py | 3 - src/easyscience/variable/descriptor_number.py | 23 +++++--- src/easyscience/variable/parameter.py | 57 +++++++------------ tests/unit_tests/variable/test_parameter.py | 5 ++ 4 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/easyscience/global_object/global_object.py b/src/easyscience/global_object/global_object.py index 48b8d53d..f37a4a20 100644 --- a/src/easyscience/global_object/global_object.py +++ b/src/easyscience/global_object/global_object.py @@ -32,9 +32,6 @@ def __init__(self): # Map. This is the conduit database between all global object species self.map: Map = self.__map - # Unique global ID for each new parameter update. Used by dependent parameters to detect cyclic dependencies. - self.update_id_iterator = 0 - def instantiate_stack(self): """ The undo/redo stack references the collective. Hence it has to be imported diff --git a/src/easyscience/variable/descriptor_number.py b/src/easyscience/variable/descriptor_number.py index 98631542..8c4ac581 100644 --- a/src/easyscience/variable/descriptor_number.py +++ b/src/easyscience/variable/descriptor_number.py @@ -117,17 +117,24 @@ def _detach_observer(self, observer: DescriptorNumber) -> None: """Detach an observer from the descriptor.""" self._observers.remove(observer) - def _notify_observers(self, update_id=None) -> None: - """Notify all observers of a change. - - :param update_id: Optional update ID to pass to observers. Used to avoid cyclic depenencies. + def _notify_observers(self) -> None: + """Notify all observers of a change.""" + for observer in self._observers: + observer._update() + + def _ping_observers(self, ping_origin=None) -> None: + """Ping all observers to check if any cyclic dependencies has been introduced. + :param ping_origin: Unique_name of the origin of this ping. Used to avoid cyclic depenencies. """ - if update_id is None: - self._global_object.update_id_iterator += 1 - update_id = self._global_object.update_id_iterator + if ping_origin == self.unique_name: + raise RuntimeError('\n Cyclic dependency detected!\n' + + f'An update of {self.unique_name} leads to it updating itself.\n' + + 'Please check your dependencies.') + if ping_origin is None: + ping_origin = self.unique_name for observer in self._observers: - observer._update(update_id=update_id, updating_object=self.unique_name) + observer._ping_observers(ping_origin=ping_origin) @property def full_value(self) -> Variable: diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 89273ed4..d6b5eaad 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -88,7 +88,6 @@ def __init__( if not isinstance(fixed, bool): raise TypeError('`fixed` must be either True or False') self._independent = True - self._observers: List[DescriptorNumber] = [] self._fixed = fixed # For fitting, but must be initialized before super().__init__ self._min = sc.scalar(float(min), unit=unit) self._max = sc.scalar(float(max), unit=unit) @@ -127,35 +126,21 @@ def from_dependency(cls, name: str, dependency_expression: str, dependency_map: parameter.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) return parameter - - def _update(self, update_id: int, updating_object: str) -> None: + def _update(self) -> None: """ Update the parameter. This is called by the DescriptorNumbers/Parameters who have this Parameter as a dependency. - - :param update_id: The id of the update. This is used to avoid cyclic dependencies. - :param updating_object: The unique_name of the object which is updating this parameter. - """ if not self._independent: - # Check if this parameter has already been updated by the updating object with this update id - if updating_object not in self._dependency_updates: - self._dependency_updates[updating_object] = 0 - if self._dependency_updates[updating_object] == update_id: - raise RuntimeError('\n Potential cyclic dependency detected!\n' + - f'This parameter, {self.unique_name}, has already been updated by {updating_object} during this update.\n' + # noqa: E501 - 'Please check your dependencies.') - else: - # Update the value of the parameter using the dependency interpreter - temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) - self._scalar.value = temporary_parameter.value - self._scalar.unit = temporary_parameter.unit - self._scalar.variance = temporary_parameter.variance - self._min.value = temporary_parameter.min if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 - self._max.value = temporary_parameter.max if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 - self._min.unit = temporary_parameter.unit - self._max.unit = temporary_parameter.unit - self._dependency_updates[updating_object] = update_id - self._notify_observers(update_id=update_id) + # Update the value of the parameter using the dependency interpreter + temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) + self._scalar.value = temporary_parameter.value + self._scalar.unit = temporary_parameter.unit + self._scalar.variance = temporary_parameter.variance + self._min.value = temporary_parameter.min if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 + self._max.value = temporary_parameter.max if isinstance(temporary_parameter, Parameter) else temporary_parameter.value # noqa: E501 + self._min.unit = temporary_parameter.unit + self._max.unit = temporary_parameter.unit + self._notify_observers() else: warnings.warn('This parameter is not dependent. It cannot be updated.') @@ -209,8 +194,8 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional 'listcomp': False, 'dictcomp': False, 'setcomp': False, 'try': False, 'while': False, 'with': False} self._dependency_interpreter = Interpreter(config=asteval_config) - self._dependency_updates = {} # Used to track update ids to avoid cyclic dependencies + # Process the dependency expression for unique names try: self._process_dependency_unique_names(self._dependency_string) except ValueError as error: @@ -221,6 +206,7 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._dependency_interpreter.symtable[key] = value self._dependency_interpreter.readonly_symbols.add(key) # Dont allow overwriting of the dependencies in the dependency expression # noqa: E501 value._attach_observer(self) + # Check the dependency expression for errors try: dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) except NameError as message: @@ -237,15 +223,15 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional error_string = self._dependency_string self._revert_dependency() raise TypeError(f'The dependency expression: "{error_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 - self._scalar.value = dependency_result.value - self._scalar.unit = dependency_result.unit - self._scalar.variance = dependency_result.variance - self._min.value = dependency_result.min if isinstance(dependency_result, Parameter) else dependency_result.value - self._max.value = dependency_result.max if isinstance(dependency_result, Parameter) else dependency_result.value - self._min.unit = dependency_result.unit - self._max.unit = dependency_result.unit + # Check for cyclic dependencies + try: + self._ping_observers() + except RuntimeError as error: + self._revert_dependency() + raise error + # Update the parameter with the dependency result self._fixed = False - self._notify_observers() + self._update() def make_independent(self) -> None: """ @@ -259,7 +245,6 @@ def make_independent(self) -> None: dependency._detach_observer(self) self._independent = True del self._dependency_map - del self._dependency_updates del self._dependency_interpreter del self._dependency_string del self._clean_dependency_string diff --git a/tests/unit_tests/variable/test_parameter.py b/tests/unit_tests/variable/test_parameter.py index 8f6aad6b..85201fa2 100644 --- a/tests/unit_tests/variable/test_parameter.py +++ b/tests/unit_tests/variable/test_parameter.py @@ -348,6 +348,11 @@ def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Paramet # Then Expect with pytest.raises(RuntimeError): normal_parameter.make_dependent_on(dependency_expression='2*c', dependency_map={'c': dependent_parameter_2}) + # Check that everything is properly cleaned up + assert dependent_parameter_2._observers == [] + assert normal_parameter.independent == True + normal_parameter.value = 50 + self.compare_parameters(dependent_parameter_2, 4*normal_parameter) def test_dependent_parameter_logical_dependency(self, normal_parameter: Parameter): # When From fcb42f44c76cac2e0bb3bc24130b469e2997b864 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 16 May 2025 15:41:29 +0200 Subject: [PATCH 56/58] Ruff --- src/easyscience/variable/parameter.py | 1 - tests/unit_tests/variable/test_parameter.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index d6b5eaad..b8a4e0fa 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -11,7 +11,6 @@ import weakref from typing import Any from typing import Dict -from typing import List from typing import Optional from typing import Union diff --git a/tests/unit_tests/variable/test_parameter.py b/tests/unit_tests/variable/test_parameter.py index 85201fa2..356ed83d 100644 --- a/tests/unit_tests/variable/test_parameter.py +++ b/tests/unit_tests/variable/test_parameter.py @@ -351,6 +351,7 @@ def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Paramet # Check that everything is properly cleaned up assert dependent_parameter_2._observers == [] assert normal_parameter.independent == True + assert normal_parameter.value == 1 normal_parameter.value = 50 self.compare_parameters(dependent_parameter_2, 4*normal_parameter) From a955691cd7571588f3e704f0e447be7cf563f220 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 19 May 2025 15:24:12 +0200 Subject: [PATCH 57/58] PR Comments --- src/easyscience/io/serializer_base.py | 3 --- src/easyscience/io/serializer_component.py | 1 - src/easyscience/variable/descriptor_number.py | 14 +++++++------- src/easyscience/variable/parameter.py | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/easyscience/io/serializer_base.py b/src/easyscience/io/serializer_base.py index 0627e8ce..bece34e3 100644 --- a/src/easyscience/io/serializer_base.py +++ b/src/easyscience/io/serializer_base.py @@ -235,9 +235,6 @@ def _convert_from_dict(d): if '@module' in d and '@class' in d: modname = d['@module'] classname = d['@class'] - # if classname in SerializerDict.REDIRECT.get(modname, {}): - # modname = SerializerDict.REDIRECT[modname][classname]["@module"] - # classname = SerializerDict.REDIRECT[modname][classname]["@class"] else: modname = None classname = None diff --git a/src/easyscience/io/serializer_component.py b/src/easyscience/io/serializer_component.py index 75a02536..702e4071 100644 --- a/src/easyscience/io/serializer_component.py +++ b/src/easyscience/io/serializer_component.py @@ -25,7 +25,6 @@ class SerializerComponent: Shortcuts for dictionary and encoding is also present. """ - _CORE = True def __deepcopy__(self, memo): return self.from_dict(self.as_dict()) diff --git a/src/easyscience/variable/descriptor_number.py b/src/easyscience/variable/descriptor_number.py index 8c4ac581..1b1e257b 100644 --- a/src/easyscience/variable/descriptor_number.py +++ b/src/easyscience/variable/descriptor_number.py @@ -122,19 +122,19 @@ def _notify_observers(self) -> None: for observer in self._observers: observer._update() - def _ping_observers(self, ping_origin=None) -> None: - """Ping all observers to check if any cyclic dependencies has been introduced. + def _validate_dependencies(self, origin=None) -> None: + """Ping all observers to check if any cyclic dependencies have been introduced. - :param ping_origin: Unique_name of the origin of this ping. Used to avoid cyclic depenencies. + :param origin: Unique_name of the origin of this validation check. Used to avoid cyclic depenencies. """ - if ping_origin == self.unique_name: + if origin == self.unique_name: raise RuntimeError('\n Cyclic dependency detected!\n' + f'An update of {self.unique_name} leads to it updating itself.\n' + 'Please check your dependencies.') - if ping_origin is None: - ping_origin = self.unique_name + if origin is None: + origin = self.unique_name for observer in self._observers: - observer._ping_observers(ping_origin=ping_origin) + observer._validate_dependencies(origin=origin) @property def full_value(self) -> Variable: diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index b8a4e0fa..f1357e38 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -224,7 +224,7 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional raise TypeError(f'The dependency expression: "{error_string}" returned a {type(dependency_result)}, it should return a Parameter or DescriptorNumber.') # noqa: E501 # Check for cyclic dependencies try: - self._ping_observers() + self._validate_dependencies() except RuntimeError as error: self._revert_dependency() raise error From 35e8213366023887de822ad60810c5b33517c8f6 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Mon, 19 May 2025 15:29:28 +0200 Subject: [PATCH 58/58] Update copyright year to 2025 --- LICENSE | 2 +- src/easyscience/base_classes/based_base.py | 4 ++-- src/easyscience/base_classes/collection_base.py | 4 ++-- src/easyscience/base_classes/obj_base.py | 4 ++-- src/easyscience/fitting/calculators/__init__.py | 4 ++-- src/easyscience/fitting/calculators/interface_factory.py | 4 ++-- src/easyscience/fitting/fitter.py | 4 ++-- src/easyscience/fitting/minimizers/__init__.py | 4 ++-- src/easyscience/fitting/minimizers/minimizer_base.py | 4 ++-- src/easyscience/fitting/minimizers/minimizer_bumps.py | 4 ++-- src/easyscience/fitting/minimizers/minimizer_dfo.py | 4 ++-- src/easyscience/fitting/minimizers/minimizer_lmfit.py | 4 ++-- src/easyscience/fitting/multi_fitter.py | 4 ++-- src/easyscience/global_object/global_object.py | 4 ++-- src/easyscience/global_object/hugger/__init__.py | 4 ++-- src/easyscience/global_object/hugger/hugger.py | 4 ++-- src/easyscience/global_object/hugger/property.py | 4 ++-- src/easyscience/global_object/logger.py | 4 ++-- src/easyscience/global_object/map.py | 4 ++-- src/easyscience/global_object/undo_redo.py | 4 ++-- src/easyscience/io/__init__.py | 4 ++-- src/easyscience/io/serializer_base.py | 4 ++-- src/easyscience/io/serializer_component.py | 4 ++-- src/easyscience/io/serializer_dict.py | 4 ++-- src/easyscience/job/analysis.py | 4 ++-- src/easyscience/job/experiment.py | 4 ++-- src/easyscience/job/job.py | 4 ++-- src/easyscience/job/theoreticalmodel.py | 4 ++-- src/easyscience/legacy/dict.py | 4 ++-- src/easyscience/legacy/json.py | 4 ++-- src/easyscience/legacy/legacy_core.py | 4 ++-- src/easyscience/legacy/xml.py | 4 ++-- src/easyscience/models/__init__.py | 4 ++-- src/easyscience/models/polynomial.py | 4 ++-- src/easyscience/utils/__init__.py | 4 ++-- src/easyscience/utils/classTools.py | 4 ++-- src/easyscience/utils/classUtils.py | 4 ++-- src/easyscience/utils/decorators.py | 4 ++-- src/easyscience/utils/string.py | 4 ++-- src/easyscience/variable/descriptor_base.py | 4 ++-- src/easyscience/variable/parameter.py | 4 ++-- tests/__init__.py | 4 ++-- tests/coords.py | 4 ++-- tests/integration_tests/fitting/test_fitter.py | 4 ++-- tests/integration_tests/fitting/test_multi_fitter.py | 4 ++-- tests/package_test.py | 2 +- tests/unit_tests/__init__.py | 4 ++-- tests/unit_tests/base_classes/test_collection_base.py | 4 ++-- tests/unit_tests/base_classes/test_obj_base.py | 4 ++-- tests/unit_tests/fitting/__init__.py | 4 ++-- tests/unit_tests/global_object/test_map.py | 4 ++-- tests/unit_tests/global_object/test_undo_redo.py | 4 ++-- tests/unit_tests/io/__init__.py | 4 ++-- tests/unit_tests/models/__init__.py | 4 ++-- tests/unit_tests/models/test_polynomial.py | 4 ++-- 55 files changed, 108 insertions(+), 108 deletions(-) diff --git a/LICENSE b/LICENSE index c1ee0cf3..f21bf746 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Easyscience contributors (https://github.com/EasyScience) +Copyright (c) 2025, Easyscience contributors (https://github.com/EasyScience) All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/src/easyscience/base_classes/based_base.py b/src/easyscience/base_classes/based_base.py index ecabce3a..0af389e4 100644 --- a/src/easyscience/base_classes/based_base.py +++ b/src/easyscience/base_classes/based_base.py @@ -1,8 +1,8 @@ from __future__ import annotations -# SPDX-FileCopyrightText: 2023 EasyScience contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project +# © 2025 Contributors to the EasyScience project diff --git a/tests/unit_tests/models/__init__.py b/tests/unit_tests/models/__init__.py index 17adea14..bb769856 100644 --- a/tests/unit_tests/models/__init__.py +++ b/tests/unit_tests/models/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022 EasyScience contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project +# © 2025 Contributors to the EasyScience project diff --git a/tests/unit_tests/models/test_polynomial.py b/tests/unit_tests/models/test_polynomial.py index e6d0fbd5..799a917a 100644 --- a/tests/unit_tests/models/test_polynomial.py +++ b/tests/unit_tests/models/test_polynomial.py @@ -1,6 +1,6 @@ -# SPDX-FileCopyrightText: 2022 EasyScience contributors +# SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project +# © 2025 Contributors to the EasyScience project import numpy as np