From 7141fc25b01122654f7f6055125d69975762aeee Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 19 Dec 2024 10:40:18 +0100 Subject: [PATCH 01/48] Numpy2 (#93) Added support for Numpy 2, dropped support for Python 3.9, added support for Python 3.13 --- .github/workflows/documentation-build.yml | 2 +- .github/workflows/python-ci.yml | 2 +- .github/workflows/python-package.yml | 2 +- .github/workflows/python-publish.yml | 2 +- pyproject.toml | 12 ++-- .../Objects/new_variable/parameter.py | 2 +- .../Objects/new_variable/test_parameter.py | 61 ++++++++++--------- 7 files changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index 159fd675..108a631f 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -32,7 +32,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.10 - name: Install Pandoc, repo and dependencies run: | sudo apt install pandoc diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index ac9967bd..b6dcf7bd 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4a69555c..97d04bfb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12', '3.13'] if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 57bab9d4..603adcdc 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.10 - name: Install dependencies and build run: | diff --git a/pyproject.toml b/pyproject.toml index 5984f52f..734444b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,22 +21,22 @@ classifiers = [ "Topic :: Scientific/Engineering", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Development Status :: 3 - Alpha" ] -requires-python = ">=3.9,<3.13" +requires-python = ">=3.10" dependencies = [ "asteval", "bumps", "DFO-LS", "lmfit", - "numpy==1.26", # Should be updated to numpy 2.0 + "numpy", "uncertainties", "xarray", - "pint==0.23", # Only to ensure that unit is reported as dimensionless rather than empty string + "pint", # Only to ensure that unit is reported as dimensionless rather than empty string "scipp" ] @@ -128,13 +128,13 @@ force-single-line = true legacy_tox_ini = """ [tox] isolated_build = True -envlist = py{3.9,3.10,3.11,3.12} +envlist = py{3.10,3.11,3.12,3.13} [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [gh-actions:env] PLATFORM = ubuntu-latest: linux diff --git a/src/easyscience/Objects/new_variable/parameter.py b/src/easyscience/Objects/new_variable/parameter.py index ebad24b4..c2c21793 100644 --- a/src/easyscience/Objects/new_variable/parameter.py +++ b/src/easyscience/Objects/new_variable/parameter.py @@ -418,7 +418,7 @@ def __repr__(self) -> str: if self.fixed: super_str += ' (fixed)' s.append(super_str) - s.append('bounds=[%s:%s]' % (repr(self.min), repr(self.max))) + s.append('bounds=[%s:%s]' % (repr(float(self.min)), repr(float(self.max)))) return '%s>' % ', '.join(s) # Seems redundant diff --git a/tests/unit_tests/Objects/new_variable/test_parameter.py b/tests/unit_tests/Objects/new_variable/test_parameter.py index 5269d81b..f6958dc2 100644 --- a/tests/unit_tests/Objects/new_variable/test_parameter.py +++ b/tests/unit_tests/Objects/new_variable/test_parameter.py @@ -101,6 +101,8 @@ def test_min(self, parameter: Parameter): def test_set_min(self, parameter: Parameter): # When Then + self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value + parameter.min = 0.1 # Expect @@ -189,6 +191,7 @@ def test_bounds(self, parameter: Parameter): def test_set_bounds(self, parameter: Parameter): # When + self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value parameter._enabled = False parameter._fixed = True @@ -298,6 +301,7 @@ def test_set_full_value(self, parameter: Parameter): def test_copy(self, parameter: Parameter): # When Then + self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value parameter_copy = parameter.__copy__() # Expect @@ -317,6 +321,7 @@ def test_copy(self, parameter: Parameter): 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 @@ -337,7 +342,7 @@ def test_as_data_dict(self, clear, parameter: Parameter): @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)), + (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)), (Parameter("test", 2, "cm", 0.01, -10, 10), Parameter("name + test", 1.02, "m", 0.010001, -0.1, 10.1), Parameter("test + name", 102, "cm", 100.01, -10, 1010))], ids=["regular", "no_bounds", "unit_conversion"]) def test_addition_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): @@ -427,7 +432,7 @@ def test_addition_exception(self, parameter : Parameter, test): @pytest.mark.parametrize("test, expected, expected_reverse", [ (Parameter("test", 2, "m", 0.01, -20, 20), Parameter("name - test", -1, "m", 0.02, -20, 30), Parameter("test - name", 1, "m", 0.02, -30, 20)), - (Parameter("test", 2, "m", 0.01), Parameter("name - test", -1, "m", 0.02, min=-np.Inf, max=np.Inf),Parameter("test - name", 1, "m", 0.02, min=-np.Inf, max=np.Inf)), + (Parameter("test", 2, "m", 0.01), Parameter("name - test", -1, "m", 0.02, min=-np.inf, max=np.inf),Parameter("test - name", 1, "m", 0.02, min=-np.inf, max=np.inf)), (Parameter("test", 2, "cm", 0.01, -10, 10), Parameter("name - test", 0.98, "m", 0.010001, -0.1, 10.1), Parameter("test - name", -98, "cm", 100.01, -1010, 10))], ids=["regular", "no_bounds", "unit_conversion"]) def test_subtraction_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): @@ -457,8 +462,8 @@ def test_subtraction_with_parameter(self, parameter : Parameter, test : Paramete def test_subtraction_with_parameter_nan_cases(self): # When - parameter = Parameter(name="name", value=1, variance=0.01, min=-np.Inf, max=np.Inf) - test = Parameter(name="test", value=2, variance=0.01, min=-np.Inf, max=np.Inf) + parameter = Parameter(name="name", value=1, variance=0.01, min=-np.inf, max=np.inf) + test = Parameter(name="test", value=2, variance=0.01, min=-np.inf, max=np.inf) # Then result = parameter - test @@ -469,15 +474,15 @@ def test_subtraction_with_parameter_nan_cases(self): assert result.value == -1.0 assert result.unit == "dimensionless" assert result.variance == 0.02 - assert result.min == -np.Inf - assert result.max == np.Inf + assert result.min == -np.inf + assert result.max == np.inf assert result_reverse.name == result_reverse.unique_name assert result_reverse.value == 1.0 assert result_reverse.unit == "dimensionless" assert result_reverse.variance == 0.02 - assert result_reverse.min == -np.Inf - assert result_reverse.max == np.Inf + assert result_reverse.min == -np.inf + assert result_reverse.max == np.inf def test_subtraction_with_scalar(self): # When @@ -541,7 +546,7 @@ def test_subtraction_exception(self, parameter : Parameter, test): @pytest.mark.parametrize("test, expected, expected_reverse", [ (Parameter("test", 2, "m", 0.01, -10, 20), Parameter("name * test", 2, "m^2", 0.05, -100, 200), Parameter("test * name", 2, "m^2", 0.05, -100, 200)), - (Parameter("test", 2, "m", 0.01), Parameter("name * test", 2, "m^2", 0.05, min=-np.Inf, max=np.Inf), Parameter("test * name", 2, "m^2", 0.05, min=-np.Inf, max=np.Inf)), + (Parameter("test", 2, "m", 0.01), Parameter("name * test", 2, "m^2", 0.05, min=-np.inf, max=np.inf), Parameter("test * name", 2, "m^2", 0.05, min=-np.inf, max=np.inf)), (Parameter("test", 2, "dm", 0.01, -10, 20), Parameter("name * test", 0.2, "m^2", 0.0005, -10, 20), Parameter("test * name", 0.2, "m^2", 0.0005, -10, 20))], ids=["regular", "no_bounds", "base_unit_conversion"]) def test_multiplication_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): @@ -568,12 +573,12 @@ def test_multiplication_with_parameter(self, parameter : Parameter, test : Param assert result_reverse.max == expected_reverse.max @pytest.mark.parametrize("test, expected, expected_reverse", [ - (Parameter("test", 0, "", 0.01, -10, 0), Parameter("name * test", 0.0, "dimensionless", 0.01, -np.Inf, 0), Parameter("test * name", 0, "dimensionless", 0.01, -np.Inf, 0)), - (Parameter("test", 0, "", 0.01, 0, 10), Parameter("name * test", 0.0, "dimensionless", 0.01, 0, np.Inf), Parameter("test * name", 0, "dimensionless", 0.01, 0, np.Inf))], + (Parameter("test", 0, "", 0.01, -10, 0), Parameter("name * test", 0.0, "dimensionless", 0.01, -np.inf, 0), Parameter("test * name", 0, "dimensionless", 0.01, -np.inf, 0)), + (Parameter("test", 0, "", 0.01, 0, 10), Parameter("name * test", 0.0, "dimensionless", 0.01, 0, np.inf), Parameter("test * name", 0, "dimensionless", 0.01, 0, np.inf))], ids=["zero_min", "zero_max"]) def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_reverse): # When - parameter = Parameter(name="name", value=1, variance=0.01, min=1, max=np.Inf) + parameter = Parameter(name="name", value=1, variance=0.01, min=1, max=np.inf) # Then result = parameter * test @@ -656,9 +661,9 @@ def test_multiplication_with_scalar(self, parameter : Parameter, test, expected, assert result_reverse.max == expected_reverse.max @pytest.mark.parametrize("test, expected, expected_reverse", [ - (Parameter("test", 2, "s", 0.01, -10, 20), Parameter("name / test", 0.5, "m/s", 0.003125, -np.Inf, np.Inf), Parameter("test / name", 2, "s/m", 0.05, -np.Inf, np.Inf)), - (Parameter("test", 2, "s", 0.01, 0, 20), Parameter("name / test", 0.5, "m/s", 0.003125, 0.0, np.Inf), Parameter("test / name", 2, "s/m", 0.05, 0.0, np.Inf)), - (Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.Inf, 0.0), Parameter("test / name", -2, "s/m", 0.05, -np.Inf, 0.0))], + (Parameter("test", 2, "s", 0.01, -10, 20), Parameter("name / test", 0.5, "m/s", 0.003125, -np.inf, np.inf), Parameter("test / name", 2, "s/m", 0.05, -np.inf, np.inf)), + (Parameter("test", 2, "s", 0.01, 0, 20), Parameter("name / test", 0.5, "m/s", 0.003125, 0.0, np.inf), Parameter("test / name", 2, "s/m", 0.05, 0.0, np.inf)), + (Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.inf, 0.0), Parameter("test / name", -2, "s/m", 0.05, -np.inf, 0.0))], ids=["crossing_zero", "only_positive", "only_negative"]) def test_division_with_parameter(self, parameter : Parameter, test, expected, expected_reverse): # When @@ -686,8 +691,8 @@ def test_division_with_parameter(self, parameter : Parameter, test, expected, ex assert result_reverse.max == expected_reverse.max @pytest.mark.parametrize("first, second, expected", [ - (Parameter("name", 1, "m", 0.01, -10, 20), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.Inf, np.Inf)), - (Parameter("name", -10, "m", 0.01, -20, -10), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", 5.0, "m/s", 0.065, 1, np.Inf)), + (Parameter("name", 1, "m", 0.01, -10, 20), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.inf, np.inf)), + (Parameter("name", -10, "m", 0.01, -20, -10), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", 5.0, "m/s", 0.065, 1, np.inf)), (Parameter("name", 10, "m", 0.01, 10, 20), Parameter("test", -20, "s", 0.01, -20, -10), Parameter("name / test", -0.5, "m/s", 3.125e-5, -2, -0.5))], ids=["first_crossing_zero_second_negative_0", "both_negative_second_negative_0", "finite_limits"]) def test_division_with_parameter_remaining_cases(self, first, second, expected): @@ -703,8 +708,8 @@ def test_division_with_parameter_remaining_cases(self, first, second, expected): assert result.max == expected.max @pytest.mark.parametrize("test, expected, expected_reverse", [ - (DescriptorNumber(name="test", value=2, variance=0.1, unit="s"), Parameter("name / test", 0.5, "m/s", 0.00875, 0, 5), Parameter("test / name", 2, "s/m", 0.14, 0.2, np.Inf)), - (2, Parameter("name / 2", 0.5, "m", 0.0025, 0, 5), Parameter("2 / name", 2, "m**-1", 0.04, 0.2, np.Inf))], + (DescriptorNumber(name="test", value=2, variance=0.1, unit="s"), Parameter("name / test", 0.5, "m/s", 0.00875, 0, 5), Parameter("test / name", 2, "s/m", 0.14, 0.2, np.inf)), + (2, Parameter("name / 2", 0.5, "m", 0.0025, 0, 5), Parameter("2 / name", 2, "m**-1", 0.04, 0.2, np.inf))], ids=["descriptor_number", "number"]) def test_division_with_descriptor_number_and_number(self, parameter : Parameter, test, expected, expected_reverse): # When @@ -750,10 +755,10 @@ def test_zero_value_divided_by_parameter(self, parameter : Parameter, test, expe assert result.variance == expected.variance @pytest.mark.parametrize("first, second, expected", [ - (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", 2, "s", 0.1, -10, 10), Parameter("name / test", 0.5, "m/s", 0.00875, -np.Inf, np.Inf)), - (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", 2, "s", 0.1, 0, 10), Parameter("name / test", -0.5, "m/s", 0.00875, -np.Inf, -0.1)), - (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", -0.5, "m/s", 0.00875, -np.Inf, -0.1)), - (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", 0.5, "m/s", 0.00875, 0.1, np.Inf)), + (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", 2, "s", 0.1, -10, 10), Parameter("name / test", 0.5, "m/s", 0.00875, -np.inf, np.inf)), + (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", 2, "s", 0.1, 0, 10), Parameter("name / test", -0.5, "m/s", 0.00875, -np.inf, -0.1)), + (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", -0.5, "m/s", 0.00875, -np.inf, -0.1)), + (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", 0.5, "m/s", 0.00875, 0.1, np.inf)), (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", 2, "s", 0.1, 1, 10), Parameter("name / test", 0.5, "m/s", 0.00875, 0.1, 1))], ids=["crossing_zero", "positive_0_with_negative", "negative_0_with_positive", "negative_0_with_negative", "finite_limits"]) def test_division_with_descriptor_number_missing_cases(self, first, second, expected): @@ -789,8 +794,8 @@ def test_divide_by_zero_value_parameter(self): @pytest.mark.parametrize("test, expected", [ (3, Parameter("name ** 3", 125, "m^3", 281.25, -125, 1000)), (2, Parameter("name ** 2", 25, "m^2", 5.0, 0, 100)), - (-1, Parameter("name ** -1", 0.2, "1/m", 8e-5, -np.Inf, np.Inf)), - (-2, Parameter("name ** -2", 0.04, "1/m^2", 1.28e-5, 0, np.Inf)), + (-1, Parameter("name ** -1", 0.2, "1/m", 8e-5, -np.inf, np.inf)), + (-2, Parameter("name ** -2", 0.04, "1/m^2", 1.28e-5, 0, np.inf)), (0, DescriptorNumber("name ** 0", 1, "dimensionless", 0)), (DescriptorNumber("test", 2), Parameter("name ** test", 25, "m^2", 5.0, 0, 100))], ids=["power_3", "power_2", "power_-1", "power_-2", "power_0", "power_descriptor_number"]) @@ -812,13 +817,13 @@ def test_power_of_parameter(self, test, expected): assert result.max == expected.max @pytest.mark.parametrize("test, exponent, expected", [ - (Parameter("name", 5, "m", 0.05, 0, 10), -1, Parameter("name ** -1", 0.2, "1/m", 8e-5, 0.1, np.Inf)), - (Parameter("name", -5, "m", 0.05, -5, 0), -1, Parameter("name ** -1", -0.2, "1/m", 8e-5, -np.Inf, -0.2)), + (Parameter("name", 5, "m", 0.05, 0, 10), -1, Parameter("name ** -1", 0.2, "1/m", 8e-5, 0.1, np.inf)), + (Parameter("name", -5, "m", 0.05, -5, 0), -1, Parameter("name ** -1", -0.2, "1/m", 8e-5, -np.inf, -0.2)), (Parameter("name", 5, "m", 0.05, 5, 10), -1, Parameter("name ** -1", 0.2, "1/m", 8e-5, 0.1, 0.2)), (Parameter("name", -5, "m", 0.05, -10, -5), -1, Parameter("name ** -1", -0.2, "1/m", 8e-5, -0.2, -0.1)), (Parameter("name", -5, "m", 0.05, -10, -5), -2, Parameter("name ** -2", 0.04, "1/m^2", 1.28e-5, 0.01, 0.04)), (Parameter("name", 5, "", 0.1, 1, 10), 0.3, Parameter("name ** 0.3", 1.6206565966927624, "", 0.0009455500095853564, 1, 1.9952623149688795)), - (Parameter("name", 5, "", 0.1), 0.5, Parameter("name ** 0.5", 2.23606797749979, "", 0.005, 0, np.Inf))], + (Parameter("name", 5, "", 0.1), 0.5, Parameter("name ** 0.5", 2.23606797749979, "", 0.005, 0, np.inf))], ids=["0_positive", "negative_0", "both_positive", "both_negative_invert", "both_negative_invert_square", "fractional", "fractional_negative_limit"]) def test_power_of_diffent_parameters(self, test, exponent, expected): # When Then From 2ea1bcc8447dbbc8262795beb75a9abea3bcb5dd Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 20 Jan 2025 13:47:25 +0100 Subject: [PATCH 02/48] Remove old parameter (#94) --- src/easyscience/Constraints.py | 44 +- src/easyscience/Objects/Groups.py | 18 +- src/easyscience/Objects/Inferface.py | 9 +- src/easyscience/Objects/ObjectClasses.py | 36 +- src/easyscience/Objects/Variable.py | 889 ------------------ .../{new_variable => variable}/__init__.py | 0 .../descriptor_base.py | 2 +- .../descriptor_bool.py | 0 .../descriptor_number.py | 35 +- .../descriptor_str.py | 0 .../{new_variable => variable}/parameter.py | 27 +- src/easyscience/Objects/virtual.py | 32 +- .../fitting/minimizers/minimizer_base.py | 24 +- .../fitting/minimizers/minimizer_bumps.py | 20 +- .../fitting/minimizers/minimizer_dfo.py | 32 +- .../fitting/minimizers/minimizer_lmfit.py | 14 +- src/easyscience/models/polynomial.py | 14 +- .../integration_tests/Fitting/test_fitter.py | 2 +- .../Fitting/test_fitter_legacy_parameter.py | 335 ------- .../Fitting/test_multi_fitter.py | 2 +- .../test_multi_fitter_legacy_parameter.py | 279 ------ .../Fitting/minimizers/test_minimizer_base.py | 8 +- .../minimizers/test_minimizer_bumps.py | 10 +- .../Fitting/minimizers/test_minimizer_dfo.py | 16 +- .../minimizers/test_minimizer_lmfit.py | 2 +- tests/unit_tests/Fitting/test_constraints.py | 2 +- .../test_constraints_legacy_parameter.py | 131 --- tests/unit_tests/Objects/test_BaseObj.py | 22 +- .../Objects/test_BaseObj_legacy_parameter.py | 503 ---------- .../Objects/test_Descriptor_Parameter.py | 579 ------------ tests/unit_tests/Objects/test_Groups.py | 7 +- tests/unit_tests/Objects/test_Virtual.py | 40 +- .../test_descriptor_base.py | 2 +- .../test_descriptor_bool.py | 2 +- .../test_descriptor_from_legacy.py | 7 +- .../test_descriptor_number.py | 16 +- .../test_descriptor_str.py | 2 +- .../test_parameter.py | 4 +- .../test_parameter_from_legacy.py | 8 +- .../global_object/test_global_object.py | 2 +- tests/unit_tests/global_object/test_map.py | 2 +- .../global_object/test_undo_redo.py | 221 +++-- tests/unit_tests/models/test_polynomial.py | 16 +- tests/unit_tests/utils/io_tests/test_core.py | 4 +- tests/unit_tests/utils/io_tests/test_dict.py | 7 +- tests/unit_tests/utils/io_tests/test_json.py | 2 +- tests/unit_tests/utils/io_tests/test_xml.py | 2 +- 47 files changed, 363 insertions(+), 3068 deletions(-) delete mode 100644 src/easyscience/Objects/Variable.py rename src/easyscience/Objects/{new_variable => variable}/__init__.py (100%) rename src/easyscience/Objects/{new_variable => variable}/descriptor_base.py (99%) rename src/easyscience/Objects/{new_variable => variable}/descriptor_bool.py (100%) rename src/easyscience/Objects/{new_variable => variable}/descriptor_number.py (94%) rename src/easyscience/Objects/{new_variable => variable}/descriptor_str.py (100%) rename src/easyscience/Objects/{new_variable => variable}/parameter.py (96%) delete mode 100644 tests/integration_tests/Fitting/test_fitter_legacy_parameter.py delete mode 100644 tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py delete mode 100644 tests/unit_tests/Fitting/test_constraints_legacy_parameter.py delete mode 100644 tests/unit_tests/Objects/test_BaseObj_legacy_parameter.py delete mode 100644 tests/unit_tests/Objects/test_Descriptor_Parameter.py rename tests/unit_tests/Objects/{new_variable => variable}/test_descriptor_base.py (98%) rename tests/unit_tests/Objects/{new_variable => variable}/test_descriptor_bool.py (97%) rename tests/unit_tests/Objects/{new_variable => variable}/test_descriptor_from_legacy.py (97%) rename tests/unit_tests/Objects/{new_variable => variable}/test_descriptor_number.py (97%) rename tests/unit_tests/Objects/{new_variable => variable}/test_descriptor_str.py (97%) rename tests/unit_tests/Objects/{new_variable => variable}/test_parameter.py (99%) rename tests/unit_tests/Objects/{new_variable => variable}/test_parameter_from_legacy.py (98%) diff --git a/src/easyscience/Constraints.py b/src/easyscience/Constraints.py index 69644085..9628db9e 100644 --- a/src/easyscience/Constraints.py +++ b/src/easyscience/Constraints.py @@ -194,13 +194,9 @@ def __init__(self, dependent_obj: V, operator: str, value: Number): super(NumericConstraint, self).__init__(dependent_obj, operator=operator, value=value) def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - ## TODO clean when full move to new_variable - import easyscience.Objects.new_variable.parameter + ## TODO Probably needs to be updated when DescriptorArray is implemented - if isinstance(obj, easyscience.Objects.new_variable.parameter.Parameter): - value = obj.value_no_call_back - else: - value = obj.raw_value + value = obj.value_no_call_back if isinstance(value, list): value = np.array(value) @@ -258,13 +254,7 @@ def __init__(self, dependent_obj: V, operator: str, value: str): super(SelfConstraint, self).__init__(dependent_obj, operator=operator, value=value) def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - ## TODO clean when full move to new_variable - import easyscience.Objects.new_variable.parameter - - if isinstance(obj, easyscience.Objects.new_variable.parameter.Parameter): - value = obj.value_no_call_back - else: - value = obj.raw_value + value = obj.value_no_call_back self.aeval.symtable['value1'] = value self.aeval.symtable['value2'] = getattr(obj, self.value) @@ -322,13 +312,7 @@ def __init__(self, dependent_obj: V, operator: str, independent_obj: V): self.external = True def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - ## TODO clean when full move to new_variable - import easyscience.Objects.new_variable.parameter - - if isinstance(obj, easyscience.Objects.new_variable.parameter.Parameter): - value = obj.value_no_call_back - else: - value = obj.raw_value + value = obj.value_no_call_back self.aeval.symtable['value1'] = value try: @@ -417,16 +401,11 @@ def __init__( self.external = True def _parse_operator(self, independent_objs: List[V], *args, **kwargs) -> Number: - import easyscience.Objects.new_variable.parameter in_str = '' value = None for idx, obj in enumerate(independent_objs): - ## TODO clean when full move to new_variable - if isinstance(obj, easyscience.Objects.new_variable.parameter.Parameter): - self.aeval.symtable['p' + str(self.independent_obj_ids[idx])] = obj.value_no_call_back - else: - self.aeval.symtable['p' + str(self.independent_obj_ids[idx])] = obj.raw_value + 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): @@ -485,25 +464,16 @@ def __init__( self.external = True def _parse_operator(self, obj: V, *args, **kwargs) -> Number: - import easyscience.Objects.new_variable.parameter 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: - ## TODO clean when full move to new_variable - if isinstance(o, easyscience.Objects.new_variable.parameter.Parameter): - value_str += f'{o.value_no_call_back},' - else: - value_str += f'{o.raw_value},' + value_str += f'{o.value_no_call_back},' value_str = value_str[:-1] else: - ## TODO clean when full move to new_variable - if isinstance(obj, easyscience.Objects.new_variable.parameter.Parameter): - value_str += f'{obj.value_no_call_back}' - else: - value_str += f'{obj.raw_value}' + value_str += f'{obj.value_no_call_back}' value_str += ')' try: diff --git a/src/easyscience/Objects/Groups.py b/src/easyscience/Objects/Groups.py index cbbecb55..90d4f0c6 100644 --- a/src/easyscience/Objects/Groups.py +++ b/src/easyscience/Objects/Groups.py @@ -15,17 +15,17 @@ 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.Objects.new_variable.descriptor_base import DescriptorBase from easyscience.Objects.ObjectClasses import BasedBase -from easyscience.Objects.ObjectClasses import Descriptor +from easyscience.Objects.variable.descriptor_base import DescriptorBase if TYPE_CHECKING: from easyscience.Objects.Inferface import iF from easyscience.Objects.ObjectClasses import B - from easyscience.Objects.Variable import V + V = TypeVar('V', bound=DescriptorBase) class BaseCollection(BasedBase, MutableSequence): @@ -69,7 +69,7 @@ def __init__( _kwargs[key] = item kwargs = _kwargs for item in list(kwargs.values()) + _args: - if not issubclass(type(item), (Descriptor, DescriptorBase, BasedBase)): + if not issubclass(type(item), (DescriptorBase, BasedBase)): raise AttributeError('A collection can only be formed from easyscience objects.') args = _args _kwargs = {} @@ -102,12 +102,12 @@ def insert(self, index: int, value: Union[V, B]) -> None: :param index: Index for EasyScience object to be inserted. :type index: int :param value: Object to be inserted. - :type value: Union[BasedBase, Descriptor] + :type value: Union[BasedBase, DescriptorBase] :return: None :rtype: None """ t_ = type(value) - if issubclass(t_, (BasedBase, Descriptor, DescriptorBase)): + if issubclass(t_, (BasedBase, DescriptorBase)): update_key = list(self._kwargs.keys()) values = list(self._kwargs.values()) # Update the internal dict @@ -124,7 +124,7 @@ def insert(self, index: int, value: Union[V, B]) -> None: def __getitem__(self, idx: Union[int, slice]) -> Union[V, B]: """ - Get an item in the collection based on it's index. + Get an item in the collection based on its index. :param idx: index or slice of the collection. :type idx: Union[int, slice] @@ -168,7 +168,7 @@ def __setitem__(self, key: int, value: Union[B, V]) -> None: if isinstance(value, Number): # noqa: S3827 item = self.__getitem__(key) item.value = value - elif issubclass(type(value), (BasedBase, Descriptor, DescriptorBase)): + elif issubclass(type(value), (BasedBase, DescriptorBase)): update_key = list(self._kwargs.keys()) values = list(self._kwargs.values()) old_item = values[key] @@ -242,7 +242,7 @@ def sort(self, mapping: Callable[[Union[B, V]], Any], reverse: bool = False) -> """ Sort the collection according to the given mapping. - :param mapping: mapping function to sort the collection. i.e. lambda parameter: parameter.raw_value + :param mapping: mapping function to sort the collection. i.e. lambda parameter: parameter.value :type mapping: Callable :param reverse: Reverse the sorting. :type reverse: bool diff --git a/src/easyscience/Objects/Inferface.py b/src/easyscience/Objects/Inferface.py index c3e7a7c9..cb37b27e 100644 --- a/src/easyscience/Objects/Inferface.py +++ b/src/easyscience/Objects/Inferface.py @@ -152,7 +152,6 @@ def generate_bindings(self, model, *args, ifun=None, **kwargs): :return: binding property :rtype: property """ - import easyscience.Objects.new_variable.parameter class_links = self.__interface_obj.create(model) props = model._get_linkable_attributes() @@ -164,12 +163,8 @@ def generate_bindings(self, model, *args, ifun=None, **kwargs): idx = props_names.index(item_key) prop = props[idx] - ## TODO clean when full move to new_variable - if isinstance(prop, easyscience.Objects.new_variable.parameter.Parameter): - # Should be fetched this way to ensure we don't get value from callback - prop_value = prop.value_no_call_back - else: - prop_value = prop.raw_value + # Should be fetched this way to ensure we don't get value from callback + prop_value = prop.value_no_call_back prop._callback = item.make_prop(item_key) prop._callback.fset(prop_value) diff --git a/src/easyscience/Objects/ObjectClasses.py b/src/easyscience/Objects/ObjectClasses.py index 4956c72c..e6a159ad 100644 --- a/src/easyscience/Objects/ObjectClasses.py +++ b/src/easyscience/Objects/ObjectClasses.py @@ -16,21 +16,18 @@ from typing import Optional from typing import Set from typing import TypeVar -from typing import Union from easyscience import global_object from easyscience.Utils.classTools import addLoggedProp from .core import ComponentSerializer -from .new_variable import Parameter as NewParameter -from .new_variable.descriptor_base import DescriptorBase -from .Variable import Descriptor -from .Variable import Parameter +from .variable import Parameter +from .variable.descriptor_base import DescriptorBase if TYPE_CHECKING: from easyscience.Constraints import C from easyscience.Objects.Inferface import iF - from easyscience.Objects.Variable import V + V = TypeVar('V', bound=DescriptorBase) class BasedBase(ComponentSerializer): @@ -160,8 +157,7 @@ def constraints(self) -> List[C]: constraints.append(con[key]) return constraints - ## TODO clean when full move to new_variable - def get_parameters(self) -> Union[List[Parameter], List[NewParameter]]: + def get_parameters(self) -> List[Parameter]: """ Get all parameter objects as a list. @@ -171,11 +167,10 @@ def get_parameters(self) -> Union[List[Parameter], List[NewParameter]]: for key, item in self._kwargs.items(): if hasattr(item, 'get_parameters'): par_list = [*par_list, *item.get_parameters()] - elif isinstance(item, Parameter) or isinstance(item, NewParameter): + elif isinstance(item, Parameter): par_list.append(item) return par_list - ## TODO clean when full move to new_variable def _get_linkable_attributes(self) -> List[V]: """ Get all objects which can be linked against as a list. @@ -186,12 +181,11 @@ def _get_linkable_attributes(self) -> List[V]: for key, item in self._kwargs.items(): if hasattr(item, '_get_linkable_attributes'): item_list = [*item_list, *item._get_linkable_attributes()] - elif issubclass(type(item), (Descriptor, DescriptorBase)): + elif issubclass(type(item), (DescriptorBase)): item_list.append(item) return item_list - ## TODO clean when full move to new_variable - def get_fit_parameters(self) -> Union[List[Parameter], List[NewParameter]]: + def get_fit_parameters(self) -> List[Parameter]: """ Get all objects which can be fitted (and are not fixed) as a list. @@ -201,7 +195,7 @@ def get_fit_parameters(self) -> Union[List[Parameter], List[NewParameter]]: for key, item in self._kwargs.items(): if hasattr(item, 'get_fit_parameters'): fit_list = [*fit_list, *item.get_fit_parameters()] - elif isinstance(item, Parameter) or isinstance(item, NewParameter): + elif isinstance(item, Parameter): if item.enabled and not item.fixed: fit_list.append(item) return fit_list @@ -235,7 +229,6 @@ class BaseObj(BasedBase): cheat with `BaseObj(*[Descriptor(...), Parameter(...), ...])`. """ - ## TODO clean when full move to new_variable def __init__( self, name: str, @@ -253,7 +246,7 @@ def __init__( 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, Descriptor, DescriptorBase)): + if issubclass(type(arg), (BaseObj, DescriptorBase)): kwargs[getattr(arg, 'name')] = arg # Set kwargs, also useful for serialization known_keys = self.__dict__.keys() @@ -261,7 +254,7 @@ def __init__( 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, Descriptor, DescriptorBase)) or 'BaseCollection' in [ + 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]) @@ -310,7 +303,6 @@ def __init__(self, foo: Parameter, bar: Parameter): test_class=BaseObj, ) - ## TODO clean when full move to new_variable def __setattr__(self, key: str, value: BV) -> None: # Assume that the annotation is a ClassVar old_obj = None @@ -323,12 +315,12 @@ def __setattr__(self, key: str, value: BV) -> None: self.__class__.__annotations__[key].__args__, ) ): - if issubclass(type(getattr(self, key, None)), (BasedBase, Descriptor, DescriptorBase)): + 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, Descriptor, DescriptorBase)): + 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) @@ -352,8 +344,8 @@ def getter(obj: BV) -> BV: @staticmethod def __setter(key: str) -> Callable[[BV], None]: def setter(obj: BV, value: float) -> None: - if issubclass(obj._kwargs[key].__class__, (Descriptor, DescriptorBase)) and not issubclass( - value.__class__, (Descriptor, DescriptorBase) + if issubclass(obj._kwargs[key].__class__, (DescriptorBase)) and not issubclass( + value.__class__, (DescriptorBase) ): obj._kwargs[key].value = value else: diff --git a/src/easyscience/Objects/Variable.py b/src/easyscience/Objects/Variable.py deleted file mode 100644 index 1f9bf555..00000000 --- a/src/easyscience/Objects/Variable.py +++ /dev/null @@ -1,889 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Set[str]: - base_cls = getattr(self, '__old_class__', self.__class__) - mro = base_cls.__mro__ - idx = mro.index(ComponentSerializer) - names = set() - for i in range(idx): - cls = mro[i] - if hasattr(cls, '_CORE'): - spec = getfullargspec(cls.__init__) - names = names.union(set(spec.args[1:])) - return names - - def __reduce__(self): - """ - Make the class picklable. Due to the nature of the dynamic class definitions special measures need to be taken. - - :return: Tuple consisting of how to make the object - :rtype: tuple - """ - state = self.encode() - cls = self.__class__ - if hasattr(self, '__old_class__'): - cls = self.__old_class__ - return cls.from_dict, (state,) - - @property - def unique_name(self) -> str: - """ - Get the unique name of this object. - - :return: Unique name of this object - """ - return self._unique_name - - @unique_name.setter - def unique_name(self, new_unique_name: str): - """Set a new unique name for the object. The old name is still kept in the map. - - :param new_unique_name: New unique name for the object""" - if not isinstance(new_unique_name, str): - raise TypeError('Unique name has to be a string.') - self._unique_name = new_unique_name - self._global_object.map.add_vertex(self) - - @property - def display_name(self) -> str: - """ - Get a pretty display name. - - :return: The pretty display name. - """ - # TODO This might be better implementing fancy f-strings where we can return html,latex, markdown etc - display_name = self._display_name - if display_name is None: - display_name = self.name - return display_name - - @display_name.setter - @property_stack_deco - def display_name(self, name_str: str): - """ - Set the pretty display name. - - :param name_str: Pretty display name of the object. - :return: None - """ - self._display_name = name_str - - @property - def unit(self) -> pint.UnitRegistry: - """ - Get the unit associated with the object. - - :return: Unit associated with self in `pint` form. - """ - return self._units.units - - @unit.setter - @property_stack_deco - def unit(self, unit_str: str): - """ - Set the unit to a new one. - - :param unit_str: String representation of the unit required. i.e `m/s` - :return: None - """ - if not isinstance(unit_str, str): - unit_str = str(unit_str) - new_unit = ureg.parse_expression(unit_str) - self._units = new_unit - self._args['units'] = str(new_unit) - self._value = self.__class__._constructor(**self._args) - - @property - def value(self) -> Any: - """ - Get the value of self as a pint. This is should be usable for most cases. If a pint - is not acceptable then the raw value can be obtained through `obj.raw_value`. - - :return: Value of self with unit. - """ - # Cached property? Should reference callback. - # Also should reference for undo/redo - if self._callback.fget is not None: - try: - value = self._callback.fget() - if hasattr(self._value, 'magnitude'): - if value != self._value.magnitude: - self.__deepValueSetter(value) - elif value != self._value: - self.__deepValueSetter(value) - - except Exception as e: - raise ValueError(f'Unable to return value:\n{e}') - r_value = self._value - if self.__isBooleanValue: - r_value = bool(r_value) - return r_value - - def __deepValueSetter(self, value: Any): - """ - Set the value of self. This creates a pint with a unit. - - :param value: New value of self - :return: None - """ - # TODO there should be a callback to the collective, logging this as a return(if from a non `EasyScience` class) - if hasattr(value, 'magnitude'): - value = value.magnitude - if hasattr(value, 'nominal_value'): - value = value.nominal_value - self._type = type(value) - self.__isBooleanValue = isinstance(value, bool) - if self.__isBooleanValue: - value = int(value) - self._args['value'] = value - self._value = self.__class__._constructor(**self._args) - - @value.setter - @property_stack_deco - def value(self, value: Any): - """ - Set the value of self. This creates a pint with a unit. - - :param value: New value of self - :return: None - """ - self.__deepValueSetter(value) - if self._callback.fset is not None: - try: - self._callback.fset(value) - except Exception as e: - raise CoreSetException(e) - - @property - def raw_value(self) -> Any: - """ - Return the raw value of self without a unit. - - :return: The raw value of self - """ - value = self._value - if hasattr(value, 'magnitude'): - value = value.magnitude - if hasattr(value, 'nominal_value'): - value = value.nominal_value - if self.__isBooleanValue: - value = bool(value) - return value - - @property - def enabled(self) -> bool: - """ - Logical property to see if the objects value can be directly set. - - :return: Can the objects value be set - """ - return self._enabled - - @enabled.setter - @property_stack_deco - def enabled(self, value: bool): - """ - 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 - - def convert_unit(self, unit_str: str): - """ - Convert the value from one unit system to another. You will should use - `compatible_units` to see if your new unit is compatible. - - :param unit_str: New unit in string form - """ - new_unit = ureg.parse_expression(unit_str) - self._value = self._value.to(new_unit) - self._units = new_unit - self._args['value'] = self.raw_value - self._args['units'] = str(self.unit) - - # @cached_property - @property - def compatible_units(self) -> List[str]: - """ - Returns all possible units for which the current unit can be converted. - - :return: Possible conversion units - """ - return [str(u) for u in self.unit.compatible_units()] - - def __repr__(self): - """Return printable representation of a Descriptor/Parameter object.""" - class_name = self.__class__.__name__ - obj_name = self.name - if self.__isBooleanValue: - obj_value = self.raw_value - else: - obj_value = self._value.magnitude - if isinstance(obj_value, float): - obj_value = '{:0.04f}'.format(obj_value) - obj_units = '' - if not self.unit.dimensionless: - obj_units = ' {:~P}'.format(self.unit) - out_str = f"<{class_name} '{obj_name}': {obj_value}{obj_units}>" - return out_str - - def to_obj_type(self, data_type: Type[Parameter], *kwargs): - """ - Convert between a `Parameter` and a `Descriptor`. - - :param data_type: class constructor of what we want to be - :param kwargs: Additional keyword/value pairs for conversion - :return: self as a new type - """ - pickled_obj = self.encode() - pickled_obj.update(kwargs) - if '@class' in pickled_obj.keys(): - pickled_obj['@class'] = data_type.__name__ - return data_type.from_dict(pickled_obj) - - def __copy__(self): - return self.__class__.from_dict(self.as_dict()) - - -V = TypeVar('V', bound=Descriptor) - - -class ComboDescriptor(Descriptor): - """ - This class is an extension of a ``EasyScience.Object.Base.Descriptor``. This class has a selection of values which can - be checked against. For example, combo box styling. - """ - - def __init__(self, *args, available_options: list = None, **kwargs): - super(ComboDescriptor, self).__init__(*args, **kwargs) - if available_options is None: - available_options = [] - self._available_options = available_options - - # We have initialized from the Descriptor class where value has it's own undo/redo decorator - # This needs to be bypassed to use the Parameter undo/redo stack - fun = self.__class__.value.fset - if hasattr(fun, 'func'): - fun = getattr(fun, 'func') - self.__previous_set: Callable[[V, Union[numbers.Number, np.ndarray]], Union[numbers.Number, np.ndarray]] = fun - - # Monkey patch the unit and the value to take into account the new max/min situation - addProp( - self, - 'value', - fget=self.__class__.value.fget, - fset=self.__class__._property_value.fset, - fdel=self.__class__.value.fdel, - ) - - @property - def _property_value(self) -> Union[numbers.Number, np.ndarray]: - return self.value - - @_property_value.setter - @property_stack_deco - def _property_value(self, set_value: Union[numbers.Number, np.ndarray, Q_]): - """ - Verify value against constraints. This hasn't really been implemented as fitting is tricky. - - :param set_value: value to be verified - :return: new value from constraint - """ - if isinstance(set_value, Q_): - set_value = set_value.magnitude - # Save the old state and create the new state - old_value = self._value - state = self._global_object.stack.enabled - if state: - self._global_object.stack.force_state(False) - try: - new_value = old_value - if set_value in self.available_options: - new_value = set_value - finally: - self._global_object.stack.force_state(state) - - # Restore to the old state - self.__previous_set(self, new_value) - - @property - def available_options(self) -> List[Union[numbers.Number, np.ndarray, Q_]]: - return self._available_options - - @available_options.setter - @property_stack_deco - def available_options(self, available_options: List[Union[numbers.Number, np.ndarray, Q_]]) -> None: - self._available_options = available_options - - def as_dict(self, **kwargs) -> Dict[str, Any]: - import json - - d = super().as_dict(**kwargs) - d['name'] = self.name - d['available_options'] = json.dumps(self.available_options) - return d - - -class Parameter(Descriptor): - """ - This class is an extension of a ``EasyScience.Object.Base.Descriptor``. Where the descriptor was for static objects, - a `Parameter` is for dynamic objects. A parameter has the ability to be used in fitting and - has additional fields to facilitate this. - """ - - _constructor = M_ - - def __init__( - self, - name: str, - value: Union[numbers.Number, np.ndarray], - error: Optional[Union[numbers.Number, np.ndarray]] = 0.0, - min: Optional[numbers.Number] = -np.inf, - max: Optional[numbers.Number] = np.inf, - fixed: Optional[bool] = False, - **kwargs, - ): - """ - This class is an extension of a ``EasyScience.Object.Base.Descriptor``. Where the descriptor was for static - objects, a `Parameter` is for dynamic objects. A parameter has the ability to be used in fitting and has - additional fields to facilitate this. - - :param name: Name of this obj - :param value: Value of this object - :param error: Error associated as sigma for this parameter - :param min: Minimum value for fitting - :param max: Maximum value for fitting - :param fixed: Should this parameter vary when fitting? - :param kwargs: Key word arguments for the `Descriptor` class. - - .. code-block:: python - - from easyscience.Objects.Base import Parameter - # Describe a phase - phase_basic = Parameter('phase', 3) - # Describe a phase with a unit - phase_unit = Parameter('phase', 3, units,='rad/s') - - .. note:: - Undo/Redo functionality is implemented for the attributes `value`, `error`, `min`, `max`, `fixed` - """ - # Set the error - self._args = {'value': value, 'units': '', 'error': error} - - if not isinstance(value, numbers.Number) or isinstance(value, np.ndarray): - raise ValueError('In a parameter the `value` must be numeric') - if value < min: - raise ValueError('`value` can not be less than `min`') - if value > max: - raise ValueError('`value` can not be greater than `max`') - if error < 0: - raise ValueError('Standard deviation `error` must be positive') - - super().__init__(name=name, value=value, **kwargs) - self._args['units'] = str(self.unit) - - # Warnings if we are given a boolean - if isinstance(self._type, bool): - warnings.warn( - 'Boolean values are not officially supported in Parameter. Use a Descriptor instead', - UserWarning, - ) - - # Create additional fitting elements - self._min: numbers.Number = min - self._max: numbers.Number = max - self._fixed: bool = fixed - self.initial_value = self.value - self._constraints: dict = { - 'user': {}, - 'builtin': { - 'min': SelfConstraint(self, '>=', '_min'), - 'max': SelfConstraint(self, '<=', '_max'), - }, - 'virtual': {}, - } - # This is for the serialization. Otherwise we wouldn't catch the values given to `super()` - self._kwargs = kwargs - - # We have initialized from the Descriptor class where value has it's own undo/redo decorator - # This needs to be bypassed to use the Parameter undo/redo stack - fun = self.__class__.value.fset - if hasattr(fun, 'func'): - fun = getattr(fun, 'func') - self.__previous_set: Callable[ - [V, Union[numbers.Number, np.ndarray]], - Union[numbers.Number, np.ndarray], - ] = fun - - # Monkey patch the unit and the value to take into account the new max/min situation - addProp( - self, - 'value', - fget=self.__class__.value.fget, - fset=self.__class__._property_value.fset, - fdel=self.__class__.value.fdel, - ) - - @property - def _property_value(self) -> Union[numbers.Number, np.ndarray, M_]: - return self.value - - @_property_value.setter - @property_stack_deco - def _property_value(self, set_value: Union[numbers.Number, np.ndarray, M_]) -> None: - """ - Verify value against constraints. This hasn't really been implemented as fitting is tricky. - - :param set_value: value to be verified - :return: new value from constraint - """ - if isinstance(set_value, M_): - set_value = set_value.magnitude.nominal_value - # Save the old state and create the new state - old_value = self._value - self._value = self.__class__._constructor(value=set_value, units=self._args['units'], error=self._args['error']) - - # First run the built in constraints. i.e. min/max - constraint_type: MappingProxyType[str, C] = self.builtin_constraints - new_value = self.__constraint_runner(constraint_type, set_value) - # Then run any user constraints. - constraint_type: dict = self.user_constraints - state = self._global_object.stack.enabled - if state: - self._global_object.stack.force_state(False) - try: - new_value = self.__constraint_runner(constraint_type, new_value) - finally: - self._global_object.stack.force_state(state) - - # And finally update any virtual constraints - constraint_type: dict = self._constraints['virtual'] - _ = self.__constraint_runner(constraint_type, new_value) - - # Restore to the old state - self._value = old_value - self.__previous_set(self, new_value) - - def convert_unit(self, new_unit: str): # noqa: S1144 - """ - Perform unit conversion. The value, max and min can change on unit change. - - :param new_unit: new unit - :return: None - """ - old_unit = str(self._args['units']) - super().convert_unit(new_unit) - # Deal with min/max. Error is auto corrected - if not self.value.unitless and old_unit != 'dimensionless': - self._min = Q_(self.min, old_unit).to(self._units).magnitude - self._max = Q_(self.max, old_unit).to(self._units).magnitude - # Log the new converted error - self._args['error'] = self.value.error.magnitude - - @property - def min(self) -> numbers.Number: - """ - Get the minimum value for fitting. - - :return: minimum value - """ - return self._min - - @min.setter - @property_stack_deco - def min(self, value: numbers.Number): - """ - Set the minimum value for fitting. - - implements undo/redo functionality. - - :param value: new minimum value - :return: None - """ - if value <= self.raw_value: - self._min = value - else: - raise ValueError(f'The current set value ({self.raw_value}) is less than the desired min value ({value}).') - - @property - def max(self) -> numbers.Number: - """ - Get the maximum value for fitting. - - :return: maximum value - """ - return self._max - - @max.setter - @property_stack_deco - def max(self, value: numbers.Number): - """ - Get the maximum value for fitting. - - implements undo/redo functionality. - - :param value: new maximum value - :return: None - """ - if value >= self.raw_value: - self._max = value - else: - raise ValueError(f'The current set value ({self.raw_value}) is greater than the desired max value ({value}).') - - @property - def fixed(self) -> bool: - """ - Can the parameter vary while fitting? - - :return: True = fixed, False = can vary - :rtype: bool - """ - return self._fixed - - @fixed.setter - @property_stack_deco - def fixed(self, value: bool): - """ - Change the parameter vary while fitting state. - - implements undo/redo functionality. - - :param value: True = fixed, False = can vary - :return: None - """ - if not self.enabled: - if self._global_object.stack.enabled: - self._global_object.stack.pop() - if global_object.debug: - raise CoreSetException(f'{str(self)} is not enabled.') - return - # TODO Should we try and cast value to bool rather than throw ValueError? - if not isinstance(value, bool): - raise ValueError - self._fixed = value - - @property - def free(self) -> bool: - return not self.fixed - - @free.setter - def free(self, value: bool) -> None: - self.fixed = not value - - @property - def error(self) -> float: - """ - The error associated with the parameter. - - :return: Error associated with parameter - """ - return float(self._value.error.magnitude) - - @error.setter - @property_stack_deco - def error(self, value: float): - """ - Set the error associated with the parameter. - - implements undo/redo functionality. - - :param value: New error value - :return: None - """ - if value < 0: - raise ValueError - self._args['error'] = value - self._value = self.__class__._constructor(**self._args) - - def __repr__(self) -> str: - """ - Return printable representation of a Parameter object. - """ - super_str = super().__repr__() - super_str = super_str[:-1] - s = [] - if self.fixed: - super_str += ' (fixed)' - s.append(super_str) - s.append('bounds=[%s:%s]' % (repr(self.min), repr(self.max))) - return '%s>' % ', '.join(s) - - def __float__(self) -> float: - return float(self.raw_value) - - @property - def builtin_constraints(self) -> MappingProxyType[str, C]: - """ - 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, C]: - """ - 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, C]) -> None: - self._constraints['user'] = constraints_dict - - def _quick_set( - self, - set_value: float, - run_builtin_constraints: bool = False, - run_user_constraints: bool = False, - run_virtual_constraints: bool = False, - ) -> None: - """ - This is a quick setter for the parameter. It bypasses all the checks and constraints, - just setting the value and issuing the interface callbacks. - - WARNING: This is a dangerous function and should only be used when you know what you are doing. - """ - # First run the built-in constraints. i.e. min/max - if run_builtin_constraints: - constraint_type: MappingProxyType = self.builtin_constraints - set_value = self.__constraint_runner(constraint_type, set_value) - # Then run any user constraints. - if run_user_constraints: - constraint_type: dict = self.user_constraints - state = self._global_object.stack.enabled - if state: - self._global_object.stack.force_state(False) - try: - set_value = self.__constraint_runner(constraint_type, set_value) - finally: - self._global_object.stack.force_state(state) - if run_virtual_constraints: - # And finally update any virtual constraints - constraint_type: dict = self._constraints['virtual'] - _ = self.__constraint_runner(constraint_type, set_value) - - # Finally set the value - self._property_value._magnitude._nominal_value = set_value - self._args['value'] = set_value - if self._callback.fset is not None: - self._callback.fset(set_value) - - def __constraint_runner( - self, - this_constraint_type: Union[dict, MappingProxyType[str, C]], - newer_value: numbers.Number, - ) -> float: - for constraint in this_constraint_type.values(): - if constraint.external: - constraint() - continue - this_new_value = constraint(no_set=True) - if this_new_value != newer_value: - if global_object.debug: - print(f'Constraint `{constraint}` has been applied') - self._value = self.__class__._constructor( - value=this_new_value, - units=self._args['units'], - error=self._args['error'], - ) - newer_value = this_new_value - return newer_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: Union[Tuple[numbers.Number, numbers.Number], numbers.Number]) -> None: - """ - Set the bounds of the parameter. *This will also enable the parameter*. - - :param new_bound: New bounds. This can be a tuple of (min, max) or a single number (min). - For changing the max use (None, max_value). - """ - # Macro checking and opening for undo/redo - close_macro = False - if self._global_object.stack.enabled: - self._global_object.stack.beginMacro('Setting bounds') - close_macro = True - # Have we only been given a single number (MIN)? - if isinstance(new_bound, numbers.Number): - self.min = new_bound - # Have we been given a tuple? - if isinstance(new_bound, tuple): - new_min, new_max = new_bound - # Are there any None values? - if isinstance(new_min, numbers.Number): - self.min = new_min - if isinstance(new_max, numbers.Number): - self.max = new_max - # Enable the parameter if needed - if not self.enabled: - self.enabled = True - # This parameter is now not fixed. - self.fixed = False - # Close the macro if we opened it - if close_macro: - self._global_object.stack.endMacro() diff --git a/src/easyscience/Objects/new_variable/__init__.py b/src/easyscience/Objects/variable/__init__.py similarity index 100% rename from src/easyscience/Objects/new_variable/__init__.py rename to src/easyscience/Objects/variable/__init__.py diff --git a/src/easyscience/Objects/new_variable/descriptor_base.py b/src/easyscience/Objects/variable/descriptor_base.py similarity index 99% rename from src/easyscience/Objects/new_variable/descriptor_base.py rename to src/easyscience/Objects/variable/descriptor_base.py index 9f144f8f..b525d4f1 100644 --- a/src/easyscience/Objects/new_variable/descriptor_base.py +++ b/src/easyscience/Objects/variable/descriptor_base.py @@ -23,7 +23,7 @@ class DescriptorBase(ComponentSerializer, metaclass=abc.ABCMeta): A `Descriptor` is typically something which describes part of a model and is non-fittable and generally changes the state of an object. """ - + _global_object = global_object # Used by serializer _REDIRECT = {'parent': None} diff --git a/src/easyscience/Objects/new_variable/descriptor_bool.py b/src/easyscience/Objects/variable/descriptor_bool.py similarity index 100% rename from src/easyscience/Objects/new_variable/descriptor_bool.py rename to src/easyscience/Objects/variable/descriptor_bool.py diff --git a/src/easyscience/Objects/new_variable/descriptor_number.py b/src/easyscience/Objects/variable/descriptor_number.py similarity index 94% rename from src/easyscience/Objects/new_variable/descriptor_number.py rename to src/easyscience/Objects/variable/descriptor_number.py index c21c52e2..91f71549 100644 --- a/src/easyscience/Objects/new_variable/descriptor_number.py +++ b/src/easyscience/Objects/variable/descriptor_number.py @@ -12,6 +12,7 @@ from scipp import UnitError from scipp import Variable +from easyscience.global_object.undo_redo import PropertyStack from easyscience.global_object.undo_redo import property_stack_deco from .descriptor_base import DescriptorBase @@ -44,8 +45,7 @@ def __init__( 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` and `value`. + .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ if not isinstance(value, numbers.Number) or isinstance(value, bool): raise TypeError(f'{value=} must be a number') @@ -176,6 +176,8 @@ def error(self) -> float: :return: Error associated with parameter """ + if self._scalar.variance is None: + return None return float(np.sqrt(self._scalar.variance)) @error.setter @@ -192,9 +194,11 @@ def error(self, value: float) -> None: if value < 0: raise ValueError(f'{value=} must be positive') value = float(value) - self._scalar.variance = value**2 + self._scalar.variance = value**2 + else: + self._scalar.variance = None - def convert_unit(self, unit_str: str): + def convert_unit(self, unit_str: str) -> None: """ Convert the value from one unit system to another. @@ -206,7 +210,28 @@ def convert_unit(self, unit_str: str): new_unit = sc.Unit(unit_str) except UnitError as message: raise UnitError(message) from None - self._scalar = self._scalar.to(unit=new_unit) + + # Save the current state for undo/redo + old_scalar = self._scalar + + # Perform the unit conversion + try: + new_scalar = self._scalar.to(unit=new_unit) + except Exception as e: + raise UnitError(f"Failed to convert unit: {e}") from e + + # Define the setter function for the undo stack + def set_scalar(obj, scalar): + obj._scalar = scalar + + # Push to undo stack + self._global_object.stack.push( + PropertyStack(self, set_scalar, old_scalar, new_scalar, text=f"Convert unit to {unit_str}") + ) + + # Update the scalar + self._scalar = new_scalar + # Just to get return type right def __copy__(self) -> DescriptorNumber: diff --git a/src/easyscience/Objects/new_variable/descriptor_str.py b/src/easyscience/Objects/variable/descriptor_str.py similarity index 100% rename from src/easyscience/Objects/new_variable/descriptor_str.py rename to src/easyscience/Objects/variable/descriptor_str.py diff --git a/src/easyscience/Objects/new_variable/parameter.py b/src/easyscience/Objects/variable/parameter.py similarity index 96% rename from src/easyscience/Objects/new_variable/parameter.py rename to src/easyscience/Objects/variable/parameter.py index c2c21793..03b3c230 100644 --- a/src/easyscience/Objects/new_variable/parameter.py +++ b/src/easyscience/Objects/variable/parameter.py @@ -76,12 +76,14 @@ def __init__( :param parent: The object which is the parent to this one .. note:: - Undo/Redo functionality is implemented for the attributes `value`, `error`, `min`, `max`, `fixed` - """ + 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') + 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: @@ -92,6 +94,7 @@ def __init__( if not isinstance(fixed, bool): raise TypeError('`fixed` must be either True or False') + 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) @@ -112,7 +115,6 @@ def __init__( weakref.finalize(self, self._callback.fdel) # Create additional fitting elements - self._fixed = fixed self._enabled = enabled self._initial_scalar = copy.deepcopy(self._scalar) builtin_constraint = { @@ -317,7 +319,6 @@ 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: """ @@ -329,13 +330,23 @@ def bounds(self, new_bound: Tuple[numbers.Number, numbers.Number]) -> None: 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 - raise ValueError(f'Current paramter value: {self._scalar.value} must be within {new_bound=}') + 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.enabled: @@ -344,6 +355,10 @@ def bounds(self, new_bound: Tuple[numbers.Number, numbers.Number]) -> None: 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]: """ diff --git a/src/easyscience/Objects/virtual.py b/src/easyscience/Objects/virtual.py index eb4e7623..03fed121 100644 --- a/src/easyscience/Objects/virtual.py +++ b/src/easyscience/Objects/virtual.py @@ -16,6 +16,12 @@ from easyscience import global_object from easyscience.Constraints import ObjConstraint +from easyscience.Objects.variable.descriptor_base import DescriptorBase +from easyscience.Objects.variable.descriptor_bool import DescriptorBool +from easyscience.Objects.variable.descriptor_number import DescriptorNumber +from easyscience.Objects.variable.descriptor_str import DescriptorStr +from easyscience.Objects.variable.parameter import Constraints +from easyscience.Objects.variable.parameter import Parameter if TYPE_CHECKING: from easyscience.Objects.ObjectClasses import BV @@ -31,8 +37,8 @@ def _remover(a_obj_id: str, v_obj_id: str): a_obj = global_object.map.get_item_by_key(a_obj_id) except ValueError: return - if a_obj._constraints['virtual'].get(v_obj_id, False): - del a_obj._constraints['virtual'][v_obj_id] + if a_obj._constraints.virtual.get(v_obj_id, False): + del a_obj._constraints.virtual[v_obj_id] def realizer(obj: BV): @@ -43,10 +49,9 @@ def realizer(obj: BV): """ if getattr(obj, '_is_virtual', False): klass = getattr(obj, '__non_virtual_class__') - import easyscience.Objects.Variable as ec_var args = [] - if klass in ec_var.__dict__.values(): # is_variable check + if klass in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # is_variable check kwargs = obj.encode_data() kwargs['unique_name'] = None return klass(**kwargs) @@ -73,8 +78,6 @@ def component_realizer(obj: BV, component: str, recursive: bool = True): :param recursive: Should we realize all sub-components of the component """ - import easyscience.Objects.Variable as ec_var - done_mapping = True if not isinstance(obj, Iterable) or not issubclass(obj.__class__, MutableSequence): old_component = obj._kwargs[component] @@ -95,7 +98,7 @@ def component_realizer(obj: BV, component: str, recursive: bool = True): else: value = key key = value.unique_name - if getattr(value, '__old_class__', value.__class__) in ec_var.__dict__.values(): + if getattr(value, '__old_class__', value.__class__) in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # noqa: E501 continue component._global_object.map.prune_vertex_from_edge(component, component._kwargs[key]) component._global_object.map.add_edge(component, old_component._kwargs[key]) @@ -148,9 +151,7 @@ def virtualizer(obj: BV) -> BV: 'relalize_component': component_realizer, } - import easyscience.Objects.Variable as ec_var - - if klass in ec_var.__dict__.values(): # is_variable check + if klass in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # is_variable check virtual_options['fixed'] = property( fget=lambda self: self._fixed, fset=lambda self, value: raise_(AttributeError('Virtual parameters cannot be fixed')), @@ -160,7 +161,8 @@ def virtualizer(obj: BV) -> BV: # Determine what to do next. args = [] # If `obj` is a parameter or descriptor etc, then simple mods. - if hasattr(obj, '_constructor'): + # if hasattr(obj, '_constructor'): + if isinstance(obj, DescriptorBase): # All Variables are based on the Descriptor. d = obj.encode_data() if hasattr(d, 'fixed'): @@ -170,8 +172,12 @@ def virtualizer(obj: BV) -> BV: v_p._enabled = False constraint = ObjConstraint(v_p, '', obj) constraint.external = True - obj._constraints['virtual'][v_p.unique_name] = constraint - v_p._constraints['builtin'] = dict() + obj._constraints.virtual[v_p.unique_name] = constraint + v_p._constraints = Constraints( + user=v_p._constraints.user, + builtin=dict(), # Set the new value for 'builtin' + virtual=v_p._constraints.virtual + ) setattr(v_p, '__previous_set', getattr(obj, '__previous_set', None)) weakref.finalize( v_p, diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index e0bd6771..02130a6e 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -20,7 +20,7 @@ # causes circular import when Parameter is imported # from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from ..available_minimizers import AvailableMinimizers from .utils import FitError @@ -194,11 +194,7 @@ def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]: for name, item in pars.items(): parameter_name = MINIMIZER_PARAMETER_PREFIX + str(name) if parameter_name not in parameters.keys(): - # TODO clean when full move to new_variable - if isinstance(item, Parameter): - parameters[parameter_name] = item.value - else: - parameters[parameter_name] = item.raw_value + parameters[parameter_name] = item.value return parameters def _generate_fit_function(self) -> Callable: @@ -235,15 +231,9 @@ def _fit_function(x: np.ndarray, **kwargs): for name, value in kwargs.items(): par_name = name[1:] if par_name in self._cached_pars.keys(): - # TODO clean when full move to new_variable - if isinstance(self._cached_pars[par_name], Parameter): # This will take into account constraints - if self._cached_pars[par_name].value != value: - self._cached_pars[par_name].value = value - else: - # This will take into account constraints - if self._cached_pars[par_name].raw_value != value: - self._cached_pars[par_name].value = value + if self._cached_pars[par_name].value != value: + self._cached_pars[par_name].value = value # Since we are calling the parameter fset will be called. # TODO Pre processing here @@ -268,11 +258,7 @@ def _create_signature(parameters: Dict[int, Parameter]) -> Signature: wrapped_parameters.append(InspectParameter('x', InspectParameter.POSITIONAL_OR_KEYWORD, annotation=_empty)) for name, parameter in parameters.items(): - ## TODO clean when full move to new_variable - if isinstance(parameter, Parameter): - default_value = parameter.value - else: - default_value = parameter.raw_value + default_value = parameter.value wrapped_parameters.append( InspectParameter( diff --git a/src/easyscience/fitting/minimizers/minimizer_bumps.py b/src/easyscience/fitting/minimizers/minimizer_bumps.py index ee9f75f5..14df1d0f 100644 --- a/src/easyscience/fitting/minimizers/minimizer_bumps.py +++ b/src/easyscience/fitting/minimizers/minimizer_bumps.py @@ -16,7 +16,7 @@ # causes circular import when Parameter is imported # from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -122,11 +122,7 @@ def fit( model = model_function(x, y, weights) self._cached_model = model - ## TODO clean when full move to new_variable - if isinstance(self._cached_pars[list(self._cached_pars.keys())[0]], Parameter): - self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} - else: - self._p_0 = {f'p{key}': self._cached_pars[key].raw_value for key in self._cached_pars.keys()} + self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} problem = FitProblem(model) # Why do we do this? Because a fitting template has to have global_object instantiated outside pre-runtime @@ -170,11 +166,7 @@ def convert_to_par_object(obj) -> BumpsParameter: :rtype: BumpsParameter """ - ## TODO clean when full move to new_variable - if isinstance(obj, Parameter): - value = obj.value - else: - value = obj.raw_value + value = obj.value return BumpsParameter( name=MINIMIZER_PARAMETER_PREFIX + obj.unique_name, @@ -253,11 +245,7 @@ def _gen_fit_results(self, fit_results, **kwargs) -> FitResults: for index, name in enumerate(self._cached_model._pnames): dict_name = name[len(MINIMIZER_PARAMETER_PREFIX) :] - ## TODO clean when full move to new_variable - if isinstance(pars[dict_name], Parameter): - item[name] = pars[dict_name].value - else: - item[name] = pars[dict_name].raw_value + item[name] = pars[dict_name].value results.p0 = self._p_0 results.p = item diff --git a/src/easyscience/fitting/minimizers/minimizer_dfo.py b/src/easyscience/fitting/minimizers/minimizer_dfo.py index 8bf09ef5..27f7eba4 100644 --- a/src/easyscience/fitting/minimizers/minimizer_dfo.py +++ b/src/easyscience/fitting/minimizers/minimizer_dfo.py @@ -12,7 +12,7 @@ # causes circular import when Parameter is imported # from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -96,11 +96,7 @@ def fit( self._cached_model.x = x self._cached_model.y = y - ## TODO clean when full move to new_variable - if isinstance(self._cached_pars[list(self._cached_pars.keys())[0]], Parameter): - self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} - else: - self._p_0 = {f'p{key}': self._cached_pars[key].raw_value for key in self._cached_pars.keys()} + self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()} # Why do we do this? Because a fitting template has to have global_object instantiated outside pre-runtime from easyscience import global_object @@ -145,22 +141,14 @@ def _make_model(self, parameters: Optional[List[Parameter]] = None) -> Callable: def _outer(obj: DFO): def _make_func(x, y, weights): - ## TODO clean when full move to new_variable dfo_pars = {} if not parameters: for name, par in obj._cached_pars.items(): - if isinstance(par, Parameter): - dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.value - else: - dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.raw_value - + dfo_pars[MINIMIZER_PARAMETER_PREFIX + str(name)] = par.value else: for par in parameters: - if isinstance(par, Parameter): - dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.value - else: - dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.raw_value + dfo_pars[MINIMIZER_PARAMETER_PREFIX + par.unique_name] = par.value def _residuals(pars_values: List[float]) -> np.ndarray: for idx, par_name in enumerate(dfo_pars.keys()): @@ -217,11 +205,7 @@ def _gen_fit_results(self, fit_results, weights, **kwargs) -> FitResults: pars = {} for p_name, par in self._cached_pars.items(): - ## TODO clean when full move to new_variable - if isinstance(par, Parameter): - pars[f'p{p_name}'] = par.value - else: - pars[f'p{p_name}'] = par.raw_value + pars[f'p{p_name}'] = par.value results.p = pars results.p0 = self._p_0 @@ -254,11 +238,7 @@ def _dfo_fit( :return: dfols fit results container """ - ## TODO clean when full move to new_variable - if isinstance(list(pars.values())[0], Parameter): - pars_values = np.array([par.value for par in pars.values()]) - else: - pars_values = np.array([par.raw_value for par in pars.values()]) + pars_values = np.array([par.value for par in pars.values()]) bounds = ( np.array([par.min for par in pars.values()]), diff --git a/src/easyscience/fitting/minimizers/minimizer_lmfit.py b/src/easyscience/fitting/minimizers/minimizer_lmfit.py index a4094ea8..bb29e8de 100644 --- a/src/easyscience/fitting/minimizers/minimizer_lmfit.py +++ b/src/easyscience/fitting/minimizers/minimizer_lmfit.py @@ -15,7 +15,7 @@ # causes circular import when Parameter is imported # from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from ..available_minimizers import AvailableMinimizers from .minimizer_base import MINIMIZER_PARAMETER_PREFIX @@ -180,11 +180,7 @@ def convert_to_par_object(parameter: Parameter) -> LMParameter: :return: lmfit Parameter compatible object. :rtype: LMParameter """ - ## TODO clean when full move to - if isinstance(parameter, Parameter): - value = parameter.value - else: - value = parameter.raw_value + value = parameter.value return LMParameter( MINIMIZER_PARAMETER_PREFIX + parameter.unique_name, @@ -221,11 +217,7 @@ def _make_model(self, pars: Optional[LMParameters] = None) -> LMModel: if isinstance(item, LMParameter): value = item.value else: - ## TODO clean when full move to new_variable - if isinstance(item, Parameter): - value = item.value - else: - value = item.raw_value + value = item.value model.set_param_hint(MINIMIZER_PARAMETER_PREFIX + str(name), value=value, min=item.min, max=item.max) diff --git a/src/easyscience/models/polynomial.py b/src/easyscience/models/polynomial.py index 4086e80c..76689c01 100644 --- a/src/easyscience/models/polynomial.py +++ b/src/easyscience/models/polynomial.py @@ -23,7 +23,7 @@ def designate_calc_fn(func): @functools.wraps(func) def wrapper(obj, *args, **kwargs): for name in list(obj.__annotations__.keys()): - func.__globals__['_' + name] = getattr(obj, name).raw_value + func.__globals__['_' + name] = getattr(obj, name).value return func(obj, *args, **kwargs) return wrapper @@ -64,16 +64,16 @@ def __init__( raise TypeError('coefficients must be a list or a BaseCollection') def __call__(self, x: np.ndarray, *args, **kwargs) -> np.ndarray: - return np.polyval([c.raw_value for c in self.coefficients], x) + 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].raw_value}'] + s += [f'{self.coefficients[0].value}'] if len(self.coefficients) >= 2: - s += [f'{self.coefficients[1].raw_value}x'] + s += [f'{self.coefficients[1].value}x'] if len(self.coefficients) >= 3: - s += [f'{c.raw_value}x^{i+2}' for i, c in enumerate(self.coefficients[2:]) if c.raw_value != 0] + 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) @@ -94,9 +94,9 @@ def __init__( if c is not None: self.c = c - # @designate_calc_fn can be used to inject parameters into the calculation function. i.e. _m = m.raw_value + # @designate_calc_fn can be used to inject parameters into the calculation function. i.e. _m = m.value def __call__(self, x: np.ndarray, *args, **kwargs) -> np.ndarray: - return self.m.raw_value * x + self.c.raw_value + return self.m.value * x + self.c.value def __repr__(self): return '{}({}, {})'.format(self.__class__.__name__, self.m, self.c) diff --git a/tests/integration_tests/Fitting/test_fitter.py b/tests/integration_tests/Fitting/test_fitter.py index cf17a79d..19e0f876 100644 --- a/tests/integration_tests/Fitting/test_fitter.py +++ b/tests/integration_tests/Fitting/test_fitter.py @@ -13,7 +13,7 @@ from easyscience.fitting.minimizers import FitError from easyscience.fitting.available_minimizers import AvailableMinimizers from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter class AbsSin(BaseObj): diff --git a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py b/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py deleted file mode 100644 index ca10fae9..00000000 --- a/tests/integration_tests/Fitting/test_fitter_legacy_parameter.py +++ /dev/null @@ -1,335 +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.raw_value == pytest.approx(item2.raw_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, 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() - assert sp_sin.phase.raw_value == pytest.approx(ref_sin.phase.raw_value, rel=1e-3) - assert sp_sin.offset.raw_value == pytest.approx(ref_sin.offset.raw_value, rel=1e-3) - - -@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.raw_value != pytest.approx(ref_sin.phase.raw_value, rel=1e-3) - assert sp_sin.offset.raw_value != pytest.approx(ref_sin.offset.raw_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, 0.1), (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.raw_value != pytest.approx(ref_sin.phase.raw_value, rel=1e-3) - assert sp_sin.offset.raw_value != pytest.approx(ref_sin.offset.raw_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.raw_value - for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()) - } - sp_ref2 = { - f"p{item1.unique_name}": item2.raw_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_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_fit_constraints(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) - - assert len(f.fit_constraints()) == 0 - c = ObjConstraint(sp_sin.offset, "2*", sp_sin.phase) - f.add_fit_constraint(c) - - 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) - assert len(f.fit_constraints()) == 1 - f.remove_fit_constraint(0) - assert len(f.fit_constraints()) == 0 - - -@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 index ba59f953..1c1735a0 100644 --- a/tests/integration_tests/Fitting/test_multi_fitter.py +++ b/tests/integration_tests/Fitting/test_multi_fitter.py @@ -12,7 +12,7 @@ from easyscience.fitting.multi_fitter import MultiFitter from easyscience.fitting.minimizers import FitError from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter class Line(BaseObj): diff --git a/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py b/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py deleted file mode 100644 index 0f9d70a2..00000000 --- a/tests/integration_tests/Fitting/test_multi_fitter_legacy_parameter.py +++ /dev/null @@ -1,279 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project None: minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') cached_par = MagicMock() - cached_par.raw_value = 1 + cached_par.value = 1 cached_pars = {'mock_parm_1': cached_par} minimizer._cached_pars = cached_pars @@ -140,9 +140,9 @@ def test_gen_fit_results(self, minimizer: Bumps, monkeypatch): minimizer._cached_model = mock_cached_model mock_cached_par_1 = MagicMock() - mock_cached_par_1.raw_value = 'par_raw_value_1' + mock_cached_par_1.value = 'par_value_1' mock_cached_par_2 = MagicMock() - mock_cached_par_2.raw_value = 'par_raw_value_2' + 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' @@ -157,10 +157,10 @@ def test_gen_fit_results(self, minimizer: Bumps, monkeypatch): 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_raw_value_1', 'ppar_2': 'par_raw_value_2'} + 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_raw_value_1', 'ppar_2': 'par_raw_value_2'}) + 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 index ae620f11..8c39b8a5 100644 --- a/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py +++ b/tests/unit_tests/Fitting/minimizers/test_minimizer_dfo.py @@ -4,7 +4,7 @@ import numpy as np import easyscience.fitting.minimizers.minimizer_dfo -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from easyscience.fitting.minimizers.minimizer_dfo import DFO from easyscience.fitting.minimizers.utils import FitError @@ -53,7 +53,7 @@ def test_fit(self, minimizer: DFO) -> None: minimizer._gen_fit_results = MagicMock(return_value='gen_fit_results') cached_par = MagicMock() - cached_par.raw_value = 1 + cached_par.value = 1 cached_pars = {'mock_parm_1': cached_par} minimizer._cached_pars = cached_pars @@ -104,10 +104,10 @@ def test_make_model(self, minimizer: DFO) -> None: mock_parm_1 = MagicMock() mock_parm_1.unique_name = 'mock_parm_1' - mock_parm_1.raw_value = 1000.0 + mock_parm_1.value = 1000.0 mock_parm_2 = MagicMock() mock_parm_2.unique_name = 'mock_parm_2' - mock_parm_2.raw_value = 2000.0 + mock_parm_2.value = 2000.0 # Then model = minimizer._make_model(parameters=[mock_parm_1, mock_parm_2]) @@ -160,9 +160,9 @@ def test_gen_fit_results(self, minimizer: DFO, monkeypatch): minimizer._cached_model = mock_cached_model mock_cached_par_1 = MagicMock() - mock_cached_par_1.raw_value = 'par_raw_value_1' + mock_cached_par_1.value = 'par_value_1' mock_cached_par_2 = MagicMock() - mock_cached_par_2.raw_value = 'par_raw_value_2' + 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' @@ -177,13 +177,13 @@ def test_gen_fit_results(self, minimizer: DFO, monkeypatch): 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_raw_value_1', 'ppar_2': 'par_raw_value_2'} + 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_raw_value_1', 'ppar_2': 'par_raw_value_2'}) + 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 diff --git a/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py b/tests/unit_tests/Fitting/minimizers/test_minimizer_lmfit.py index e17b7401..f981436e 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.Objects.new_variable import Parameter +from easyscience.Objects.variable import Parameter from lmfit import Parameter as LMParameter from easyscience.fitting.minimizers.utils import FitError diff --git a/tests/unit_tests/Fitting/test_constraints.py b/tests/unit_tests/Fitting/test_constraints.py index 566da085..47a5792b 100644 --- a/tests/unit_tests/Fitting/test_constraints.py +++ b/tests/unit_tests/Fitting/test_constraints.py @@ -13,7 +13,7 @@ from easyscience.Constraints import NumericConstraint from easyscience.Constraints import ObjConstraint -from easyscience.Objects.new_variable.parameter import Parameter +from easyscience.Objects.variable.parameter import Parameter @pytest.fixture diff --git a/tests/unit_tests/Fitting/test_constraints_legacy_parameter.py b/tests/unit_tests/Fitting/test_constraints_legacy_parameter.py deleted file mode 100644 index b20cd0f2..00000000 --- a/tests/unit_tests/Fitting/test_constraints_legacy_parameter.py +++ /dev/null @@ -1,131 +0,0 @@ -__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 Tuple[List[Parameter], List[int]]: - return [Parameter("a", 1), Parameter("b", 2)], [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 == twoPars[1][0] - - # Should update to new value - c = NumericConstraint(twoPars[0][1], "==", value) - c() - assert twoPars[0][1].value == 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 == value - - # Should skip - c = NumericConstraint(twoPars[0][1], ">", value) - c() - assert twoPars[0][1].value == 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 == twoPars[1][0] - - # Should update to new value - c = NumericConstraint(twoPars[0][1], "<", value) - c() - assert twoPars[0][1].value == 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 == 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 == 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 == value - assert p1.value == value - assert p2.value == value - - -def test_ConstraintEnable_Disable(twoPars): - - assert twoPars[0][0].enabled - assert twoPars[0][1].enabled - - 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 - - c.enabled = False - assert not c.enabled - assert twoPars[0][1].enabled - assert twoPars[0][0].enabled - - c.enabled = True - assert c.enabled - assert twoPars[0][1].enabled - assert not twoPars[0][0].enabled diff --git a/tests/unit_tests/Objects/test_BaseObj.py b/tests/unit_tests/Objects/test_BaseObj.py index c545e6bc..d95d9b0a 100644 --- a/tests/unit_tests/Objects/test_BaseObj.py +++ b/tests/unit_tests/Objects/test_BaseObj.py @@ -19,8 +19,8 @@ import easyscience from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import DescriptorNumber -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import DescriptorNumber +from easyscience.Objects.variable import Parameter from easyscience.Utils.io.dict import DictSerializer from easyscience import global_object @@ -357,7 +357,7 @@ def test_subclassing(): from typing import ClassVar from easyscience.models.polynomial import Line - from easyscience.Objects.Variable import Parameter + from easyscience.Objects.variable import Parameter class L2(Line): diff: ClassVar[Parameter] @@ -375,23 +375,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) @@ -410,11 +410,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 diff --git a/tests/unit_tests/Objects/test_BaseObj_legacy_parameter.py b/tests/unit_tests/Objects/test_BaseObj_legacy_parameter.py deleted file mode 100644 index 5fdb087e..00000000 --- a/tests/unit_tests/Objects/test_BaseObj_legacy_parameter.py +++ /dev/null @@ -1,503 +0,0 @@ -__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 -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project " - d = Descriptor("test", 1, units="cm") - assert repr(d) == f"<{d.__class__.__name__} 'test': 1 cm>" - - -def test_parameter_repr(): - d = Parameter("test", 1) - assert repr(d) == f"<{d.__class__.__name__} 'test': 1.0+/-0, bounds=[-inf:inf]>" - d = Parameter("test", 1, units="cm") - assert repr(d) == f"<{d.__class__.__name__} 'test': 1.0+/-0 cm, bounds=[-inf:inf]>" - - d = Parameter("test", 1, fixed=True) - assert ( - repr(d) - == f"<{d.__class__.__name__} 'test': 1.0+/-0 (fixed), bounds=[-inf:inf]>" - ) - d = Parameter("test", 1, units="cm", fixed=True) - assert ( - repr(d) - == f"<{d.__class__.__name__} 'test': 1.0+/-0 cm (fixed), bounds=[-inf:inf]>" - ) - - -def test_descriptor_as_dict(): - d = Descriptor("test", 1) - result = d.as_dict() - expected = { - "@module": Descriptor.__module__, - "@class": Descriptor.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1, - "units": "dimensionless", - "description": "", - "url": "", - "display_name": "test", - "callback": None, - } - for key in expected.keys(): - if key == "callback": - continue - assert result[key] == expected[key] - - -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, - "error": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "units": "dimensionless", - } - for key in expected.keys(): - if key == "callback": - continue - assert result[key] == expected[key] - - # Check that additional arguments work - d = Parameter("test", 1, units="km", url="https://www.boo.com") - result = d.as_dict() - expected = { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "units": "kilometer", - "value": 1.0, - "error": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - } - for key in expected.keys(): - if key == "callback": - continue - assert result[key] == expected[key] - - -@pytest.mark.parametrize( - "reference, constructor", - ( - [ - { - "@module": Descriptor.__module__, - "@class": Descriptor.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1, - "units": "dimensionless", - "description": "", - "url": "", - "display_name": "test", - "callback": None, - }, - Descriptor, - ], - [ - { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "units": "kilometer", - "value": 1.0, - "error": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - }, - Parameter, - ], - ), - ids=["Descriptor", "Parameter"], -) -def test_item_from_dict(reference, constructor): - d = constructor.from_dict(reference) - for key, item in reference.items(): - if key == "callback" or key.startswith("@"): - continue - if key == "units": - key = "unit" - if key == "value": - key = "raw_value" - obtained = getattr(d, key) - if isinstance(obtained, (ureg.Unit, Q_)): - obtained = str(obtained) - assert obtained == item - - -@pytest.mark.parametrize( - "construct", - ( - { - "@module": Descriptor.__module__, - "@class": Descriptor.__name__, - "@version": easyscience.__version__, - "name": "test", - "value": 1, - "units": "dimensionless", - "description": "", - "url": "", - "display_name": "test", - "callback": None, - }, - { - "@module": Parameter.__module__, - "@class": Parameter.__name__, - "@version": easyscience.__version__, - "name": "test", - "units": "kilometer", - "value": 1.0, - "error": 0.0, - "min": -np.inf, - "max": np.inf, - "fixed": False, - "url": "https://www.boo.com", - }, - ), - ids=["Descriptor", "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 - if key == "units": - key = "unit" - if key == "value": - key = "raw_value" - obtained = getattr(d, key) - if isinstance(obtained, (ureg.Unit, Q_)): - obtained = str(obtained) - 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.raw_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.raw_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 = {"error": 1.0, "min": -0.1, "max": 2147483648, "fixed": False} - advanced_result = { - "error": {"name": "error", "value": advanced["error"]}, - "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) - if isinstance(obtained, (ureg.Unit, Q_)): - obtained = str(obtained) - 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 - - p = Descriptor("test", 1, display_name=value) - assert p.display_name == value - - -@pytest.mark.parametrize("instance", (Descriptor, Parameter), indirect=True) -def test_item_boolean_value(instance): - def creator(value): - if instance == Parameter: - with pytest.warns(UserWarning): - item = instance("test", value) - else: - item = instance("test", value) - return item - - def setter(item, value): - if instance == Parameter: - with pytest.warns(UserWarning): - item.value = value - else: - item.value = value - - item = creator(True) - assert item.value is True - setter(item, False) - assert item.value is False - - item = creator(False) - assert item.value is False - setter(item, True) - assert item.value is True - - -@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 diff --git a/tests/unit_tests/Objects/test_Groups.py b/tests/unit_tests/Objects/test_Groups.py index eb9ce042..3850b5f3 100644 --- a/tests/unit_tests/Objects/test_Groups.py +++ b/tests/unit_tests/Objects/test_Groups.py @@ -12,8 +12,8 @@ import easyscience from easyscience.Objects.Groups import BaseCollection from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import DescriptorNumber -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import DescriptorNumber +from easyscience.Objects.variable import Parameter from easyscience import global_object test_dict = { @@ -381,6 +381,7 @@ def testit(item1, item2): @pytest.mark.parametrize("cls", class_constructors) def test_baseCollection_from_dict(cls): + global_object.map._clear() #TODO: figure out why this test fails without this line name = "testing" kwargs = {"p1": DescriptorNumber("par1", 1)} expected = cls.from_dict(test_dict) @@ -437,6 +438,7 @@ def test_baseCollection_iterator(cls): @pytest.mark.parametrize("cls", class_constructors) def test_baseCollection_iterator_dict(cls): + global_object.map._clear() #TODO: figure out why this test fails without this line name = "test" p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) @@ -456,6 +458,7 @@ def test_baseCollection_iterator_dict(cls): @pytest.mark.parametrize("cls", class_constructors) def test_baseCollection_sameName(cls): + global_object.map._clear() #TODO: figure out why this test fails without this line name = "test" p1 = Parameter("p1", 1) p2 = Parameter("p1", 2) diff --git a/tests/unit_tests/Objects/test_Virtual.py b/tests/unit_tests/Objects/test_Virtual.py index 7c610887..04d331b9 100644 --- a/tests/unit_tests/Objects/test_Virtual.py +++ b/tests/unit_tests/Objects/test_Virtual.py @@ -9,7 +9,7 @@ from easyscience.models.polynomial import Line from easyscience.Objects import virtual as Virtual -from easyscience.Objects.Variable import Parameter +from easyscience.Objects.variable.parameter import Parameter @pytest.mark.parametrize( @@ -33,7 +33,7 @@ def test_virtual_variable(cls): assert hasattr(v_obj, attr) assert obj.name == v_obj.name - assert obj.raw_value == v_obj.raw_value + assert obj.value == v_obj.value @pytest.mark.parametrize( @@ -43,35 +43,37 @@ def test_virtual_variable(cls): ], ) def test_virtual_variable_modify(cls): + import gc obj = cls(name="test", value=1) v_obj = Virtual.virtualizer(obj) assert obj.name == v_obj.name - assert obj.raw_value == v_obj.raw_value + assert obj.value == v_obj.value new_value = 2.0 obj.value = new_value - assert obj.raw_value == v_obj.raw_value + assert obj.value == v_obj.value id_vobj = v_obj.unique_name - assert id_vobj in list(obj._constraints["virtual"].keys()) + assert id_vobj in list(obj._constraints.virtual.keys()) del v_obj - # assert id_vobj not in list(obj._constraints["virtual"].keys()) + gc.collect() # Force garbage collection + assert id_vobj not in list(obj._constraints.virtual.keys()) def test_Base_obj(): l = Line(2, 1) v_l = Virtual.virtualizer(l) assert l.name == v_l.name - assert l.m.raw_value == v_l.m.raw_value - assert l.c.raw_value == v_l.c.raw_value + assert l.m.value == v_l.m.value + assert l.c.value == v_l.c.value m = 4.0 l.m = m - assert l.m.raw_value == m - assert l.m.raw_value == v_l.m.raw_value - assert l.c.raw_value == v_l.c.raw_value + assert l.m.value == m + assert l.m.value == v_l.m.value + assert l.c.value == v_l.c.value def test_Base_obj(): @@ -79,19 +81,19 @@ def test_Base_obj(): l = Line(old_m, 1) v_l = Virtual.virtualizer(l) assert l.name == v_l.name - assert l.m.raw_value == v_l.m.raw_value - assert l.c.raw_value == v_l.c.raw_value + assert l.m.value == v_l.m.value + assert l.c.value == v_l.c.value Virtual.component_realizer(v_l, "m") m = 4.0 l.m = m - assert l.m.raw_value == m - assert v_l.m.raw_value == old_m - assert l.c.raw_value == v_l.c.raw_value + assert l.m.value == m + assert v_l.m.value == old_m + assert l.c.value == v_l.c.value m_other = 5.0 v_l.m = m_other - assert l.m.raw_value == m - assert v_l.m.raw_value == m_other - assert l.c.raw_value == v_l.c.raw_value + assert l.m.value == m + assert v_l.m.value == m_other + assert l.c.value == v_l.c.value diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_base.py b/tests/unit_tests/Objects/variable/test_descriptor_base.py similarity index 98% rename from tests/unit_tests/Objects/new_variable/test_descriptor_base.py rename to tests/unit_tests/Objects/variable/test_descriptor_base.py index 7a93b4f2..38140a34 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_base.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_base.py @@ -1,7 +1,7 @@ import pytest from easyscience import global_object -from easyscience.Objects.new_variable.descriptor_base import DescriptorBase +from easyscience.Objects.variable.descriptor_base import DescriptorBase class TestDesciptorBase: diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_bool.py b/tests/unit_tests/Objects/variable/test_descriptor_bool.py similarity index 97% rename from tests/unit_tests/Objects/new_variable/test_descriptor_bool.py rename to tests/unit_tests/Objects/variable/test_descriptor_bool.py index 0f0e9fa9..63bf484a 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_bool.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_bool.py @@ -1,6 +1,6 @@ import pytest -from easyscience.Objects.new_variable.descriptor_bool import DescriptorBool +from easyscience.Objects.variable.descriptor_bool import DescriptorBool from easyscience import global_object class TestDescriptorBool: diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_from_legacy.py b/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py similarity index 97% rename from tests/unit_tests/Objects/new_variable/test_descriptor_from_legacy.py rename to tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py index d559a709..038697ae 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_from_legacy.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_from_legacy.py @@ -12,9 +12,9 @@ import scipp as sc import easyscience -from easyscience.Objects.new_variable import DescriptorBool -from easyscience.Objects.new_variable import DescriptorNumber -from easyscience.Objects.new_variable import DescriptorStr +from easyscience.Objects.variable import DescriptorBool +from easyscience.Objects.variable import DescriptorNumber +from easyscience.Objects.variable import DescriptorStr @pytest.fixture @@ -213,4 +213,3 @@ def test_item_boolean_value(): assert item.value is False item.value = True assert item.value is True - diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_number.py b/tests/unit_tests/Objects/variable/test_descriptor_number.py similarity index 97% rename from tests/unit_tests/Objects/new_variable/test_descriptor_number.py rename to tests/unit_tests/Objects/variable/test_descriptor_number.py index 75edc6dd..62b359a4 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_number.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_number.py @@ -3,7 +3,7 @@ import scipp as sc from scipp import UnitError -from easyscience.Objects.new_variable.descriptor_number import DescriptorNumber +from easyscience.Objects.variable.descriptor_number import DescriptorNumber from easyscience import global_object class TestDescriptorNumber: @@ -157,6 +157,20 @@ def test_set_variance(self, descriptor: DescriptorNumber): # Expect assert descriptor._scalar.variance == 0.2 + assert descriptor.error == 0.4472135954999579 + + def test_error(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.error == 0.31622776601683794 + + def test_set_error(self, descriptor: DescriptorNumber): + # When Then + descriptor.error = 0.31622776601683794 + + # Expect + assert descriptor.error == 0.31622776601683794 + assert descriptor.variance == 0.1 + def test_value(self, descriptor: DescriptorNumber): # When Then Expect diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_str.py b/tests/unit_tests/Objects/variable/test_descriptor_str.py similarity index 97% rename from tests/unit_tests/Objects/new_variable/test_descriptor_str.py rename to tests/unit_tests/Objects/variable/test_descriptor_str.py index c087a527..71c50715 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_str.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_str.py @@ -1,6 +1,6 @@ import pytest -from easyscience.Objects.new_variable.descriptor_str import DescriptorStr +from easyscience.Objects.variable.descriptor_str import DescriptorStr from easyscience import global_object class TestDescriptorStr: diff --git a/tests/unit_tests/Objects/new_variable/test_parameter.py b/tests/unit_tests/Objects/variable/test_parameter.py similarity index 99% rename from tests/unit_tests/Objects/new_variable/test_parameter.py rename to tests/unit_tests/Objects/variable/test_parameter.py index f6958dc2..e00350b6 100644 --- a/tests/unit_tests/Objects/new_variable/test_parameter.py +++ b/tests/unit_tests/Objects/variable/test_parameter.py @@ -5,8 +5,8 @@ from scipp import UnitError -from easyscience.Objects.new_variable.parameter import Parameter -from easyscience.Objects.new_variable.descriptor_number import DescriptorNumber +from easyscience.Objects.variable.parameter import Parameter +from easyscience.Objects.variable.descriptor_number import DescriptorNumber from easyscience import global_object class TestParameter: diff --git a/tests/unit_tests/Objects/new_variable/test_parameter_from_legacy.py b/tests/unit_tests/Objects/variable/test_parameter_from_legacy.py similarity index 98% rename from tests/unit_tests/Objects/new_variable/test_parameter_from_legacy.py rename to tests/unit_tests/Objects/variable/test_parameter_from_legacy.py index 867c103d..f4dcd2ea 100644 --- a/tests/unit_tests/Objects/new_variable/test_parameter_from_legacy.py +++ b/tests/unit_tests/Objects/variable/test_parameter_from_legacy.py @@ -13,10 +13,10 @@ import scipp as sc import easyscience -from easyscience.Objects.Variable import CoreSetException +from easyscience.Utils.Exceptions import CoreSetException -from easyscience.Objects.new_variable import Parameter -from easyscience.Objects.Variable import global_object +from easyscience.Objects.variable import Parameter +from easyscience import global_object def _generate_inputs(): @@ -421,4 +421,4 @@ def test_parameter_bounds(value): assert p.max == 2 assert p.bounds == (0, 2) assert p.enabled is True - assert p.fixed is False + assert p.fixed is False \ No newline at end of file diff --git a/tests/unit_tests/global_object/test_global_object.py b/tests/unit_tests/global_object/test_global_object.py index d52c817a..2997b523 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.new_variable.descriptor_bool import DescriptorBool +from easyscience.Objects.variable.descriptor_bool 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 bc7958d9..b3eae678 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 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,78 +319,79 @@ 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'), -# pytest.param([Parameter.__isub__, float.__sub__], id='Subtraction')]) +# TODO: Check if this test is needed +# @pytest.mark.parametrize('math_funcs', [pytest.param([Parameter.__add__, float.__add__], id='Addition'), +# pytest.param([Parameter.__sub__, float.__sub__], id='Subtraction')]) # def test_parameter_maths_basic(math_funcs): # a = 1.0 # b = 2.0 # sa = 0.1 # sb = 0.2 -# + # p_fun = math_funcs[0] # f_fun = math_funcs[1] -# + # result_value = f_fun(a, b) # result_error = (sa ** 2 + sb ** 2) ** 0.5 -# + # from easyscience import global_object # global_object.stack.enabled = True -# + # # Perform basic test # p1 = Parameter('a', a) # p2 = Parameter('b', b) -# -# p1 = p_fun(p1, p2) -# assert float(p1) == result_value -# global_object.stack.undo() -# assert float(p1) == a -# global_object.stack.redo() -# assert float(p1) == result_value -# -# # Perform basic + error -# p1 = Parameter('a', a, error=sa) -# p2 = Parameter('b', b, error=sb) -# p1 = p_fun(p1, p2) -# assert float(p1) == result_value -# assert p1.error == result_error -# global_object.stack.undo() -# assert float(p1) == a -# assert p1.error == sa -# global_object.stack.redo() -# assert float(p1) == result_value -# assert p1.error == result_error -# -# # Perform basic + units -# p1 = Parameter('a', a, error=sa, units='m/s') -# p2 = Parameter('b', b, error=sb, units='m/s') + # p1 = p_fun(p1, p2) -# assert float(p1) == result_value -# assert p1.error == result_error -# assert str(p1.unit) == 'meter / second' + +# assert p1.value == result_value # global_object.stack.undo() -# assert float(p1) == a -# assert p1.error == sa -# assert str(p1.unit) == 'meter / second' +# assert p1.value == a # global_object.stack.redo() -# assert float(p1) == result_value -# assert p1.error == result_error -# assert str(p1.unit) == 'meter / second' -# -# +# assert p1.value == result_value + + # # Perform basic + error + # p1 = Parameter('a', a, error=sa) + # p2 = Parameter('b', b, error=sb) + # p1 = p_fun(p1, p2) + # assert p1.value == result_value + # assert p1.error == result_error + # global_object.stack.undo() + # assert p1.value == a + # assert p1.error == sa + # global_object.stack.redo() + # assert p1.value == result_value + # assert p1.error == result_error + + # # Perform basic + units + # p1 = Parameter('a', a, error=sa, units='m/s') + # p2 = Parameter('b', b, error=sb, units='m/s') + # p1 = p_fun(p1, p2) + # assert p1.value == result_value + # assert p1.error == result_error + # assert str(p1.unit) == 'meter / second' + # global_object.stack.undo() + # assert p1.value == a + # assert p1.error == sa + # assert str(p1.unit) == 'meter / second' + # global_object.stack.redo() + # assert p1.value == result_value + # assert p1.error == result_error + # assert str(p1.unit) == 'meter / second' + + # @pytest.mark.parametrize('math_funcs', [pytest.param([Parameter.__imul__, float.__mul__, # 'meter ** 2 / second ** 2'], id='Multiplication'), # pytest.param([Parameter.__itruediv__, float.__truediv__, @@ -359,53 +402,53 @@ def __call__(self, x: np.ndarray) -> np.ndarray: # sa = 0.1 # sb = 0.2 # unit = 'meter / second' -# + # p_fun = math_funcs[0] # f_fun = math_funcs[1] # u_str = math_funcs[2] -# + # result_value = f_fun(a, b) # result_error = ((sa / a) ** 2 + (sb / b) ** 2) ** 0.5 * result_value -# + # from easyscience import global_object # global_object.stack.enabled = True -# + # # Perform basic test # p1 = Parameter('a', a) # p2 = Parameter('b', b) -# + # p1 = p_fun(p1, p2) -# assert float(p1) == result_value +# assert p1.value == result_value # global_object.stack.undo() -# assert float(p1) == a +# assert p1.value == a # global_object.stack.redo() -# assert float(p1) == result_value -# +# assert p1.value == result_value + # # Perform basic + error # p1 = Parameter('a', a, error=sa) # p2 = Parameter('b', b, error=sb) # p1 = p_fun(p1, p2) -# assert float(p1) == result_value +# assert p1.value == result_value # assert p1.error == result_error # global_object.stack.undo() -# assert float(p1) == a +# assert p1.value == a # assert p1.error == sa # global_object.stack.redo() -# assert float(p1) == result_value +# assert p1.value == result_value # assert p1.error == result_error -# + # # Perform basic + units # p1 = Parameter('a', a, error=sa, units=unit) # p2 = Parameter('b', b, error=sb, units=unit) # p1 = p_fun(p1, p2) -# assert float(p1) == result_value +# assert p1.value == result_value # assert p1.error == result_error # assert str(p1.unit) == u_str # global_object.stack.undo() -# assert float(p1) == a +# assert p1.value == a # assert p1.error == sa # assert str(p1.unit) == unit # global_object.stack.redo() -# assert float(p1) == result_value +# assert p1.value == result_value # assert p1.error == result_error # assert str(p1.unit) == u_str diff --git a/tests/unit_tests/models/test_polynomial.py b/tests/unit_tests/models/test_polynomial.py index 9e7a5afd..adccb9e1 100644 --- a/tests/unit_tests/models/test_polynomial.py +++ b/tests/unit_tests/models/test_polynomial.py @@ -10,7 +10,7 @@ from easyscience.models.polynomial import Line from easyscience.models.polynomial import Polynomial -from easyscience.Objects.Variable import Parameter +from easyscience.Objects.variable.parameter import Parameter line_test_cases = ((1, 2), (-1, -2), (0.72, 6.48)) poly_test_cases = ( @@ -29,11 +29,11 @@ def test_Line_pars(m, c): line = Line(m, c) - assert line.m.raw_value == m - assert line.c.raw_value == c + assert line.m.value == m + assert line.c.value == c x = np.linspace(0, 10, 100) - y = line.m.raw_value * x + line.c.raw_value + y = line.m.value * x + line.c.value assert np.allclose(line(x), y) @@ -43,11 +43,11 @@ def test_Line_constructor(m, c): c_ = Parameter("c", c) line = Line(m_, c_) - assert line.m.raw_value == m - assert line.c.raw_value == c + assert line.m.value == m + assert line.c.value == c x = np.linspace(0, 10, 100) - y = line.m.raw_value * x + line.c.raw_value + y = line.m.value * x + line.c.value assert np.allclose(line(x), y) @@ -55,7 +55,7 @@ def test_Line_constructor(m, c): def test_Polynomial_pars(coo): poly = Polynomial(coefficients=coo) - vals = {coo.raw_value for coo in poly.coefficients} + vals = {coo.value for coo in poly.coefficients} assert len(vals.difference(set(coo))) == 0 x = np.linspace(0, 10, 100) diff --git a/tests/unit_tests/utils/io_tests/test_core.py b/tests/unit_tests/utils/io_tests/test_core.py index c49f5f1d..3e87d539 100644 --- a/tests/unit_tests/utils/io_tests/test_core.py +++ b/tests/unit_tests/utils/io_tests/test_core.py @@ -9,8 +9,8 @@ import easyscience from easyscience.Objects.ObjectClasses import BaseObj -from easyscience.Objects.new_variable import DescriptorNumber -from easyscience.Objects.new_variable import Parameter +from easyscience.Objects.variable import DescriptorNumber +from easyscience.Objects.variable import Parameter dp_param_dict = { "argnames": "dp_kwargs, dp_cls", diff --git a/tests/unit_tests/utils/io_tests/test_dict.py b/tests/unit_tests/utils/io_tests/test_dict.py index 6a456581..884f86b6 100644 --- a/tests/unit_tests/utils/io_tests/test_dict.py +++ b/tests/unit_tests/utils/io_tests/test_dict.py @@ -10,7 +10,7 @@ from easyscience.Utils.io.dict import DataDictSerializer from easyscience.Utils.io.dict import DictSerializer -from easyscience.Objects.new_variable import DescriptorNumber +from easyscience.Objects.variable import DescriptorNumber from easyscience.Objects.ObjectClasses import BaseObj from .test_core import A @@ -230,7 +230,7 @@ def __init__(self, a, b, unique_name): "data": [1.0, 2.0, 3.0], }, "a": { - "@module": "easyscience.Objects.new_variable.descriptor_number", + "@module": "easyscience.Objects.variable.descriptor_number", "@class": "DescriptorNumber", "@version": version, "description": "", @@ -328,6 +328,7 @@ def test_group_encode2(): 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]): @@ -349,7 +350,7 @@ def test_group_encode2(): # # def test_objs(reference_obj, test_obj, in_dict): # if 'value' in in_dict.keys(): -# in_dict['raw_value'] = in_dict.pop('value') +# in_dict['value'] = in_dict.pop('value') # if 'units' in in_dict.keys(): # del in_dict['units'] # for k in in_dict.keys(): diff --git a/tests/unit_tests/utils/io_tests/test_json.py b/tests/unit_tests/utils/io_tests/test_json.py index e5d25c03..cec6e4c0 100644 --- a/tests/unit_tests/utils/io_tests/test_json.py +++ b/tests/unit_tests/utils/io_tests/test_json.py @@ -9,7 +9,7 @@ from easyscience.Utils.io.json import JsonDataSerializer from easyscience.Utils.io.json import JsonSerializer -from easyscience.Objects.new_variable import DescriptorNumber +from easyscience.Objects.variable import DescriptorNumber from .test_core import A 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 9f908a99..2edb761e 100644 --- a/tests/unit_tests/utils/io_tests/test_xml.py +++ b/tests/unit_tests/utils/io_tests/test_xml.py @@ -9,7 +9,7 @@ import pytest from easyscience.Utils.io.xml import XMLSerializer -from easyscience.Objects.new_variable import DescriptorNumber +from easyscience.Objects.variable import DescriptorNumber from .test_core import A from .test_core import dp_param_dict From d087e073937569ce1907cb9af494d7dc8ff95e2b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 20 Jan 2025 14:21:18 +0100 Subject: [PATCH 03/48] Remove virtual (#98) --- src/easyscience/Objects/virtual.py | 199 ----------------------- tests/unit_tests/Objects/test_Virtual.py | 99 ----------- 2 files changed, 298 deletions(-) delete mode 100644 src/easyscience/Objects/virtual.py delete mode 100644 tests/unit_tests/Objects/test_Virtual.py diff --git a/src/easyscience/Objects/virtual.py b/src/easyscience/Objects/virtual.py deleted file mode 100644 index 03fed121..00000000 --- a/src/easyscience/Objects/virtual.py +++ /dev/null @@ -1,199 +0,0 @@ -# SPDX-FileCopyrightText: 2022 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project - -from __future__ import annotations - -__author__ = 'github.com/wardsimon' -__version__ = '0.0.1' - -import inspect -import weakref -from copy import deepcopy -from typing import TYPE_CHECKING -from typing import Iterable -from typing import MutableSequence - -from easyscience import global_object -from easyscience.Constraints import ObjConstraint -from easyscience.Objects.variable.descriptor_base import DescriptorBase -from easyscience.Objects.variable.descriptor_bool import DescriptorBool -from easyscience.Objects.variable.descriptor_number import DescriptorNumber -from easyscience.Objects.variable.descriptor_str import DescriptorStr -from easyscience.Objects.variable.parameter import Constraints -from easyscience.Objects.variable.parameter import Parameter - -if TYPE_CHECKING: - from easyscience.Objects.ObjectClasses import BV - - -def raise_(ex): - raise ex - - -def _remover(a_obj_id: str, v_obj_id: str): - try: - # Try to get parent object (might be deleted) - a_obj = global_object.map.get_item_by_key(a_obj_id) - except ValueError: - return - if a_obj._constraints.virtual.get(v_obj_id, False): - del a_obj._constraints.virtual[v_obj_id] - - -def realizer(obj: BV): - """ - Convert component which is `Virtual` in a `Virtual` object into a `Real` component. - - :param obj: Virtual object which has the property `component` - """ - if getattr(obj, '_is_virtual', False): - klass = getattr(obj, '__non_virtual_class__') - - args = [] - if klass in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # is_variable check - kwargs = obj.encode_data() - kwargs['unique_name'] = None - return klass(**kwargs) - else: - kwargs = {name: realizer(item) for name, item in obj._kwargs.items()} - if isinstance(klass, Iterable) or issubclass(klass, MutableSequence): - for key, value in inspect.signature(klass).parameters.items(): - if value.kind in [ - inspect.Parameter.POSITIONAL_ONLY, - inspect.Parameter.POSITIONAL_OR_KEYWORD, - ]: - args.append(getattr(obj, key)) - return klass(*args, **kwargs) - else: - return obj - - -def component_realizer(obj: BV, component: str, recursive: bool = True): - """ - Convert component which is `Virtual` in a `Virtual` object into a `Real` component. - - :param obj: Virtual object which has the property `component` - :param component: The name of the component to be converted - :param recursive: Should we realize all sub-components of the component - """ - - done_mapping = 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 - else: - old_component = obj[component] - new_components = realizer(obj[component]) - idx = obj.index(old_component) - del obj[component] - obj.insert(idx, new_components) - done_mapping = False - if not recursive: - for key in iter(component): - if isinstance(key, str): - value = component._kwargs[key] - else: - value = key - key = value.unique_name - if getattr(value, '__old_class__', value.__class__) in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # noqa: E501 - continue - component._global_object.map.prune_vertex_from_edge(component, component._kwargs[key]) - component._global_object.map.add_edge(component, old_component._kwargs[key]) - component._kwargs[key] = old_component._kwargs[key] - done_mapping = False - if done_mapping: - obj._global_object.map.prune_vertex_from_edge(obj, old_component) - obj._global_object.map.add_edge(obj, new_components) - obj._kwargs[component] = new_components - - -def virtualizer(obj: BV) -> BV: - """ - Convert a real `EasyScience` object to a virtual object. - This means that the object returned is an exact copy which is unsettable, unchangeable - and linked to the parent object. - The object can be realized and returned as a copy via the `realizer` function. If you need a - component realized in place then `relalize_component` should be called. - - :param obj: - :type obj: - :return: - :rtype: - """ - # First check if we're already a virtual object - if getattr(obj, '_is_virtual', False): - new_obj = deepcopy(obj) - old_obj = obj._global_object.map.get_item_by_key(obj._derived_from) - constraint = ObjConstraint(new_obj, '', old_obj) - constraint.external = True - old_obj._constraints['virtual'][str(obj.unique_name)] = constraint - new_obj._constraints['builtin'] = dict() - # setattr(new_obj, "__previous_set", getattr(olobj, "__previous_set", None)) - weakref.finalize( - new_obj, - _remover, - old_obj.unique_name, - new_obj.unique_name, - ) - return new_obj - - # The supplied class - klass = getattr(obj, '__old_class__', obj.__class__) - virtual_options = { - '_is_virtual': True, - 'is_virtual': property(fget=lambda self: self._is_virtual), - '_derived_from': property(fget=lambda self: obj.unique_name), - '__non_virtual_class__': klass, - 'realize': realizer, - 'relalize_component': component_realizer, - } - - if klass in [DescriptorBool, DescriptorNumber, DescriptorStr, Parameter]: # is_variable check - virtual_options['fixed'] = property( - fget=lambda self: self._fixed, - fset=lambda self, value: raise_(AttributeError('Virtual parameters cannot be fixed')), - ) - # Generate a new class - cls = type('Virtual' + klass.__name__, (klass,), virtual_options) - # Determine what to do next. - args = [] - # If `obj` is a parameter or descriptor etc, then simple mods. - # if hasattr(obj, '_constructor'): - if isinstance(obj, DescriptorBase): - # All Variables are based on the Descriptor. - d = obj.encode_data() - if hasattr(d, 'fixed'): - d['fixed'] = True - d['unique_name'] = None - v_p = cls(**d) - v_p._enabled = False - constraint = ObjConstraint(v_p, '', obj) - constraint.external = True - obj._constraints.virtual[v_p.unique_name] = constraint - v_p._constraints = Constraints( - user=v_p._constraints.user, - builtin=dict(), # Set the new value for 'builtin' - virtual=v_p._constraints.virtual - ) - setattr(v_p, '__previous_set', getattr(obj, '__previous_set', None)) - weakref.finalize( - v_p, - _remover, - obj.unique_name, - v_p.unique_name, - ) - else: - # In this case, we need to be recursive. - kwargs = {name: virtualizer(item) for name, item in obj._kwargs.items()} - if isinstance(klass, Iterable) or issubclass(klass, MutableSequence): - for key, value in inspect.signature(cls).parameters.items(): - if value.kind in [ - inspect.Parameter.POSITIONAL_ONLY, - inspect.Parameter.POSITIONAL_OR_KEYWORD, - ]: - args.append(getattr(obj, key)) - v_p = cls(*args, **kwargs) - return v_p diff --git a/tests/unit_tests/Objects/test_Virtual.py b/tests/unit_tests/Objects/test_Virtual.py deleted file mode 100644 index 04d331b9..00000000 --- a/tests/unit_tests/Objects/test_Virtual.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project Date: Wed, 22 Jan 2025 12:59:28 +0100 Subject: [PATCH 04/48] make descriptor_array --- src/easyscience/Objects/variable/descriptor_array.py | 0 tests/unit_tests/Objects/variable/test_descriptor_array.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/easyscience/Objects/variable/descriptor_array.py create mode 100644 tests/unit_tests/Objects/variable/test_descriptor_array.py diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py new file mode 100644 index 00000000..e69de29b From 69cd6bdf6782fb2ed505e248cdf0a16873d5c818 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:34:17 +0100 Subject: [PATCH 05/48] update from_scipp method --- .../Objects/variable/descriptor_array.py | 431 ++++++++++++++++++ 1 file changed, 431 insertions(+) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index e69de29b..550a25e3 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import numbers +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Union + +import numpy as np +import scipp as sc +from scipp import UnitError +from scipp import Variable + +from easyscience.global_object.undo_redo import PropertyStack +from easyscience.global_object.undo_redo import property_stack_deco + +from .descriptor_base import DescriptorBase + + +class DescriptorArray(DescriptorBase): + """ + A `Descriptor` for Array values with units. The internal representation is a scipp array. + """ + + def __init__( + self, + name: str, + value: numbers.Number, + unit: Optional[Union[str, sc.Unit]] = '', + variance: Optional[numbers.Number] = None, + unique_name: Optional[str] = None, + description: Optional[str] = None, + url: Optional[str] = None, + display_name: Optional[str] = None, + parent: Optional[Any] = None, + ): + """Constructor for the DescriptorArray 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 + .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. + """ + # if not isinstance(value, numbers.Number) or isinstance(value, bool): + # raise TypeError(f'{value=} must be a number') + # if variance is not None: + # if not isinstance(variance, numbers.Number) or isinstance(variance, bool): + # raise TypeError(f'{variance=} must be a number or None') + # if variance < 0: + # raise ValueError(f'{variance=} must be positive') + # variance = float(variance) + # if not isinstance(unit, sc.Unit) and not isinstance(unit, str): + # raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') + # try: + # self._scalar = sc.scalar(float(value), unit=unit, variance=variance) + # except Exception as message: + # raise UnitError(message) + + self._array = sc.array(dims=['row','column'],values=float(value), unit=unit, variance=variance) + + super().__init__( + name=name, + unique_name=unique_name, + description=description, + url=url, + display_name=display_name, + parent=parent, + ) + + # 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()) + + @classmethod + def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorArray: + """ + Create a DescriptorArray from a scipp array. + + :param name: Name of the descriptor + :param full_value: Value of the descriptor as a scipp variable + :param kwargs: Additional parameters for the descriptor + :return: DescriptorArray + """ + if not isinstance(full_value, Variable): + raise TypeError(f'{full_value=} must be a scipp array') + if len(full_value.dims) != 0: + raise TypeError(f'{full_value=} must be a scipp array') + return cls(name=name, value=full_value.values, unit=full_value.unit, variance=full_value.variances, **kwargs) + + @property + def full_value(self) -> Variable: + """ + Get the value of self as a scipp scalar. This is should be usable for most cases. + + :return: Value of self with unit. + """ + return self._scalar + + @full_value.setter + def full_value(self, full_value: Variable) -> None: + raise AttributeError( + f'Full_value is read-only. Change the value and variance seperately. Or create a new {self.__class__.__name__}.' + ) + + @property + def value(self) -> numbers.Number: + """ + Get the value. This should be usable for most cases. The full value can be obtained from `obj.full_value`. + + :return: Value of self with unit. + """ + return self._scalar.value + + @value.setter + @property_stack_deco + 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`. + + :param value: New value of self + """ + if not isinstance(value, numbers.Number) or isinstance(value, bool): + raise TypeError(f'{value=} must be a number') + self._scalar.value = float(value) + + @property + def unit(self) -> str: + """ + Get the unit. + + :return: Unit as a string. + """ + return str(self._scalar.unit) + + @unit.setter + def unit(self, unit_str: str) -> None: + raise AttributeError( + ( + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' + ) + ) # noqa: E501 + + @property + def variance(self) -> float: + """ + Get the variance. + + :return: variance. + """ + return self._scalar.variance + + @variance.setter + @property_stack_deco + def variance(self, variance_float: float) -> None: + """ + Set the variance. + + :param variance_float: Variance as a float + """ + if variance_float is not None: + if not isinstance(variance_float, numbers.Number): + raise TypeError(f'{variance_float=} must be a number or None') + if variance_float < 0: + raise ValueError(f'{variance_float=} must be positive') + variance_float = float(variance_float) + self._scalar.variance = variance_float + + @property + def error(self) -> float: + """ + The standard deviation for the parameter. + + :return: Error associated with parameter + """ + if self._scalar.variance is None: + return None + return float(np.sqrt(self._scalar.variance)) + + @error.setter + @property_stack_deco + def error(self, value: float) -> None: + """ + Set the standard deviation for the parameter. + + :param value: New error value + """ + if value is not None: + if not isinstance(value, numbers.Number): + raise TypeError(f'{value=} must be a number or None') + if value < 0: + raise ValueError(f'{value=} must be positive') + value = float(value) + self._scalar.variance = value**2 + else: + self._scalar.variance = None + + 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 + """ + if not isinstance(unit_str, str): + raise TypeError(f'{unit_str=} must be a string representing a valid scipp unit') + try: + new_unit = sc.Unit(unit_str) + except UnitError as message: + raise UnitError(message) from None + + # Save the current state for undo/redo + old_scalar = self._scalar + + # Perform the unit conversion + try: + new_scalar = self._scalar.to(unit=new_unit) + except Exception as e: + raise UnitError(f"Failed to convert unit: {e}") from e + + # Define the setter function for the undo stack + def set_scalar(obj, scalar): + obj._scalar = scalar + + # Push to undo stack + self._global_object.stack.push( + PropertyStack(self, set_scalar, old_scalar, new_scalar, text=f"Convert unit to {unit_str}") + ) + + # Update the scalar + self._scalar = new_scalar + + + # Just to get return type right + def __copy__(self) -> DescriptorArray: + return super().__copy__() + + def __repr__(self) -> str: + """Return printable representation.""" + string = '<' + string += self.__class__.__name__ + ' ' + string += f"'{self._name}': " + string += f'{self._scalar.value:.4f}' + if self.variance: + string += f' \u00b1 {self.error:.4f}' + obj_unit = self._scalar.unit + if obj_unit == 'dimensionless': + obj_unit = '' + else: + obj_unit = f' {obj_unit}' + string += obj_unit + string += '>' + return string + # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" + + def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + raw_dict = super().as_dict(skip=skip) + raw_dict['value'] = self._scalar.value + raw_dict['unit'] = str(self._scalar.unit) + raw_dict['variance'] = self._scalar.variance + return raw_dict + + def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError('Numbers can only be added to dimensionless values') + new_value = self.full_value + other + elif type(other) is DescriptorArray: + original_unit = other.unit + try: + 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) + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __radd__(self, other: numbers.Number) -> DescriptorArray: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError('Numbers can only be added to dimensionless values') + new_value = other + self.full_value + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __sub__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError('Numbers can only be subtracted from dimensionless values') + new_value = self.full_value - other + elif type(other) is DescriptorArray: + original_unit = other.unit + try: + 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) + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __rsub__(self, other: numbers.Number) -> DescriptorArray: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError('Numbers can only be subtracted from dimensionless values') + new_value = other - self.full_value + else: + return NotImplemented + descriptor = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor.name = descriptor.unique_name + return descriptor + + def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + if isinstance(other, numbers.Number): + new_value = self.full_value * other + elif type(other) is DescriptorArray: + new_value = self.full_value * other.full_value + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __rmul__(self, other: numbers.Number) -> DescriptorArray: + if isinstance(other, numbers.Number): + new_value = other * self.full_value + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __truediv__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + if isinstance(other, numbers.Number): + original_other = other + if other == 0: + raise ZeroDivisionError('Cannot divide by zero') + new_value = self.full_value / other + elif type(other) is DescriptorArray: + original_other = other.value + if original_other == 0: + raise ZeroDivisionError('Cannot divide by zero') + new_value = self.full_value / other.full_value + other.value = original_other + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __rtruediv__(self, other: numbers.Number) -> DescriptorArray: + if isinstance(other, numbers.Number): + if self.value == 0: + raise ZeroDivisionError('Cannot divide by zero') + new_value = other / self.full_value + else: + return NotImplemented + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __pow__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + if isinstance(other, numbers.Number): + exponent = other + elif type(other) is DescriptorArray: + if other.unit != 'dimensionless': + raise UnitError('Exponents must be dimensionless') + if other.variance is not None: + raise ValueError('Exponents must not have variance') + exponent = other.value + else: + return NotImplemented + try: + new_value = self.full_value**exponent + except Exception as message: + raise message from None + if np.isnan(new_value.value): + raise ValueError('The result of the exponentiation is not a number') + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __rpow__(self, other: numbers.Number) -> numbers.Number: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError('Exponents must be dimensionless') + if self.variance is not None: + raise ValueError('Exponents must not have variance') + new_value = other**self.value + else: + return NotImplemented + return new_value + + def __neg__(self) -> DescriptorArray: + new_value = -self.full_value + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __abs__(self) -> DescriptorArray: + new_value = abs(self.full_value) + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def _base_unit(self) -> str: + string = str(self._scalar.unit) + for i, letter in enumerate(string): + if letter == 'e': + if string[i : i + 2] not in ['e+', 'e-']: + return string[i:] + elif letter not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-']: + return string[i:] + return '' From 4b513825d0a8adae3ec80be7e0f55860bb6eac46 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:37:00 +0100 Subject: [PATCH 06/48] Update value (not finished) --- .../Objects/variable/descriptor_array.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 550a25e3..6c9703df 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -96,11 +96,11 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorArra @property def full_value(self) -> Variable: """ - Get the value of self as a scipp scalar. This is should be usable for most cases. + Get the value of self as a scipp array. This is should be usable for most cases. :return: Value of self with unit. """ - return self._scalar + return self._array @full_value.setter def full_value(self, full_value: Variable) -> None: @@ -115,19 +115,19 @@ def value(self) -> numbers.Number: :return: Value of self with unit. """ - return self._scalar.value + return self._array.values @value.setter @property_stack_deco - def value(self, value: numbers.Number) -> None: + def value(self, value: numbers.Number) -> None: #TODO: Update typing """ Set the value of self. This should be usable for most cases. The full value can be obtained from `obj.full_value`. :param value: New value of self """ - if not isinstance(value, numbers.Number) or isinstance(value, bool): - raise TypeError(f'{value=} must be a number') - self._scalar.value = float(value) + # if not isinstance(value, numbers.Number) or isinstance(value, bool): #TODO: add check if it's an array, possibly a numpy array + # raise TypeError(f'{value=} must be a number') + self._scalar.values = float(value) @property def unit(self) -> str: From 755fc91d348061579fa133f4dc4813e7544f56f0 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:37:31 +0100 Subject: [PATCH 07/48] change _scalar to _array --- .../Objects/variable/descriptor_array.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 6c9703df..71197366 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -58,7 +58,7 @@ def __init__( # if not isinstance(unit, sc.Unit) and not isinstance(unit, str): # raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') # try: - # self._scalar = sc.scalar(float(value), unit=unit, variance=variance) + # self._array = sc.scalar(float(value), unit=unit, variance=variance) # except Exception as message: # raise UnitError(message) @@ -127,7 +127,7 @@ def value(self, value: numbers.Number) -> None: #TODO: Update typing """ # if not isinstance(value, numbers.Number) or isinstance(value, bool): #TODO: add check if it's an array, possibly a numpy array # raise TypeError(f'{value=} must be a number') - self._scalar.values = float(value) + self._array.values = float(value) @property def unit(self) -> str: @@ -136,7 +136,7 @@ def unit(self) -> str: :return: Unit as a string. """ - return str(self._scalar.unit) + return str(self._array.unit) @unit.setter def unit(self, unit_str: str) -> None: @@ -154,7 +154,7 @@ def variance(self) -> float: :return: variance. """ - return self._scalar.variance + return self._array.variance @variance.setter @property_stack_deco @@ -170,7 +170,7 @@ def variance(self, variance_float: float) -> None: if variance_float < 0: raise ValueError(f'{variance_float=} must be positive') variance_float = float(variance_float) - self._scalar.variance = variance_float + self._array.variance = variance_float @property def error(self) -> float: @@ -179,9 +179,9 @@ def error(self) -> float: :return: Error associated with parameter """ - if self._scalar.variance is None: + if self._array.variance is None: return None - return float(np.sqrt(self._scalar.variance)) + return float(np.sqrt(self._array.variance)) @error.setter @property_stack_deco @@ -197,9 +197,9 @@ def error(self, value: float) -> None: if value < 0: raise ValueError(f'{value=} must be positive') value = float(value) - self._scalar.variance = value**2 + self._array.variance = value**2 else: - self._scalar.variance = None + self._array.variance = None def convert_unit(self, unit_str: str) -> None: """ @@ -215,25 +215,25 @@ def convert_unit(self, unit_str: str) -> None: raise UnitError(message) from None # Save the current state for undo/redo - old_scalar = self._scalar + old_array = self._array # Perform the unit conversion try: - new_scalar = self._scalar.to(unit=new_unit) + new_array = self._array.to(unit=new_unit) except Exception as e: raise UnitError(f"Failed to convert unit: {e}") from e # Define the setter function for the undo stack - def set_scalar(obj, scalar): - obj._scalar = scalar + def set_array(obj, scalar): + obj._array = scalar # Push to undo stack self._global_object.stack.push( - PropertyStack(self, set_scalar, old_scalar, new_scalar, text=f"Convert unit to {unit_str}") + PropertyStack(self, set_array, old_array, new_array, text=f"Convert unit to {unit_str}") ) # Update the scalar - self._scalar = new_scalar + self._array = new_array # Just to get return type right @@ -245,10 +245,10 @@ def __repr__(self) -> str: string = '<' string += self.__class__.__name__ + ' ' string += f"'{self._name}': " - string += f'{self._scalar.value:.4f}' + string += f'{self._array.value:.4f}' if self.variance: string += f' \u00b1 {self.error:.4f}' - obj_unit = self._scalar.unit + obj_unit = self._array.unit if obj_unit == 'dimensionless': obj_unit = '' else: @@ -260,9 +260,9 @@ def __repr__(self) -> str: def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict = super().as_dict(skip=skip) - raw_dict['value'] = self._scalar.value - raw_dict['unit'] = str(self._scalar.unit) - raw_dict['variance'] = self._scalar.variance + raw_dict['value'] = self._array.value + raw_dict['unit'] = str(self._array.unit) + raw_dict['variance'] = self._array.variance return raw_dict def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: @@ -421,7 +421,7 @@ def __abs__(self) -> DescriptorArray: return descriptor_number def _base_unit(self) -> str: - string = str(self._scalar.unit) + string = str(self._array.unit) for i, letter in enumerate(string): if letter == 'e': if string[i : i + 2] not in ['e+', 'e-']: From 3073d7860f93c0be5cd2a5fbc9cde3a71a3852ed Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:39:51 +0100 Subject: [PATCH 08/48] outcomment error for now --- .../Objects/variable/descriptor_array.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 71197366..3d1ff7fb 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -164,42 +164,43 @@ def variance(self, variance_float: float) -> None: :param variance_float: Variance as a float """ + #TODO:update chekcs of variance if variance_float is not None: if not isinstance(variance_float, numbers.Number): raise TypeError(f'{variance_float=} must be a number or None') if variance_float < 0: raise ValueError(f'{variance_float=} must be positive') variance_float = float(variance_float) - self._array.variance = variance_float - - @property - def error(self) -> float: - """ - The standard deviation for the parameter. - - :return: Error associated with parameter - """ - if self._array.variance is None: - return None - return float(np.sqrt(self._array.variance)) - - @error.setter - @property_stack_deco - def error(self, value: float) -> None: - """ - Set the standard deviation for the parameter. - - :param value: New error value - """ - if value is not None: - if not isinstance(value, numbers.Number): - raise TypeError(f'{value=} must be a number or None') - if value < 0: - raise ValueError(f'{value=} must be positive') - value = float(value) - self._array.variance = value**2 - else: - self._array.variance = None + self._array.variances = variance_float + + # @property #TODO: do we even want errors on arrays? + # def error(self) -> float: + # """ + # The standard deviation for the parameter. + + # :return: Error associated with parameter + # """ + # if self._array.variances is None: + # return None + # return float(np.sqrt(self._array.variances)) + + # @error.setter + # @property_stack_deco + # def error(self, value: float) -> None: + # """ + # Set the standard deviation for the parameter. + + # :param value: New error value + # """ + # if value is not None: + # if not isinstance(value, numbers.Number): + # raise TypeError(f'{value=} must be a number or None') + # if value < 0: + # raise ValueError(f'{value=} must be positive') + # value = float(value) + # self._array.variances = value**2 #TODO: check if this works for lists + # else: + # self._array.variance = None def convert_unit(self, unit_str: str) -> None: """ From 4921185aa1a50d3d83e874bc08230437446273f2 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:42:07 +0100 Subject: [PATCH 09/48] outcommented add, radd etc. --- .../Objects/variable/descriptor_array.py | 348 +++++++++--------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 3d1ff7fb..eb4e96ea 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -233,7 +233,7 @@ def set_array(obj, scalar): PropertyStack(self, set_array, old_array, new_array, text=f"Convert unit to {unit_str}") ) - # Update the scalar + # Update the array self._array = new_array @@ -241,185 +241,185 @@ def set_array(obj, scalar): def __copy__(self) -> DescriptorArray: return super().__copy__() - def __repr__(self) -> str: - """Return printable representation.""" - string = '<' - string += self.__class__.__name__ + ' ' - string += f"'{self._name}': " - string += f'{self._array.value:.4f}' - if self.variance: - string += f' \u00b1 {self.error:.4f}' - obj_unit = self._array.unit - if obj_unit == 'dimensionless': - obj_unit = '' - else: - obj_unit = f' {obj_unit}' - string += obj_unit - string += '>' - return string - # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" + # def __repr__(self) -> str: #TODO: update __repr__ + # """Return printable representation.""" + # string = '<' + # string += self.__class__.__name__ + ' ' + # string += f"'{self._name}': " + # string += f'{self._array.values:.4f}' + # if self.variance: + # string += f' \u00b1 {self.error:.4f}' + # obj_unit = self._array.unit + # if obj_unit == 'dimensionless': + # obj_unit = '' + # else: + # obj_unit = f' {obj_unit}' + # string += obj_unit + # string += '>' + # return string + # # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict = super().as_dict(skip=skip) - raw_dict['value'] = self._array.value + raw_dict['value'] = self._array.values raw_dict['unit'] = str(self._array.unit) - raw_dict['variance'] = self._array.variance + raw_dict['variance'] = self._array.variances return raw_dict - def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - if isinstance(other, numbers.Number): - if self.unit != 'dimensionless': - raise UnitError('Numbers can only be added to dimensionless values') - new_value = self.full_value + other - elif type(other) is DescriptorArray: - original_unit = other.unit - try: - 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) - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __radd__(self, other: numbers.Number) -> DescriptorArray: - if isinstance(other, numbers.Number): - if self.unit != 'dimensionless': - raise UnitError('Numbers can only be added to dimensionless values') - new_value = other + self.full_value - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __sub__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - if isinstance(other, numbers.Number): - if self.unit != 'dimensionless': - raise UnitError('Numbers can only be subtracted from dimensionless values') - new_value = self.full_value - other - elif type(other) is DescriptorArray: - original_unit = other.unit - try: - 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) - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __rsub__(self, other: numbers.Number) -> DescriptorArray: - if isinstance(other, numbers.Number): - if self.unit != 'dimensionless': - raise UnitError('Numbers can only be subtracted from dimensionless values') - new_value = other - self.full_value - else: - return NotImplemented - descriptor = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor.name = descriptor.unique_name - return descriptor - - def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - if isinstance(other, numbers.Number): - new_value = self.full_value * other - elif type(other) is DescriptorArray: - new_value = self.full_value * other.full_value - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.convert_unit(descriptor_number._base_unit()) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __rmul__(self, other: numbers.Number) -> DescriptorArray: - if isinstance(other, numbers.Number): - new_value = other * self.full_value - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __truediv__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - if isinstance(other, numbers.Number): - original_other = other - if other == 0: - raise ZeroDivisionError('Cannot divide by zero') - new_value = self.full_value / other - elif type(other) is DescriptorArray: - original_other = other.value - if original_other == 0: - raise ZeroDivisionError('Cannot divide by zero') - new_value = self.full_value / other.full_value - other.value = original_other - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.convert_unit(descriptor_number._base_unit()) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __rtruediv__(self, other: numbers.Number) -> DescriptorArray: - if isinstance(other, numbers.Number): - if self.value == 0: - raise ZeroDivisionError('Cannot divide by zero') - new_value = other / self.full_value - else: - return NotImplemented - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __pow__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - if isinstance(other, numbers.Number): - exponent = other - elif type(other) is DescriptorArray: - if other.unit != 'dimensionless': - raise UnitError('Exponents must be dimensionless') - if other.variance is not None: - raise ValueError('Exponents must not have variance') - exponent = other.value - else: - return NotImplemented - try: - new_value = self.full_value**exponent - except Exception as message: - raise message from None - if np.isnan(new_value.value): - raise ValueError('The result of the exponentiation is not a number') - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __rpow__(self, other: numbers.Number) -> numbers.Number: - if isinstance(other, numbers.Number): - if self.unit != 'dimensionless': - raise UnitError('Exponents must be dimensionless') - if self.variance is not None: - raise ValueError('Exponents must not have variance') - new_value = other**self.value - else: - return NotImplemented - return new_value - - def __neg__(self) -> DescriptorArray: - new_value = -self.full_value - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number - - def __abs__(self) -> DescriptorArray: - new_value = abs(self.full_value) - descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - descriptor_number.name = descriptor_number.unique_name - return descriptor_number + # def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: TODO: update all of these + # if isinstance(other, numbers.Number): + # if self.unit != 'dimensionless': + # raise UnitError('Numbers can only be added to dimensionless values') + # new_value = self.full_value + other + # elif type(other) is DescriptorArray: + # original_unit = other.unit + # try: + # 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) + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __radd__(self, other: numbers.Number) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # if self.unit != 'dimensionless': + # raise UnitError('Numbers can only be added to dimensionless values') + # new_value = other + self.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __sub__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # if self.unit != 'dimensionless': + # raise UnitError('Numbers can only be subtracted from dimensionless values') + # new_value = self.full_value - other + # elif type(other) is DescriptorArray: + # original_unit = other.unit + # try: + # 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) + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __rsub__(self, other: numbers.Number) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # if self.unit != 'dimensionless': + # raise UnitError('Numbers can only be subtracted from dimensionless values') + # new_value = other - self.full_value + # else: + # return NotImplemented + # descriptor = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor.name = descriptor.unique_name + # return descriptor + + # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # new_value = self.full_value * other + # elif type(other) is DescriptorArray: + # new_value = self.full_value * other.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.convert_unit(descriptor_number._base_unit()) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __rmul__(self, other: numbers.Number) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # new_value = other * self.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __truediv__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # original_other = other + # if other == 0: + # raise ZeroDivisionError('Cannot divide by zero') + # new_value = self.full_value / other + # elif type(other) is DescriptorArray: + # original_other = other.value + # if original_other == 0: + # raise ZeroDivisionError('Cannot divide by zero') + # new_value = self.full_value / other.full_value + # other.value = original_other + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.convert_unit(descriptor_number._base_unit()) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __rtruediv__(self, other: numbers.Number) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # if self.value == 0: + # raise ZeroDivisionError('Cannot divide by zero') + # new_value = other / self.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __pow__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # exponent = other + # elif type(other) is DescriptorArray: + # if other.unit != 'dimensionless': + # raise UnitError('Exponents must be dimensionless') + # if other.variance is not None: + # raise ValueError('Exponents must not have variance') + # exponent = other.value + # else: + # return NotImplemented + # try: + # new_value = self.full_value**exponent + # except Exception as message: + # raise message from None + # if np.isnan(new_value.value): + # raise ValueError('The result of the exponentiation is not a number') + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __rpow__(self, other: numbers.Number) -> numbers.Number: + # if isinstance(other, numbers.Number): + # if self.unit != 'dimensionless': + # raise UnitError('Exponents must be dimensionless') + # if self.variance is not None: + # raise ValueError('Exponents must not have variance') + # new_value = other**self.value + # else: + # return NotImplemented + # return new_value + + # def __neg__(self) -> DescriptorArray: + # new_value = -self.full_value + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __abs__(self) -> DescriptorArray: + # new_value = abs(self.full_value) + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number def _base_unit(self) -> str: string = str(self._array.unit) From 865c2daeae8823e98af4fb0413bdeee1c9e6ac0b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:54:47 +0100 Subject: [PATCH 10/48] identify TODOs --- .../Objects/variable/descriptor_array.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index eb4e96ea..b24156a4 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -55,14 +55,16 @@ def __init__( # if variance < 0: # raise ValueError(f'{variance=} must be positive') # variance = float(variance) - # if not isinstance(unit, sc.Unit) and not isinstance(unit, str): - # raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') + if not isinstance(unit, sc.Unit) and not isinstance(unit, str): + raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') # try: # self._array = sc.scalar(float(value), unit=unit, variance=variance) # except Exception as message: # raise UnitError(message) - self._array = sc.array(dims=['row','column'],values=float(value), unit=unit, variance=variance) + + #TODO:do check needed like in the above outcommented code, but update for arrays as in the following line + self._array = sc.array(dims=['row','column'],values=value, unit=unit, variances=variance) super().__init__( name=name, @@ -164,7 +166,7 @@ def variance(self, variance_float: float) -> None: :param variance_float: Variance as a float """ - #TODO:update chekcs of variance + #TODO:update chekcs of variance to check for an array if variance_float is not None: if not isinstance(variance_float, numbers.Number): raise TypeError(f'{variance_float=} must be a number or None') @@ -173,7 +175,7 @@ def variance(self, variance_float: float) -> None: variance_float = float(variance_float) self._array.variances = variance_float - # @property #TODO: do we even want errors on arrays? + # @property #TODO: update to handle arrays # def error(self) -> float: # """ # The standard deviation for the parameter. @@ -241,23 +243,23 @@ def set_array(obj, scalar): def __copy__(self) -> DescriptorArray: return super().__copy__() - # def __repr__(self) -> str: #TODO: update __repr__ - # """Return printable representation.""" - # string = '<' - # string += self.__class__.__name__ + ' ' - # string += f"'{self._name}': " - # string += f'{self._array.values:.4f}' - # if self.variance: - # string += f' \u00b1 {self.error:.4f}' - # obj_unit = self._array.unit - # if obj_unit == 'dimensionless': - # obj_unit = '' - # else: - # obj_unit = f' {obj_unit}' - # string += obj_unit - # string += '>' - # return string - # # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" + def __repr__(self) -> str: #TODO: update __repr__ to give the content of the array, inspired by this code + """Return printable representation.""" + string = '<' + string += self.__class__.__name__ + ' ' + string += f"'{self._name}': " + # string += f'{self._array.values:.4f}' + # if self.variance: + # string += f' \u00b1 {self.error:.4f}' + obj_unit = self._array.unit + if obj_unit == 'dimensionless': + obj_unit = '' + else: + obj_unit = f' {obj_unit}' + string += obj_unit + string += '>' + return string + # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict = super().as_dict(skip=skip) @@ -266,6 +268,7 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict + # TODO: add matrix multiplication and division using numpy. # def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: TODO: update all of these # if isinstance(other, numbers.Number): # if self.unit != 'dimensionless': From 527ae1d138b67bbe10e2a65405c9c566cf84da28 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 13:58:45 +0100 Subject: [PATCH 11/48] Update __init__ --- .../Objects/variable/descriptor_array.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index b24156a4..031ab25f 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -47,23 +47,30 @@ def __init__( param parent: Parent of the descriptor .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ - # if not isinstance(value, numbers.Number) or isinstance(value, bool): - # raise TypeError(f'{value=} must be a number') - # if variance is not None: - # if not isinstance(variance, numbers.Number) or isinstance(variance, bool): - # raise TypeError(f'{variance=} must be a number or None') - # if variance < 0: - # raise ValueError(f'{variance=} must be positive') - # variance = float(variance) + + if not isinstance(value, (list, np.ndarray)): + raise TypeError(f"{value=} must be a list or numpy array.") + if isinstance(value, list): + value = np.array(value) # Convert to numpy array for consistent handling. + + if variance is not None: + if not isinstance(variance, (list, np.ndarray)): + raise TypeError(f"{variance=} must be a list or numpy array if provided.") + if isinstance(variance, list): + variance = np.array(variance) # Convert to numpy array for consistent handling. + if variance.shape != value.shape: + raise ValueError(f"{variance=} must have the same shape as {value=}.") + if not np.all(variance >= 0): + raise ValueError(f"{variance=} must only contain non-negative values.") + if not isinstance(unit, sc.Unit) and not isinstance(unit, str): raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') - # try: - # self._array = sc.scalar(float(value), unit=unit, variance=variance) - # except Exception as message: - # raise UnitError(message) - - #TODO:do check needed like in the above outcommented code, but update for arrays as in the following line + try: + self._array = sc.array(dims=['row', 'column'], values=value, unit=unit, variances=variance) + except Exception as message: + raise UnitError(message) + self._array = sc.array(dims=['row','column'],values=value, unit=unit, variances=variance) super().__init__( From 8619a1ee3ea25a3e5c9bdd62e1bb4a12ccbc90d7 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 14:07:39 +0100 Subject: [PATCH 12/48] update variance --- .../Objects/variable/descriptor_array.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 031ab25f..57e5dcf6 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -128,15 +128,22 @@ def value(self) -> numbers.Number: @value.setter @property_stack_deco - def value(self, value: numbers.Number) -> None: #TODO: Update typing + def value(self, value: Union[list, np.ndarray]) -> None: """ - Set the value of self. This should be usable for most cases. The full value can be obtained from `obj.full_value`. + Set the value of self. Ensures the input is an array and matches the shape of the existing array. + The full value can be obtained from `obj.full_value`. - :param value: New value of self + :param value: New value for the DescriptorArray, must be a list or numpy array. """ - # if not isinstance(value, numbers.Number) or isinstance(value, bool): #TODO: add check if it's an array, possibly a numpy array - # raise TypeError(f'{value=} must be a number') - self._array.values = float(value) + if not isinstance(value, (list, np.ndarray)): + raise TypeError(f"{value=} must be a list or numpy array.") + if isinstance(value, list): + value = np.array(value) # Convert lists to numpy arrays for consistent handling. + + if value.shape != self._array.values.shape: + raise ValueError(f"{value=} must have the same shape as the existing array values.") + + self._array.values = value @property def unit(self) -> str: @@ -167,20 +174,26 @@ def variance(self) -> float: @variance.setter @property_stack_deco - def variance(self, variance_float: float) -> None: + def variance(self, variance: Union[list, np.ndarray]) -> None: """ - Set the variance. + Set the variance of self. Ensures the input is an array and matches the shape of the existing values. - :param variance_float: Variance as a float + :param variance: New variance for the DescriptorArray, must be a list or numpy array. """ - #TODO:update chekcs of variance to check for an array - if variance_float is not None: - if not isinstance(variance_float, numbers.Number): - raise TypeError(f'{variance_float=} must be a number or None') - if variance_float < 0: - raise ValueError(f'{variance_float=} must be positive') - variance_float = float(variance_float) - self._array.variances = variance_float + if variance is not None: + if not isinstance(variance, (list, np.ndarray)): + raise TypeError(f"{variance=} must be a list or numpy array.") + if isinstance(variance, list): + variance = np.array(variance) # Convert lists to numpy arrays for consistent handling. + + if variance.shape != self._array.values.shape: + raise ValueError(f"{variance=} must have the same shape as the array values.") + + if not np.all(variance >= 0): + raise ValueError(f"{variance=} must only contain non-negative values.") + + self._array.variances = variance + # @property #TODO: update to handle arrays # def error(self) -> float: From 710a555c3c27871e31ce37e57992c401fd480fef Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 14:12:12 +0100 Subject: [PATCH 13/48] update error --- .../Objects/variable/descriptor_array.py | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 57e5dcf6..85e7e1c4 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -194,35 +194,42 @@ def variance(self, variance: Union[list, np.ndarray]) -> None: self._array.variances = variance + @property + def error(self) -> Optional[np.ndarray]: + """ + The standard deviations , calculated as the square root of variances. - # @property #TODO: update to handle arrays - # def error(self) -> float: - # """ - # The standard deviation for the parameter. - - # :return: Error associated with parameter - # """ - # if self._array.variances is None: - # return None - # return float(np.sqrt(self._array.variances)) - - # @error.setter - # @property_stack_deco - # def error(self, value: float) -> None: - # """ - # Set the standard deviation for the parameter. - - # :param value: New error value - # """ - # if value is not None: - # if not isinstance(value, numbers.Number): - # raise TypeError(f'{value=} must be a number or None') - # if value < 0: - # raise ValueError(f'{value=} must be positive') - # value = float(value) - # self._array.variances = value**2 #TODO: check if this works for lists - # else: - # self._array.variance = None + :return: A numpy array of standard deviations, or None if variances are not set. + """ + if self._array.variances is None: + return None + return np.sqrt(self._array.variances) + + @error.setter + @property_stack_deco + def error(self, error: Union[list, np.ndarray]) -> None: + """ + Set the standard deviation for the parameter, which updates the variances. + + :param error: A list or numpy array of standard deviations. + """ + if error is not None: + if not isinstance(error, (list, np.ndarray)): + raise TypeError(f"{error=} must be a list or numpy array.") + if isinstance(error, list): + error = np.array(error) # Convert lists to numpy arrays for consistent handling. + + if error.shape != self._array.values.shape: + raise ValueError(f"{error=} must have the same shape as the array values.") + + if not np.all(error >= 0): + raise ValueError(f"{error=} must only contain non-negative values.") + + # Update variances as the square of the errors + self._array.variances = error**2 + else: + self._array.variances = None + def convert_unit(self, unit_str: str) -> None: """ From bd6d12d368f99205d3102e89c79cf89da8359fcf Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 14:24:36 +0100 Subject: [PATCH 14/48] add repr --- .../Objects/variable/descriptor_array.py | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 85e7e1c4..3711bc7d 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -270,23 +270,40 @@ def set_array(obj, scalar): def __copy__(self) -> DescriptorArray: return super().__copy__() - def __repr__(self) -> str: #TODO: update __repr__ to give the content of the array, inspired by this code - """Return printable representation.""" - string = '<' - string += self.__class__.__name__ + ' ' - string += f"'{self._name}': " - # string += f'{self._array.values:.4f}' - # if self.variance: - # string += f' \u00b1 {self.error:.4f}' - obj_unit = self._array.unit - if obj_unit == 'dimensionless': - obj_unit = '' - else: - obj_unit = f' {obj_unit}' - string += obj_unit - string += '>' - return string - # return f"<{class_name} '{obj_name}': {obj_value:0.04f}{obj_unit}>" + def __repr__(self) -> str: + """ + Return a string representation of the DescriptorArray, showing its name, value, variance, and unit. + Large arrays are summarized for brevity. + """ + # Base string with name + string = f"<{self.__class__.__name__} '{self._name}': " + + # Summarize array values + values_summary = np.array2string( + self._array.values, + precision=4, + threshold=10, # Show full array if <=10 elements, else summarize + edgeitems=3, # Show first and last 3 elements for large arrays + ) + string += f"values={values_summary}" + + # Add errors if they exists + if self._array.variances is not None: + errors_summary = np.array2string( + self.error, + precision=4, + threshold=10, + edgeitems=3, + ) + string += f", errors={errors_summary}" + + # Add unit + obj_unit = str(self._array.unit) + if obj_unit and obj_unit != "dimensionless": + string += f", unit={obj_unit}" + + string += ">" + return string def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict = super().as_dict(skip=skip) @@ -295,8 +312,13 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict - # TODO: add matrix multiplication and division using numpy. - # def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: TODO: update all of these +# TODO: add arithmetic operations +# They should be allowed between DescriptorArray and numbers, and between DescriptorArray and DescriptorArray. +# The result should be a new DescriptorArray with the same unit as the first argument. + + + + # def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: # if isinstance(other, numbers.Number): # if self.unit != 'dimensionless': # raise UnitError('Numbers can only be added to dimensionless values') @@ -460,3 +482,7 @@ def _base_unit(self) -> str: elif letter not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-']: return string[i:] return '' + + + + # TODO: add matrix multiplication and division using numpy. From 7287ebdc1ad8b688be2400570fd64012afd530af Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 22 Jan 2025 15:06:42 +0100 Subject: [PATCH 15/48] add __add__ --- .../Objects/variable/descriptor_array.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 3711bc7d..3a419075 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -312,6 +312,77 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict + + def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise addition with another DescriptorArray, numpy array, list, or number. + + :param other: The object to add. Must be a DescriptorArray with compatible units, + or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the addition. + """ + if isinstance(other, numbers.Number): + if self.unit not in [None, "dimensionless"]: + raise UnitError("Numbers can only be added to dimensionless values") + new_full_value = self.full_value + other # scipp can handle addition with numbers + + elif isinstance(other, (list, np.ndarray)): + if self.unit not in [None, "dimensionless"]: + raise UnitError("Addition with numpy arrays or lists is only allowed for dimensionless values") + + # Convert `other` to numpy array if it's a list + if isinstance(other, list): + other = np.array(other) + + # Ensure dimensions match + if other.shape != self._array.values.shape: + raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") + + new_value = self._array.values + other + new_full_value=sc.array(dims=['row','column'],values=new_value,unit=self.unit,variances=self._array.variances) + + elif isinstance(other, DescriptorArray): + original_unit = other.unit + try: + other.convert_unit(self.unit) + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + + # Ensure dimensions match + if self.full_value.dims != other.full_value.dims: + raise ValueError(f"Dimensions of the DescriptorArrays do not match: " + f"{self.full_value.dims} vs {other.full_value.dims}") + + new_full_value = self.full_value + other.full_value + + # Revert `other` to its original unit + other.convert_unit(original_unit) + else: + return NotImplemented + + descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) + descriptor_array.name = descriptor_array.unique_name + return descriptor_array + + + def __radd__(self, other: Union[list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Handle reverse addition for numbers, numpy arrays, and lists. Element-wise operation. + Converts the unit of `self` to match `other` if `other` is a DescriptorArray. + + :param other: The object to add. Must be a DescriptorArray, numpy array, list, or number. + :return: A new DescriptorArray representing the result of the addition. + """ + if isinstance(other, DescriptorArray): + # Ensure the reverse operation respects unit compatibility + return other.__add__(self) + else: + # Delegate to `__add__` for other types + return self.__add__(other) + + + + # TODO: add arithmetic operations # They should be allowed between DescriptorArray and numbers, and between DescriptorArray and DescriptorArray. # The result should be a new DescriptorArray with the same unit as the first argument. From 222bd4e30aaadf080abd70ee378ca4bdb5762e98 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 11:19:29 +0100 Subject: [PATCH 16/48] update __init__ of variable --- src/easyscience/Objects/variable/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/easyscience/Objects/variable/__init__.py b/src/easyscience/Objects/variable/__init__.py index 37b95714..04a9f6c9 100644 --- a/src/easyscience/Objects/variable/__init__.py +++ b/src/easyscience/Objects/variable/__init__.py @@ -1,9 +1,11 @@ +from .descriptor_array import DescriptorArray from .descriptor_bool import DescriptorBool from .descriptor_number import DescriptorNumber from .descriptor_str import DescriptorStr from .parameter import Parameter __all__ = [ + DescriptorArray, DescriptorBool, DescriptorNumber, DescriptorStr, From 34d08bbc3c07686fb1c54c6d375550c8f2575326 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 13:39:27 +0100 Subject: [PATCH 17/48] Add addition of DescriptorNumber --- .../Objects/variable/descriptor_array.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 3a419075..36d82023 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -14,6 +14,7 @@ from easyscience.global_object.undo_redo import PropertyStack from easyscience.global_object.undo_redo import property_stack_deco +from easyscience.Objects.variable import DescriptorNumber from .descriptor_base import DescriptorBase @@ -311,6 +312,9 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['unit'] = str(self._array.unit) raw_dict['variance'] = self._array.variances return raw_dict + + + def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: @@ -341,6 +345,18 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number new_value = self._array.values + other new_full_value=sc.array(dims=['row','column'],values=new_value,unit=self.unit,variances=self._array.variances) + elif isinstance(other, DescriptorNumber): + original_unit = other.unit + try: + 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 + + # Revert `other` to its original unit + other.convert_unit(original_unit) + elif isinstance(other, DescriptorArray): original_unit = other.unit try: @@ -373,7 +389,7 @@ def __radd__(self, other: Union[list, np.ndarray, numbers.Number]) -> Descriptor :param other: The object to add. Must be a DescriptorArray, numpy array, list, or number. :return: A new DescriptorArray representing the result of the addition. """ - if isinstance(other, DescriptorArray): + if isinstance(other, DescriptorArray,DescriptorNumber): # Ensure the reverse operation respects unit compatibility return other.__add__(self) else: From baf35fc21a3003615e16b975deb96c9c5c74d9d3 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 13:40:51 +0100 Subject: [PATCH 18/48] add neg and abs --- src/easyscience/Objects/variable/descriptor_array.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 36d82023..efe9c687 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -314,7 +314,7 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: return raw_dict - + def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: @@ -396,7 +396,17 @@ def __radd__(self, other: Union[list, np.ndarray, numbers.Number]) -> Descriptor # Delegate to `__add__` for other types return self.__add__(other) + def __neg__(self) -> DescriptorNumber: + new_value = -self.full_value + descriptor_array = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_array.name = descriptor_array.unique_name + return descriptor_array + def __abs__(self) -> DescriptorNumber: + new_value = abs(self.full_value) + descriptor_array = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_array.name = descriptor_array.unique_name + return descriptor_array # TODO: add arithmetic operations From 19cdf789f4a4db3842e3e29123a431ec770f045f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 13:57:14 +0100 Subject: [PATCH 19/48] update __radd__ to handle DescriptorNumber --- .../Objects/variable/descriptor_array.py | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index efe9c687..27b69c7e 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -319,9 +319,9 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ - Perform element-wise addition with another DescriptorArray, numpy array, list, or number. + Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. - :param other: The object to add. Must be a DescriptorArray with compatible units, + :param other: The object to add. Must be a DescriptorArray or DescriptorNumber with compatible units, or a numpy array/list with the same shape if the DescriptorArray is dimensionless. :return: A new DescriptorArray representing the result of the addition. """ @@ -381,21 +381,49 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number return descriptor_array - def __radd__(self, other: Union[list, np.ndarray, numbers.Number]) -> DescriptorArray: + def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ - Handle reverse addition for numbers, numpy arrays, and lists. Element-wise operation. - Converts the unit of `self` to match `other` if `other` is a DescriptorArray. - - :param other: The object to add. Must be a DescriptorArray, numpy array, list, or number. - :return: A new DescriptorArray representing the result of the addition. + Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. + Ensures unit compatibility when `other` is a DescriptorNumber. """ - if isinstance(other, DescriptorArray,DescriptorNumber): - # Ensure the reverse operation respects unit compatibility + if isinstance(other, DescriptorArray): + # Delegate reverse addition to `other`, respecting unit compatibility return other.__add__(self) + + elif isinstance(other, DescriptorNumber): + # Ensure unit compatibility for DescriptorNumber + original_unit = self.unit + try: + self.convert_unit(other.unit) # Convert `self` to `other`'s unit + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + + result = self.__add__(other) + + # Revert `self` to its original unit + self.convert_unit(original_unit) + return result + else: - # Delegate to `__add__` for other types + # Delegate to `__add__` for other types (e.g., list, np.ndarray, scalar) return self.__add__(other) + + def __sub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise subtraction with another DescriptorArray, numpy array, list, or number. + + :param other: The object to subtract. Must be a DescriptorArray with compatible units, + or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the subtraction. + """ + if isinstance(other, (DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number)): + # Leverage __neg__ and __add__ for subtraction + return self.__add__(-other) + else: + return NotImplemented + + def __neg__(self) -> DescriptorNumber: new_value = -self.full_value descriptor_array = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) From 2ecbb0a2c17ea95122b8a528fa06f06d2bc98dc1 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 14:13:32 +0100 Subject: [PATCH 20/48] fix __add__ --- src/easyscience/Objects/variable/descriptor_array.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 27b69c7e..cab49bb4 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -14,9 +14,9 @@ from easyscience.global_object.undo_redo import PropertyStack from easyscience.global_object.undo_redo import property_stack_deco -from easyscience.Objects.variable import DescriptorNumber from .descriptor_base import DescriptorBase +from .descriptor_number import DescriptorNumber class DescriptorArray(DescriptorBase): @@ -99,8 +99,6 @@ def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorArra """ if not isinstance(full_value, Variable): raise TypeError(f'{full_value=} must be a scipp array') - if len(full_value.dims) != 0: - raise TypeError(f'{full_value=} must be a scipp array') return cls(name=name, value=full_value.values, unit=full_value.unit, variance=full_value.variances, **kwargs) @property From 94fad5f5a5c3a0de27b32a3fc9ac1d6beb34595b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 14:25:25 +0100 Subject: [PATCH 21/48] fixed add and radd --- .../Objects/variable/descriptor_array.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index cab49bb4..9c734137 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -314,7 +314,6 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. @@ -326,7 +325,7 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number if isinstance(other, numbers.Number): if self.unit not in [None, "dimensionless"]: raise UnitError("Numbers can only be added to dimensionless values") - new_full_value = self.full_value + other # scipp can handle addition with numbers + new_full_value = self.full_value + other # scipp can handle addition with numbers elif isinstance(other, (list, np.ndarray)): if self.unit not in [None, "dimensionless"]: @@ -341,44 +340,40 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") new_value = self._array.values + other - new_full_value=sc.array(dims=['row','column'],values=new_value,unit=self.unit,variances=self._array.variances) + new_full_value = sc.array(dims=['row', 'column'], values=new_value, unit=self.unit, variances=self._array.variances) elif isinstance(other, DescriptorNumber): - original_unit = other.unit try: - other.convert_unit(self.unit) + other_converted = other.copy() + other_converted.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 - - # Revert `other` to its original unit - other.convert_unit(original_unit) + new_full_value = self.full_value + other_converted.full_value elif isinstance(other, DescriptorArray): - original_unit = other.unit try: - other.convert_unit(self.unit) + other_converted = other.copy() + other_converted.convert_unit(self.unit) except UnitError: raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None # Ensure dimensions match - if self.full_value.dims != other.full_value.dims: + if self.full_value.dims != other_converted.full_value.dims: raise ValueError(f"Dimensions of the DescriptorArrays do not match: " - f"{self.full_value.dims} vs {other.full_value.dims}") + f"{self.full_value.dims} vs {other_converted.full_value.dims}") - new_full_value = self.full_value + other.full_value + new_full_value = self.full_value + other_converted.full_value - # Revert `other` to its original unit - other.convert_unit(original_unit) else: return NotImplemented - + descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array + def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. @@ -420,7 +415,20 @@ def __sub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number return self.__add__(-other) else: return NotImplemented + + def __rsub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise subtraction with another DescriptorArray, numpy array, list, or number. + :param other: The object to subtract. Must be a DescriptorArray with compatible units, + or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the subtraction. + """ + if isinstance(other, (DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number)): + # Leverage __neg__ and __add__ for subtraction + return -(self.__radd__(-other)) + else: + return NotImplemented def __neg__(self) -> DescriptorNumber: new_value = -self.full_value From ee215dcd3efda92dfcc5d384f7c49503f1a1bdb8 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 14:34:24 +0100 Subject: [PATCH 22/48] autogenerated tests --- .../Objects/variable/descriptor_array.py | 6 +- .../Objects/variable/test_descriptor_array.py | 172 ++++++++++++++++++ 2 files changed, 175 insertions(+), 3 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 9c734137..0b31d262 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -169,7 +169,7 @@ def variance(self) -> float: :return: variance. """ - return self._array.variance + return self._array.variances @variance.setter @property_stack_deco @@ -344,7 +344,7 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number elif isinstance(other, DescriptorNumber): try: - other_converted = other.copy() + other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None @@ -353,7 +353,7 @@ def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number elif isinstance(other, DescriptorArray): try: - other_converted = other.copy() + other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index e69de29b..a561507c 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -0,0 +1,172 @@ +import pytest +import numpy as np +import scipp as sc +from scipp import UnitError +from easyscience.Objects.variable.descriptor_array import DescriptorArray + +class TestDescriptorArray: + @pytest.fixture + def descriptor(self): + values = np.array([[1., 2.], [3., 4.]]) + variances = np.array([[0.1, 0.2], [0.3, 0.4]]) + return DescriptorArray( + name="name", + value=values, + unit="m", + variance=variances, + description="description", + url="url", + display_name="display_name", + parent=None, + ) + + def test_init(self, descriptor: DescriptorArray): + # When Then Expect + assert np.array_equal(descriptor._array.values, np.array([[1., 2.], [3., 4.]])) + assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) + assert descriptor._array.unit == "m" + + # From super + assert descriptor._name == "name" + assert descriptor._description == "description" + assert descriptor._url == "url" + assert descriptor._display_name == "display_name" + + def test_init_invalid_value_type(self): + with pytest.raises(TypeError): + DescriptorArray(name="name", value=1, unit="m") + + def test_init_invalid_variance_type(self): + with pytest.raises(TypeError): + DescriptorArray(name="name", value=[[1, 2]], unit="m", variance=1) + + def test_init_variance_shape_mismatch(self): + with pytest.raises(ValueError): + DescriptorArray( + name="name", + value=[[1, 2]], + unit="m", + variance=[[0.1]], + ) + + def test_init_invalid_unit(self): + with pytest.raises(UnitError): + DescriptorArray(name="name", value=[[1, 2]], unit="unknown") + + def test_from_scipp(self): + # When + scipp_array = sc.array(dims=["row", "column"], values=[[1., 2.]], unit="m", variances=[[0.1, 0.2]]) + descriptor = DescriptorArray.from_scipp(name="name", full_value=scipp_array) + + # Then + assert np.array_equal(descriptor._array.values, [[1., 2.]]) + assert np.array_equal(descriptor._array.variances, [[0.1, 0.2]]) + assert descriptor._array.unit == "m" + + def test_full_value(self, descriptor: DescriptorArray): + # When Then + assert descriptor.full_value.unit == "m" + assert np.array_equal(descriptor.full_value.values, [[1., 2.], [3., 4.]]) + assert np.array_equal(descriptor.full_value.variances, [[0.1, 0.2], [0.3, 0.4]]) + + def test_set_full_value(self, descriptor: DescriptorArray): + # When Then + with pytest.raises(AttributeError): + descriptor.full_value = sc.scalar(2, unit="m") + + def test_value_property(self, descriptor: DescriptorArray): + # When Then + assert np.array_equal(descriptor.value, [[1., 2.], [3., 4.]]) + + def test_set_value(self, descriptor: DescriptorArray): + # When + new_value = np.array([[5, 6], [7, 8]]) + descriptor.value = new_value + + # Then + assert np.array_equal(descriptor.value, new_value) + + def test_unit(self, descriptor: DescriptorArray): + # When Then + assert descriptor.unit == "m" + + def test_set_unit(self, descriptor: DescriptorArray): + # When Then + with pytest.raises(AttributeError): + descriptor.unit = "cm" + + def test_variance(self, descriptor: DescriptorArray): + # When Then + assert np.array_equal(descriptor.variance, [[0.1, 0.2], [0.3, 0.4]]) + + def test_set_variance(self, descriptor: DescriptorArray): + # When + new_variance = np.array([[0.5, 0.6], [0.7, 0.8]]) + descriptor.variance = new_variance + + # Then + assert np.array_equal(descriptor.variance, new_variance) + + def test_error(self, descriptor: DescriptorArray): + # When Then + assert np.array_equal(descriptor.error, np.sqrt([[0.1, 0.2], [0.3, 0.4]])) + + def test_set_error(self, descriptor: DescriptorArray): + # When + new_error = np.array([[0.1, 0.2], [0.3, 0.4]]) + descriptor.error = new_error + + # Then + assert np.array_equal(descriptor.variance, new_error**2) + + def test_convert_unit(self, descriptor: DescriptorArray): + # When + descriptor.convert_unit("cm") + + # Then + assert descriptor.unit == "cm" + assert np.array_equal(descriptor.value, [[100, 200], [300, 400]]) + + def test_addition(self, descriptor: DescriptorArray): + # When + other = DescriptorArray("other", value=[[1, 1], [1, 1]], unit="m") + result = descriptor + other + + # Then + assert result.unit == "m" + assert np.array_equal(result.value, [[2, 3], [4, 5]]) + + def test_addition_with_scalar(self, descriptor: DescriptorArray): + # When + descriptor = DescriptorArray("descriptor", value=[[1., 2.], [3., 4.]]) + result = descriptor + 1.0 + + # Then + assert result.unit == "dimensionless" + assert np.array_equal(result.value, [[2, 3], [4, 5]]) + + def test_subtraction(self, descriptor: DescriptorArray): + # When + other = DescriptorArray("other", value=[[1, 1], [1, 1]], unit="m") + result = descriptor - other + + # Then + assert result.unit == "m" + assert np.array_equal(result.value, [[0, 1], [2, 3]]) + + def test_subtraction_with_scalar(self, descriptor: DescriptorArray): + # When + result = descriptor - 1.0 + + # Then + assert result.unit == "dimensionless" + assert np.array_equal(result.value, [[0, 1], [2, 3]]) + + def test_repr(self, descriptor: DescriptorArray): + # When + repr_str = repr(descriptor) + + # Then + assert "DescriptorArray" in repr_str + assert "values=[[1 2]" in repr_str + assert "unit=m" in repr_str From 9320b9a08553fd18479cc9067f5823a86bd93479 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 14:39:40 +0100 Subject: [PATCH 23/48] start over with tests --- .../Objects/variable/test_descriptor_array.py | 507 ++++++++++++++---- 1 file changed, 410 insertions(+), 97 deletions(-) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index a561507c..7d24d2c4 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -1,30 +1,35 @@ import pytest -import numpy as np +from unittest.mock import MagicMock import scipp as sc from scipp import UnitError -from easyscience.Objects.variable.descriptor_array import DescriptorArray -class TestDescriptorArray: +from easyscience.Objects.variable.descriptor_number import DescriptorNumber +from easyscience import global_object + +class TestDescriptorNumber: @pytest.fixture def descriptor(self): - values = np.array([[1., 2.], [3., 4.]]) - variances = np.array([[0.1, 0.2], [0.3, 0.4]]) - return DescriptorArray( + descriptor = DescriptorNumber( name="name", - value=values, + value=1, unit="m", - variance=variances, + variance=0.1, description="description", url="url", display_name="display_name", parent=None, ) + return descriptor + + @pytest.fixture + def clear(self): + global_object.map._clear() - def test_init(self, descriptor: DescriptorArray): + def test_init(self, descriptor: DescriptorNumber): # When Then Expect - assert np.array_equal(descriptor._array.values, np.array([[1., 2.], [3., 4.]])) - assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) - assert descriptor._array.unit == "m" + assert descriptor._scalar.value == 1 + assert descriptor._scalar.unit == "m" + assert descriptor._scalar.variance == 0.1 # From super assert descriptor._name == "name" @@ -32,141 +37,449 @@ def test_init(self, descriptor: DescriptorArray): assert descriptor._url == "url" assert descriptor._display_name == "display_name" - def test_init_invalid_value_type(self): - with pytest.raises(TypeError): - DescriptorArray(name="name", value=1, unit="m") + def test_init_sc_unit(self): + # When Then + descriptor = DescriptorNumber( + name="name", + value=1, + unit=sc.units.Unit("m"), + variance=0.1, + description="description", + url="url", + display_name="display_name", + parent=None, + ) + + # Expect + assert descriptor._scalar.value == 1 + assert descriptor._scalar.unit == "m" + assert descriptor._scalar.variance == 0.1 + + def test_init_sc_unit_unknown(self): + # When Then Expect + with pytest.raises(UnitError): + DescriptorNumber( + name="name", + value=1, + unit="unknown", + variance=0.1, + description="description", + url="url", + display_name="display_name", + parent=None, + ) - def test_init_invalid_variance_type(self): + @pytest.mark.parametrize("value", [True, "string"]) + def test_init_value_type_exception(self, value): + # When + + # Then Expect with pytest.raises(TypeError): - DescriptorArray(name="name", value=[[1, 2]], unit="m", variance=1) + DescriptorNumber( + name="name", + value=value, + unit="m", + variance=0.1, + description="description", + url="url", + display_name="display_name", + parent=None, + ) + + def test_init_variance_exception(self): + # When + variance = -1 - def test_init_variance_shape_mismatch(self): + # Then Expect with pytest.raises(ValueError): - DescriptorArray( + DescriptorNumber( name="name", - value=[[1, 2]], + value=1, unit="m", - variance=[[0.1]], + variance=variance, + description="description", + url="url", + display_name="display_name", + parent=None, ) - def test_init_invalid_unit(self): - with pytest.raises(UnitError): - DescriptorArray(name="name", value=[[1, 2]], unit="unknown") - + # test from_scipp def test_from_scipp(self): # When - scipp_array = sc.array(dims=["row", "column"], values=[[1., 2.]], unit="m", variances=[[0.1, 0.2]]) - descriptor = DescriptorArray.from_scipp(name="name", full_value=scipp_array) + full_value = sc.scalar(1, unit='m') # Then - assert np.array_equal(descriptor._array.values, [[1., 2.]]) - assert np.array_equal(descriptor._array.variances, [[0.1, 0.2]]) - assert descriptor._array.unit == "m" + descriptor = DescriptorNumber.from_scipp(name="name", full_value=full_value) - def test_full_value(self, descriptor: DescriptorArray): - # When Then - assert descriptor.full_value.unit == "m" - assert np.array_equal(descriptor.full_value.values, [[1., 2.], [3., 4.]]) - assert np.array_equal(descriptor.full_value.variances, [[0.1, 0.2], [0.3, 0.4]]) + # Expect + assert descriptor._scalar.value == 1 + assert descriptor._scalar.unit == "m" + assert descriptor._scalar.variance == None - def test_set_full_value(self, descriptor: DescriptorArray): - # When Then + @pytest.mark.parametrize("full_value", [sc.array(values=[1,2], dims=["x"]), sc.array(values=[[1], [2]], dims=["x","y"]), object(), 1, "string"], ids=["1D", "2D", "object", "int", "string"]) + def test_from_scipp_type_exception(self, full_value): + # When Then Expect + with pytest.raises(TypeError): + DescriptorNumber.from_scipp(name="name", full_value=full_value) + + def test_full_value(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.full_value == sc.scalar(1, unit='m') + + def test_set_full_value(self, descriptor: DescriptorNumber): + with pytest.raises(AttributeError): + descriptor.full_value = sc.scalar(2, unit='s') + + def test_unit(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.unit == 'm' + + def test_set_unit(self, descriptor: DescriptorNumber): with pytest.raises(AttributeError): - descriptor.full_value = sc.scalar(2, unit="m") + descriptor.unit = 's' - def test_value_property(self, descriptor: DescriptorArray): + def test_convert_unit(self, descriptor: DescriptorNumber): + # When Then + descriptor.convert_unit('mm') + + # Expect + assert descriptor._scalar.unit == 'mm' + assert descriptor._scalar.value == 1000 + assert descriptor._scalar.variance == 100000 + + def test_variance(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.variance == 0.1 + + def test_set_variance(self, descriptor: DescriptorNumber): # When Then - assert np.array_equal(descriptor.value, [[1., 2.], [3., 4.]]) + descriptor.variance = 0.2 - def test_set_value(self, descriptor: DescriptorArray): - # When - new_value = np.array([[5, 6], [7, 8]]) - descriptor.value = new_value + # Expect + assert descriptor._scalar.variance == 0.2 + assert descriptor.error == 0.4472135954999579 + + def test_error(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.error == 0.31622776601683794 + + def test_set_error(self, descriptor: DescriptorNumber): + # When Then + descriptor.error = 0.31622776601683794 + + # Expect + assert descriptor.error == 0.31622776601683794 + assert descriptor.variance == 0.1 - # Then - assert np.array_equal(descriptor.value, new_value) - def test_unit(self, descriptor: DescriptorArray): + def test_value(self, descriptor: DescriptorNumber): + # When Then Expect + assert descriptor.value == 1 + + def test_set_value(self, descriptor: DescriptorNumber): # When Then - assert descriptor.unit == "m" + descriptor.value = 2 + + # Expect + assert descriptor._scalar.value == 2 - def test_set_unit(self, descriptor: DescriptorArray): + def test_repr(self, descriptor: DescriptorNumber): # When Then - with pytest.raises(AttributeError): - descriptor.unit = "cm" + repr_str = str(descriptor) + + # Expect + assert repr_str == "" - def test_variance(self, descriptor: DescriptorArray): + def test_copy(self, descriptor: DescriptorNumber): # When Then - assert np.array_equal(descriptor.variance, [[0.1, 0.2], [0.3, 0.4]]) + descriptor_copy = descriptor.__copy__() + + # Expect + assert type(descriptor_copy) == DescriptorNumber + assert descriptor_copy._scalar.value == descriptor._scalar.value + assert descriptor_copy._scalar.unit == descriptor._scalar.unit - def test_set_variance(self, descriptor: DescriptorArray): + 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"), + ("10dm^2", "m^2")], + ids=["scientific_notation", "numbers", "unit_prefix"]) + def test_base_unit(self, unit_string, expected): # When - new_variance = np.array([[0.5, 0.6], [0.7, 0.8]]) - descriptor.variance = new_variance + descriptor = DescriptorNumber(name="name", value=1, unit=unit_string) # Then - assert np.array_equal(descriptor.variance, new_variance) + base_unit = descriptor._base_unit() + + # Expect + assert base_unit == expected - def test_error(self, descriptor: DescriptorArray): + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test + name", 3, "m", 0.11)), + (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test + name", 102, "cm", 1000.01))], + ids=["regular", "unit_conversion"]) + def test_addition(self, descriptor: DescriptorNumber, test, expected): # When Then - assert np.array_equal(descriptor.error, np.sqrt([[0.1, 0.2], [0.3, 0.4]])) + result = test + descriptor - def test_set_error(self, descriptor: DescriptorArray): - # When - new_error = np.array([[0.1, 0.2], [0.3, 0.4]]) - descriptor.error = new_error + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + assert descriptor.unit == 'm' + + def test_addition_with_scalar(self): + # When + descriptor = DescriptorNumber(name="name", value=1, variance=0.1) # Then - assert np.array_equal(descriptor.variance, new_error**2) + result = descriptor + 1.0 + result_reverse = 1.0 + descriptor - def test_convert_unit(self, descriptor: DescriptorArray): - # When - descriptor.convert_unit("cm") + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2.0 + assert result.unit == "dimensionless" + assert result.variance == 0.1 - # Then - assert descriptor.unit == "cm" - assert np.array_equal(descriptor.value, [[100, 200], [300, 400]]) + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 2.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.1 - def test_addition(self, descriptor: DescriptorArray): - # When - other = DescriptorArray("other", value=[[1, 1], [1, 1]], unit="m") - result = descriptor + other + @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["add_scalar_to_unit", "incompatible_units"]) + def test_addition_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(UnitError): + result = descriptor + test + with pytest.raises(UnitError): + result_reverse = test + descriptor + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test - name", 1, "m", 0.11)), + (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test - name", -98, "cm", 1000.01))], + ids=["regular", "unit_conversion"]) + def test_subtraction(self, descriptor: DescriptorNumber, test, expected): + # When Then + result = test - descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + assert descriptor.unit == 'm' + + def test_subtraction_with_scalar(self): + # When + descriptor = DescriptorNumber(name="name", value=2, variance=0.1) # Then + result = descriptor - 1.0 + result_reverse = 1.0 - descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 1.0 + assert result.unit == "dimensionless" + assert result.variance == 0.1 + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == -1.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.1 + + @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["sub_scalar_to_unit", "incompatible_units"]) + def test_subtraction_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(UnitError): + result = test - descriptor + with pytest.raises(UnitError): + result_reverse = descriptor - test + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test * name", 2, "m^2", 0.41)), + (DescriptorNumber("test", 2, "dm", 0.01), DescriptorNumber("test * name", 0.2, "m^2", 0.0041))], + ids=["regular", "base_unit_conversion"]) + def test_multiplication(self, descriptor: DescriptorNumber, test, expected): + # When Then + result = test * descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + + def test_multiplication_with_scalar(self, descriptor: DescriptorNumber): + # When Then + result = descriptor * 2.0 + result_reverse = 2.0 * descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2.0 assert result.unit == "m" - assert np.array_equal(result.value, [[2, 3], [4, 5]]) + assert result.variance == 0.4 + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 2.0 + assert result_reverse.unit == "m" + assert result_reverse.variance == 0.4 + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (DescriptorNumber("test", 2, "m^2", 0.01,), DescriptorNumber("name / test", 0.5, "1/m", 0.025625), DescriptorNumber("test / name", 2, "m", 0.41)), + (2, DescriptorNumber("name / 2", 0.5, "m", 0.025), DescriptorNumber("2 / name", 2, "1/m", 0.4))], + ids=["descriptorNumber", "scalar"]) + def test_division(self, descriptor: DescriptorNumber, test, expected, expected_reverse): + # When Then + result = descriptor / test + result_reverse = test / descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == pytest.approx(expected_reverse.variance) + + @pytest.mark.parametrize("test", [0, DescriptorNumber("test", 0, "m", 0.01)], ids=["zero", "zero_descriptor"]) + def test_division_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(ZeroDivisionError): + result = descriptor / test + + def test_division_exception_reverse(self): + # When + descriptor = DescriptorNumber(name="name", value=0, variance=0.1) + + # Then Expect + with pytest.raises(ZeroDivisionError): + result = 2 / descriptor + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2), DescriptorNumber("name ** test", 4, unit="m^2", variance=1.6)), + (2, DescriptorNumber("name ** 2", 4, unit="m^2", variance=1.6)), + (-2, DescriptorNumber("name ** -2", 0.25, unit="1/m^2", variance=0.00625))], + ids=["descriptorNumber", "scalar", "negative_scalar"]) + def test_power_of_descriptor(self, test, expected): + # When + descriptor = DescriptorNumber(name="name", value=2, unit="m", variance=0.1) - def test_addition_with_scalar(self, descriptor: DescriptorArray): - # When - descriptor = DescriptorArray("descriptor", value=[[1., 2.], [3., 4.]]) - result = descriptor + 1.0 + # Then + result = descriptor ** test + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + def test_power_of_dimensionless_descriptor(self): + # When + descriptor = DescriptorNumber(name="name", value=2, unit="dimensionless", variance=0.1) # Then + result = descriptor ** 0.5 + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 1.4142135623730951 assert result.unit == "dimensionless" - assert np.array_equal(result.value, [[2, 3], [4, 5]]) + assert result.variance == pytest.approx(0.0125) + + @pytest.mark.parametrize("descriptor, exponent, exception", [ + (DescriptorNumber("name", 2), DescriptorNumber("test", 2, unit="m"), UnitError), + (DescriptorNumber("name", 2), DescriptorNumber("test", 2, variance=0.1), ValueError), + (DescriptorNumber("name", 2, unit="m"), 0.5, UnitError), + (DescriptorNumber("name", -2), 0.5, ValueError)], + ids=["descriptor_unit", "descriptor_variance", "fractional_of_unit", "fractonal_of_negative"]) + def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): + # When Then Expect + with pytest.raises(exception): + result = descriptor ** exponent - def test_subtraction(self, descriptor: DescriptorArray): - # When - other = DescriptorArray("other", value=[[1, 1], [1, 1]], unit="m") - result = descriptor - other + + def test_descriptor_as_exponentiation(self): + # When + descriptor = DescriptorNumber(name="name", value=2) # Then - assert result.unit == "m" - assert np.array_equal(result.value, [[0, 1], [2, 3]]) + result = 2 ** descriptor - def test_subtraction_with_scalar(self, descriptor: DescriptorArray): - # When - result = descriptor - 1.0 + # Expect + assert result == 4 + + @pytest.mark.parametrize("exponent, exception", [ + (DescriptorNumber("test", 2, unit="m"), UnitError), + (DescriptorNumber("test", 2, variance=0.1), ValueError)], + ids=["descriptor_unit", "descriptor_variance"]) + def test_descriptor_as_exponentiation_exception(self, exponent, exception): + # When Then Expect + with pytest.raises(exception): + result = 2 ** exponent + + def test_negation(self): + # When + descriptor = DescriptorNumber(name="name", unit="m", value=2, variance=0.1) # Then - assert result.unit == "dimensionless" - assert np.array_equal(result.value, [[0, 1], [2, 3]]) + result = -descriptor - def test_repr(self, descriptor: DescriptorArray): - # When - repr_str = repr(descriptor) + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == -2 + assert result.unit == "m" + assert result.variance == 0.1 + + def test_abs(self): + # When + descriptor = DescriptorNumber(name="name", unit="m", value=-2, variance=0.1) # Then - assert "DescriptorArray" in repr_str - assert "values=[[1 2]" in repr_str - assert "unit=m" in repr_str + result = abs(descriptor) + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2 + assert result.unit == "m" + assert result.variance == 0.1 + From ea62449f238925a56a6ea44e460270aa4c630f9c Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 15:01:40 +0100 Subject: [PATCH 24/48] adapted some tests from descriptor_number --- .../Objects/variable/descriptor_array.py | 2 +- .../Objects/variable/test_descriptor_array.py | 776 +++++++++--------- 2 files changed, 389 insertions(+), 389 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 0b31d262..44d368d6 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -71,7 +71,7 @@ def __init__( self._array = sc.array(dims=['row', 'column'], values=value, unit=unit, variances=variance) except Exception as message: raise UnitError(message) - + # TODO: handle 1xn and nx1 arrays self._array = sc.array(dims=['row','column'],values=value, unit=unit, variances=variance) super().__init__( diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 7d24d2c4..ed843be5 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -3,17 +3,19 @@ import scipp as sc from scipp import UnitError -from easyscience.Objects.variable.descriptor_number import DescriptorNumber +import numpy as np + +from easyscience.Objects.variable.descriptor_array import DescriptorArray from easyscience import global_object -class TestDescriptorNumber: +class TestDescriptorArray: @pytest.fixture def descriptor(self): - descriptor = DescriptorNumber( + descriptor = DescriptorArray( name="name", - value=1, + value=[[1., 2.], [3., 4.]], unit="m", - variance=0.1, + variance=[[0.1, 0.2], [0.3, 0.4]], description="description", url="url", display_name="display_name", @@ -25,11 +27,11 @@ def descriptor(self): def clear(self): global_object.map._clear() - def test_init(self, descriptor: DescriptorNumber): + def test_init(self, descriptor: DescriptorArray): # When Then Expect - assert descriptor._scalar.value == 1 - assert descriptor._scalar.unit == "m" - assert descriptor._scalar.variance == 0.1 + assert np.array_equal(descriptor._array.values,np.array([[1., 2.], [3., 4.]])) + assert descriptor._array.unit == "m" + assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) # From super assert descriptor._name == "name" @@ -39,11 +41,11 @@ def test_init(self, descriptor: DescriptorNumber): def test_init_sc_unit(self): # When Then - descriptor = DescriptorNumber( + descriptor = DescriptorArray( name="name", - value=1, + value=[[1., 2.], [3., 4.]], unit=sc.units.Unit("m"), - variance=0.1, + variance=[[0.1, 0.2], [0.3, 0.4]], description="description", url="url", display_name="display_name", @@ -51,18 +53,18 @@ def test_init_sc_unit(self): ) # Expect - assert descriptor._scalar.value == 1 - assert descriptor._scalar.unit == "m" - assert descriptor._scalar.variance == 0.1 + assert np.array_equal(descriptor._array.values,np.array([[1., 2.], [3., 4.]])) + assert descriptor._array.unit == "m" + assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) def test_init_sc_unit_unknown(self): # When Then Expect with pytest.raises(UnitError): - DescriptorNumber( + DescriptorArray( name="name", - value=1, + value=[[1., 2.], [3., 4.]], unit="unknown", - variance=0.1, + variance=[[0.1, 0.2], [0.3, 0.4]], description="description", url="url", display_name="display_name", @@ -75,11 +77,11 @@ def test_init_value_type_exception(self, value): # Then Expect with pytest.raises(TypeError): - DescriptorNumber( + DescriptorArray( name="name", value=value, unit="m", - variance=0.1, + variance=[[0.1, 0.2], [0.3, 0.4]], description="description", url="url", display_name="display_name", @@ -88,13 +90,12 @@ def test_init_value_type_exception(self, value): def test_init_variance_exception(self): # When - variance = -1 - + variance=[[-0.1, -0.2], [-0.3, -0.4]] # Then Expect with pytest.raises(ValueError): - DescriptorNumber( + DescriptorArray( name="name", - value=1, + value=[[1., 2.], [3., 4.]], unit="m", variance=variance, description="description", @@ -106,380 +107,379 @@ def test_init_variance_exception(self): # test from_scipp def test_from_scipp(self): # When - full_value = sc.scalar(1, unit='m') - + full_value = sc.array(dims=['row','column'],values=[[1,2],[3,4]], unit='m') # Then - descriptor = DescriptorNumber.from_scipp(name="name", full_value=full_value) + descriptor = DescriptorArray.from_scipp(name="name", full_value=full_value) # Expect - assert descriptor._scalar.value == 1 - assert descriptor._scalar.unit == "m" - assert descriptor._scalar.variance == None - - @pytest.mark.parametrize("full_value", [sc.array(values=[1,2], dims=["x"]), sc.array(values=[[1], [2]], dims=["x","y"]), object(), 1, "string"], ids=["1D", "2D", "object", "int", "string"]) - def test_from_scipp_type_exception(self, full_value): - # When Then Expect - with pytest.raises(TypeError): - DescriptorNumber.from_scipp(name="name", full_value=full_value) - - def test_full_value(self, descriptor: DescriptorNumber): - # When Then Expect - assert descriptor.full_value == sc.scalar(1, unit='m') + assert np.array_equal(descriptor._array.values,[[1,2],[3,4]]) + assert descriptor._array.unit == "m" + assert descriptor._array.variances == None + + # @pytest.mark.parametrize("full_value", [sc.array(values=[1,2], dims=["x"]), sc.array(values=[[1], [2]], dims=["x","y"]), object(), 1, "string"], ids=["1D", "2D", "object", "int", "string"]) + # def test_from_scipp_type_exception(self, full_value): + # # When Then Expect + # with pytest.raises(TypeError): + # DescriptorArray.from_scipp(name="name", full_value=full_value) + + # def test_full_value(self, descriptor: DescriptorArray): + # # When Then Expect + # assert descriptor.full_value == sc.scalar(1, unit='m') - def test_set_full_value(self, descriptor: DescriptorNumber): - with pytest.raises(AttributeError): - descriptor.full_value = sc.scalar(2, unit='s') + # def test_set_full_value(self, descriptor: DescriptorArray): + # with pytest.raises(AttributeError): + # descriptor.full_value = sc.scalar(2, unit='s') - def test_unit(self, descriptor: DescriptorNumber): - # When Then Expect - assert descriptor.unit == 'm' + # def test_unit(self, descriptor: DescriptorArray): + # # When Then Expect + # assert descriptor.unit == 'm' - def test_set_unit(self, descriptor: DescriptorNumber): - with pytest.raises(AttributeError): - descriptor.unit = 's' - - def test_convert_unit(self, descriptor: DescriptorNumber): - # When Then - descriptor.convert_unit('mm') - - # Expect - assert descriptor._scalar.unit == 'mm' - assert descriptor._scalar.value == 1000 - assert descriptor._scalar.variance == 100000 - - def test_variance(self, descriptor: DescriptorNumber): - # When Then Expect - assert descriptor.variance == 0.1 + # def test_set_unit(self, descriptor: DescriptorArray): + # with pytest.raises(AttributeError): + # descriptor.unit = 's' + + # def test_convert_unit(self, descriptor: DescriptorArray): + # # When Then + # descriptor.convert_unit('mm') + + # # Expect + # assert descriptor._array.unit == 'mm' + # assert descriptor._array.value == 1000 + # assert descriptor._array.variance == 100000 + + # def test_variance(self, descriptor: DescriptorArray): + # # When Then Expect + # assert descriptor.variance == 0.1 - def test_set_variance(self, descriptor: DescriptorNumber): - # When Then - descriptor.variance = 0.2 + # def test_set_variance(self, descriptor: DescriptorArray): + # # When Then + # descriptor.variance = 0.2 - # Expect - assert descriptor._scalar.variance == 0.2 - assert descriptor.error == 0.4472135954999579 + # # Expect + # assert descriptor._array.variance == 0.2 + # assert descriptor.error == 0.4472135954999579 - def test_error(self, descriptor: DescriptorNumber): - # When Then Expect - assert descriptor.error == 0.31622776601683794 + # def test_error(self, descriptor: DescriptorArray): + # # When Then Expect + # assert descriptor.error == 0.31622776601683794 - def test_set_error(self, descriptor: DescriptorNumber): - # When Then - descriptor.error = 0.31622776601683794 - - # Expect - assert descriptor.error == 0.31622776601683794 - assert descriptor.variance == 0.1 - - - def test_value(self, descriptor: DescriptorNumber): - # When Then Expect - assert descriptor.value == 1 - - def test_set_value(self, descriptor: DescriptorNumber): - # When Then - descriptor.value = 2 - - # Expect - assert descriptor._scalar.value == 2 - - def test_repr(self, descriptor: DescriptorNumber): - # When Then - repr_str = str(descriptor) - - # Expect - assert repr_str == "" - - def test_copy(self, descriptor: DescriptorNumber): - # When Then - descriptor_copy = descriptor.__copy__() - - # Expect - assert type(descriptor_copy) == 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"), - ("10dm^2", "m^2")], - ids=["scientific_notation", "numbers", "unit_prefix"]) - def test_base_unit(self, unit_string, expected): - # When - descriptor = DescriptorNumber(name="name", value=1, unit=unit_string) - - # Then - base_unit = descriptor._base_unit() - - # Expect - assert base_unit == expected - - @pytest.mark.parametrize("test, expected", [ - (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test + name", 3, "m", 0.11)), - (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test + name", 102, "cm", 1000.01))], - ids=["regular", "unit_conversion"]) - def test_addition(self, descriptor: DescriptorNumber, test, expected): - # When Then - result = test + descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == expected.value - assert result.unit == expected.unit - assert result.variance == expected.variance + # def test_set_error(self, descriptor: DescriptorArray): + # # When Then + # descriptor.error = 0.31622776601683794 + + # # Expect + # assert descriptor.error == 0.31622776601683794 + # assert descriptor.variance == 0.1 + + + # def test_value(self, descriptor: DescriptorArray): + # # When Then Expect + # assert descriptor.value == 1 + + # def test_set_value(self, descriptor: DescriptorArray): + # # When Then + # descriptor.value = 2 + + # # Expect + # assert descriptor._array.value == 2 + + # def test_repr(self, descriptor: DescriptorArray): + # # When Then + # repr_str = str(descriptor) + + # # Expect + # assert repr_str == "" + + # def test_copy(self, descriptor: DescriptorArray): + # # When Then + # descriptor_copy = descriptor.__copy__() + + # # Expect + # assert type(descriptor_copy) == DescriptorArray + # assert descriptor_copy._array.value == descriptor._array.value + # assert descriptor_copy._array.unit == descriptor._array.unit + + # def test_as_data_dict(self, clear, descriptor: DescriptorArray): + # # 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": "DescriptorArray_0", + # } + + # @pytest.mark.parametrize("unit_string, expected", [ + # ("1e+9", "dimensionless"), + # ("1000", "dimensionless"), + # ("10dm^2", "m^2")], + # ids=["scientific_notation", "numbers", "unit_prefix"]) + # def test_base_unit(self, unit_string, expected): + # # When + # descriptor = DescriptorArray(name="name", value=1, unit=unit_string) + + # # Then + # base_unit = descriptor._base_unit() + + # # Expect + # assert base_unit == expected + + # @pytest.mark.parametrize("test, expected", [ + # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test + name", 3, "m", 0.11)), + # (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test + name", 102, "cm", 1000.01))], + # ids=["regular", "unit_conversion"]) + # def test_addition(self, descriptor: DescriptorArray, test, expected): + # # When Then + # result = test + descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == expected.value + # assert result.unit == expected.unit + # assert result.variance == expected.variance - assert descriptor.unit == 'm' - - def test_addition_with_scalar(self): - # When - descriptor = DescriptorNumber(name="name", value=1, variance=0.1) - - # Then - result = descriptor + 1.0 - result_reverse = 1.0 + descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == 2.0 - assert result.unit == "dimensionless" - assert result.variance == 0.1 - - assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name - assert result_reverse.value == 2.0 - assert result_reverse.unit == "dimensionless" - assert result_reverse.variance == 0.1 - - @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["add_scalar_to_unit", "incompatible_units"]) - def test_addition_exception(self, descriptor: DescriptorNumber, test): - # When Then Expect - with pytest.raises(UnitError): - result = descriptor + test - with pytest.raises(UnitError): - result_reverse = test + descriptor + # assert descriptor.unit == 'm' + + # def test_addition_with_array(self): + # # When + # descriptor = DescriptorArray(name="name", value=1, variance=0.1) + + # # Then + # result = descriptor + 1.0 + # result_reverse = 1.0 + descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == 2.0 + # assert result.unit == "dimensionless" + # assert result.variance == 0.1 + + # assert type(result_reverse) == DescriptorArray + # assert result_reverse.name == result_reverse.unique_name + # assert result_reverse.value == 2.0 + # assert result_reverse.unit == "dimensionless" + # assert result_reverse.variance == 0.1 + + # @pytest.mark.parametrize("test", [1.0, DescriptorArray("test", 2, "s",)], ids=["add_array_to_unit", "incompatible_units"]) + # def test_addition_exception(self, descriptor: DescriptorArray, test): + # # When Then Expect + # with pytest.raises(UnitError): + # result = descriptor + test + # with pytest.raises(UnitError): + # result_reverse = test + descriptor - @pytest.mark.parametrize("test, expected", [ - (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test - name", 1, "m", 0.11)), - (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test - name", -98, "cm", 1000.01))], - ids=["regular", "unit_conversion"]) - def test_subtraction(self, descriptor: DescriptorNumber, test, expected): - # When Then - result = test - descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == expected.value - assert result.unit == expected.unit - assert result.variance == expected.variance - - assert descriptor.unit == 'm' - - def test_subtraction_with_scalar(self): - # When - descriptor = DescriptorNumber(name="name", value=2, variance=0.1) - - # Then - result = descriptor - 1.0 - result_reverse = 1.0 - descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == 1.0 - assert result.unit == "dimensionless" - assert result.variance == 0.1 - - assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name - assert result_reverse.value == -1.0 - assert result_reverse.unit == "dimensionless" - assert result_reverse.variance == 0.1 - - @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["sub_scalar_to_unit", "incompatible_units"]) - def test_subtraction_exception(self, descriptor: DescriptorNumber, test): - # When Then Expect - with pytest.raises(UnitError): - result = test - descriptor - with pytest.raises(UnitError): - result_reverse = descriptor - test - - @pytest.mark.parametrize("test, expected", [ - (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test * name", 2, "m^2", 0.41)), - (DescriptorNumber("test", 2, "dm", 0.01), DescriptorNumber("test * name", 0.2, "m^2", 0.0041))], - ids=["regular", "base_unit_conversion"]) - def test_multiplication(self, descriptor: DescriptorNumber, test, expected): - # When Then - result = test * descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == expected.value - assert result.unit == expected.unit - assert result.variance == pytest.approx(expected.variance) - - def test_multiplication_with_scalar(self, descriptor: DescriptorNumber): - # When Then - result = descriptor * 2.0 - result_reverse = 2.0 * descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == 2.0 - assert result.unit == "m" - assert result.variance == 0.4 - - assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name - assert result_reverse.value == 2.0 - assert result_reverse.unit == "m" - assert result_reverse.variance == 0.4 - - @pytest.mark.parametrize("test, expected, expected_reverse", [ - (DescriptorNumber("test", 2, "m^2", 0.01,), DescriptorNumber("name / test", 0.5, "1/m", 0.025625), DescriptorNumber("test / name", 2, "m", 0.41)), - (2, DescriptorNumber("name / 2", 0.5, "m", 0.025), DescriptorNumber("2 / name", 2, "1/m", 0.4))], - ids=["descriptorNumber", "scalar"]) - def test_division(self, descriptor: DescriptorNumber, test, expected, expected_reverse): - # When Then - result = descriptor / test - result_reverse = test / descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == expected.value - assert result.unit == expected.unit - assert result.variance == pytest.approx(expected.variance) - - assert type(result_reverse) == DescriptorNumber - assert result_reverse.name == result_reverse.unique_name - assert result_reverse.value == expected_reverse.value - assert result_reverse.unit == expected_reverse.unit - assert result_reverse.variance == pytest.approx(expected_reverse.variance) - - @pytest.mark.parametrize("test", [0, DescriptorNumber("test", 0, "m", 0.01)], ids=["zero", "zero_descriptor"]) - def test_division_exception(self, descriptor: DescriptorNumber, test): - # When Then Expect - with pytest.raises(ZeroDivisionError): - result = descriptor / test - - def test_division_exception_reverse(self): - # When - descriptor = DescriptorNumber(name="name", value=0, variance=0.1) - - # Then Expect - with pytest.raises(ZeroDivisionError): - result = 2 / descriptor - - @pytest.mark.parametrize("test, expected", [ - (DescriptorNumber("test", 2), DescriptorNumber("name ** test", 4, unit="m^2", variance=1.6)), - (2, DescriptorNumber("name ** 2", 4, unit="m^2", variance=1.6)), - (-2, DescriptorNumber("name ** -2", 0.25, unit="1/m^2", variance=0.00625))], - ids=["descriptorNumber", "scalar", "negative_scalar"]) - def test_power_of_descriptor(self, test, expected): - # When - descriptor = DescriptorNumber(name="name", value=2, unit="m", variance=0.1) - - # Then - result = descriptor ** test - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == expected.value - assert result.unit == expected.unit - assert result.variance == expected.variance - - def test_power_of_dimensionless_descriptor(self): - # When - descriptor = DescriptorNumber(name="name", value=2, unit="dimensionless", variance=0.1) - - # Then - result = descriptor ** 0.5 - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == 1.4142135623730951 - assert result.unit == "dimensionless" - assert result.variance == pytest.approx(0.0125) - - @pytest.mark.parametrize("descriptor, exponent, exception", [ - (DescriptorNumber("name", 2), DescriptorNumber("test", 2, unit="m"), UnitError), - (DescriptorNumber("name", 2), DescriptorNumber("test", 2, variance=0.1), ValueError), - (DescriptorNumber("name", 2, unit="m"), 0.5, UnitError), - (DescriptorNumber("name", -2), 0.5, ValueError)], - ids=["descriptor_unit", "descriptor_variance", "fractional_of_unit", "fractonal_of_negative"]) - def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): - # When Then Expect - with pytest.raises(exception): - result = descriptor ** exponent - - - def test_descriptor_as_exponentiation(self): - # When - descriptor = DescriptorNumber(name="name", value=2) - - # Then - result = 2 ** descriptor - - # Expect - assert result == 4 - - @pytest.mark.parametrize("exponent, exception", [ - (DescriptorNumber("test", 2, unit="m"), UnitError), - (DescriptorNumber("test", 2, variance=0.1), ValueError)], - ids=["descriptor_unit", "descriptor_variance"]) - def test_descriptor_as_exponentiation_exception(self, exponent, exception): - # When Then Expect - with pytest.raises(exception): - result = 2 ** exponent - - def test_negation(self): - # When - descriptor = DescriptorNumber(name="name", unit="m", value=2, variance=0.1) - - # Then - result = -descriptor - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == -2 - assert result.unit == "m" - assert result.variance == 0.1 - - def test_abs(self): - # When - descriptor = DescriptorNumber(name="name", unit="m", value=-2, variance=0.1) - - # Then - result = abs(descriptor) - - # Expect - assert type(result) == DescriptorNumber - assert result.name == result.unique_name - assert result.value == 2 - assert result.unit == "m" - assert result.variance == 0.1 + # @pytest.mark.parametrize("test, expected", [ + # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test - name", 1, "m", 0.11)), + # (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test - name", -98, "cm", 1000.01))], + # ids=["regular", "unit_conversion"]) + # def test_subtraction(self, descriptor: DescriptorArray, test, expected): + # # When Then + # result = test - descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == expected.value + # assert result.unit == expected.unit + # assert result.variance == expected.variance + + # assert descriptor.unit == 'm' + + # def test_subtraction_with_array(self): + # # When + # descriptor = DescriptorArray(name="name", value=2, variance=0.1) + + # # Then + # result = descriptor - 1.0 + # result_reverse = 1.0 - descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == 1.0 + # assert result.unit == "dimensionless" + # assert result.variance == 0.1 + + # assert type(result_reverse) == DescriptorArray + # assert result_reverse.name == result_reverse.unique_name + # assert result_reverse.value == -1.0 + # assert result_reverse.unit == "dimensionless" + # assert result_reverse.variance == 0.1 + + # @pytest.mark.parametrize("test", [1.0, DescriptorArray("test", 2, "s",)], ids=["sub_array_to_unit", "incompatible_units"]) + # def test_subtraction_exception(self, descriptor: DescriptorArray, test): + # # When Then Expect + # with pytest.raises(UnitError): + # result = test - descriptor + # with pytest.raises(UnitError): + # result_reverse = descriptor - test + + # @pytest.mark.parametrize("test, expected", [ + # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test * name", 2, "m^2", 0.41)), + # (DescriptorArray("test", 2, "dm", 0.01), DescriptorArray("test * name", 0.2, "m^2", 0.0041))], + # ids=["regular", "base_unit_conversion"]) + # def test_multiplication(self, descriptor: DescriptorArray, test, expected): + # # When Then + # result = test * descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == expected.value + # assert result.unit == expected.unit + # assert result.variance == pytest.approx(expected.variance) + + # def test_multiplication_with_array(self, descriptor: DescriptorArray): + # # When Then + # result = descriptor * 2.0 + # result_reverse = 2.0 * descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == 2.0 + # assert result.unit == "m" + # assert result.variance == 0.4 + + # assert type(result_reverse) == DescriptorArray + # assert result_reverse.name == result_reverse.unique_name + # assert result_reverse.value == 2.0 + # assert result_reverse.unit == "m" + # assert result_reverse.variance == 0.4 + + # @pytest.mark.parametrize("test, expected, expected_reverse", [ + # (DescriptorArray("test", 2, "m^2", 0.01,), DescriptorArray("name / test", 0.5, "1/m", 0.025625), DescriptorArray("test / name", 2, "m", 0.41)), + # (2, DescriptorArray("name / 2", 0.5, "m", 0.025), DescriptorArray("2 / name", 2, "1/m", 0.4))], + # ids=["DescriptorArray", "scalar"]) + # def test_division(self, descriptor: DescriptorArray, test, expected, expected_reverse): + # # When Then + # result = descriptor / test + # result_reverse = test / descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == expected.value + # assert result.unit == expected.unit + # assert result.variance == pytest.approx(expected.variance) + + # assert type(result_reverse) == DescriptorArray + # assert result_reverse.name == result_reverse.unique_name + # assert result_reverse.value == expected_reverse.value + # assert result_reverse.unit == expected_reverse.unit + # assert result_reverse.variance == pytest.approx(expected_reverse.variance) + + # @pytest.mark.parametrize("test", [0, DescriptorArray("test", 0, "m", 0.01)], ids=["zero", "zero_descriptor"]) + # def test_division_exception(self, descriptor: DescriptorArray, test): + # # When Then Expect + # with pytest.raises(ZeroDivisionError): + # result = descriptor / test + + # def test_division_exception_reverse(self): + # # When + # descriptor = DescriptorArray(name="name", value=0, variance=0.1) + + # # Then Expect + # with pytest.raises(ZeroDivisionError): + # result = 2 / descriptor + + # @pytest.mark.parametrize("test, expected", [ + # (DescriptorArray("test", 2), DescriptorArray("name ** test", 4, unit="m^2", variance=1.6)), + # (2, DescriptorArray("name ** 2", 4, unit="m^2", variance=1.6)), + # (-2, DescriptorArray("name ** -2", 0.25, unit="1/m^2", variance=0.00625))], + # ids=["DescriptorArray", "scalar", "negative_array"]) + # def test_power_of_descriptor(self, test, expected): + # # When + # descriptor = DescriptorArray(name="name", value=2, unit="m", variance=0.1) + + # # Then + # result = descriptor ** test + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == expected.value + # assert result.unit == expected.unit + # assert result.variance == expected.variance + + # def test_power_of_dimensionless_descriptor(self): + # # When + # descriptor = DescriptorArray(name="name", value=2, unit="dimensionless", variance=0.1) + + # # Then + # result = descriptor ** 0.5 + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == 1.4142135623730951 + # assert result.unit == "dimensionless" + # assert result.variance == pytest.approx(0.0125) + + # @pytest.mark.parametrize("descriptor, exponent, exception", [ + # (DescriptorArray("name", 2), DescriptorArray("test", 2, unit="m"), UnitError), + # (DescriptorArray("name", 2), DescriptorArray("test", 2, variance=0.1), ValueError), + # (DescriptorArray("name", 2, unit="m"), 0.5, UnitError), + # (DescriptorArray("name", -2), 0.5, ValueError)], + # ids=["descriptor_unit", "descriptor_variance", "fractional_of_unit", "fractonal_of_negative"]) + # def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): + # # When Then Expect + # with pytest.raises(exception): + # result = descriptor ** exponent + + + # def test_descriptor_as_exponentiation(self): + # # When + # descriptor = DescriptorArray(name="name", value=2) + + # # Then + # result = 2 ** descriptor + + # # Expect + # assert result == 4 + + # @pytest.mark.parametrize("exponent, exception", [ + # (DescriptorArray("test", 2, unit="m"), UnitError), + # (DescriptorArray("test", 2, variance=0.1), ValueError)], + # ids=["descriptor_unit", "descriptor_variance"]) + # def test_descriptor_as_exponentiation_exception(self, exponent, exception): + # # When Then Expect + # with pytest.raises(exception): + # result = 2 ** exponent + + # def test_negation(self): + # # When + # descriptor = DescriptorArray(name="name", unit="m", value=2, variance=0.1) + + # # Then + # result = -descriptor + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == -2 + # assert result.unit == "m" + # assert result.variance == 0.1 + + # def test_abs(self): + # # When + # descriptor = DescriptorArray(name="name", unit="m", value=-2, variance=0.1) + + # # Then + # result = abs(descriptor) + + # # Expect + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert result.value == 2 + # assert result.unit == "m" + # assert result.variance == 0.1 From 964e8cf2f3cc207e9500104d4ef2b8843097978c Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 15:09:34 +0100 Subject: [PATCH 25/48] more tests --- .../Objects/variable/test_descriptor_array.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index ed843be5..7f567598 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -122,30 +122,31 @@ def test_from_scipp(self): # with pytest.raises(TypeError): # DescriptorArray.from_scipp(name="name", full_value=full_value) - # def test_full_value(self, descriptor: DescriptorArray): - # # When Then Expect - # assert descriptor.full_value == sc.scalar(1, unit='m') + def test_full_value(self, descriptor: DescriptorArray): + # When Then Expect + assert descriptor.full_value == sc.array(dims=['row','column'],values=[[1,2],[3,4]], unit='m') - # def test_set_full_value(self, descriptor: DescriptorArray): - # with pytest.raises(AttributeError): - # descriptor.full_value = sc.scalar(2, unit='s') + def test_set_full_value(self, descriptor: DescriptorArray): + with pytest.raises(AttributeError): + descriptor.full_value = sc.array(dims=['row','column'],values=[[1,2],[3,4]], unit='s') - # def test_unit(self, descriptor: DescriptorArray): - # # When Then Expect - # assert descriptor.unit == 'm' + def test_unit(self, descriptor: DescriptorArray): + # When Then Expect + assert descriptor.unit == 'm' - # def test_set_unit(self, descriptor: DescriptorArray): - # with pytest.raises(AttributeError): - # descriptor.unit = 's' + def test_set_unit(self, descriptor: DescriptorArray): + with pytest.raises(AttributeError): + descriptor.unit = 's' - # def test_convert_unit(self, descriptor: DescriptorArray): - # # When Then - # descriptor.convert_unit('mm') + def test_convert_unit(self, descriptor: DescriptorArray): + # When Then + descriptor.convert_unit('mm') - # # Expect - # assert descriptor._array.unit == 'mm' - # assert descriptor._array.value == 1000 - # assert descriptor._array.variance == 100000 + # Expect + assert descriptor._array.unit == 'mm' + print(descriptor._array.variances) + assert np.array_equal(descriptor._array.values,[[1000,2000],[3000,4000]]) + assert np.array_equal(descriptor._array.variances,[[100000,200000],[300000,400000]]) # def test_variance(self, descriptor: DescriptorArray): # # When Then Expect From c9436d05b70cbde5d217651834cb7e573158dfd4 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 15:55:52 +0100 Subject: [PATCH 26/48] update __repr__ and more tests --- .../Objects/variable/descriptor_array.py | 2 + .../Objects/variable/test_descriptor_array.py | 83 ++++++++++--------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 44d368d6..5b3aa56c 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -302,8 +302,10 @@ def __repr__(self) -> str: string += f", unit={obj_unit}" string += ">" + string=string.replace('\n', ',') return string + def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict = super().as_dict(skip=skip) raw_dict['value'] = self._array.values diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 7f567598..730fd765 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -148,57 +148,62 @@ def test_convert_unit(self, descriptor: DescriptorArray): assert np.array_equal(descriptor._array.values,[[1000,2000],[3000,4000]]) assert np.array_equal(descriptor._array.variances,[[100000,200000],[300000,400000]]) - # def test_variance(self, descriptor: DescriptorArray): - # # When Then Expect - # assert descriptor.variance == 0.1 + def test_variance(self, descriptor: DescriptorArray): + # When Then Expect + assert np.array_equal(descriptor._array.variances, np.array([[0.1, 0.2], [0.3, 0.4]])) + - # def test_set_variance(self, descriptor: DescriptorArray): - # # When Then - # descriptor.variance = 0.2 + def test_set_variance(self, descriptor: DescriptorArray): + # When Then + descriptor.variance = [[0.2, 0.3], [0.4, 0.5]] - # # Expect - # assert descriptor._array.variance == 0.2 - # assert descriptor.error == 0.4472135954999579 + # Expect + assert np.array_equal(descriptor.variance, np.array([[0.2, 0.3], [0.4, 0.5]])) - # def test_error(self, descriptor: DescriptorArray): - # # When Then Expect - # assert descriptor.error == 0.31622776601683794 - - # def test_set_error(self, descriptor: DescriptorArray): - # # When Then - # descriptor.error = 0.31622776601683794 + assert np.array_equal(descriptor.error, np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]]))) - # # Expect - # assert descriptor.error == 0.31622776601683794 - # assert descriptor.variance == 0.1 + def test_error(self, descriptor: DescriptorArray): + # When Then Expect + assert np.array_equal(descriptor.error, np.sqrt(np.array([[0.1, 0.2], [0.3, 0.4]]))) + + def test_set_error(self, descriptor: DescriptorArray): + # When Then + descriptor.error = np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]])) + print(descriptor.variance) + print(descriptor.error) + # Expect + assert np.allclose(descriptor.error, np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]]))) + assert np.allclose(descriptor.variance, np.array([[0.2, 0.3], [0.4, 0.5]])) - # def test_value(self, descriptor: DescriptorArray): - # # When Then Expect - # assert descriptor.value == 1 - # def test_set_value(self, descriptor: DescriptorArray): - # # When Then - # descriptor.value = 2 + def test_value(self, descriptor: DescriptorArray): + # When Then Expect + print(descriptor.value) + + assert np.array_equal(descriptor.value, np.array([[1, 2], [3, 4]])) - # # Expect - # assert descriptor._array.value == 2 + def test_set_value(self, descriptor: DescriptorArray): + # When Then + descriptor.value = ([[0.2, 0.3], [0.4, 0.5]]) + # Expect + assert np.array_equal(descriptor._array.values, np.array([[0.2, 0.3], [0.4, 0.5]])) - # def test_repr(self, descriptor: DescriptorArray): - # # When Then - # repr_str = str(descriptor) + def test_repr(self, descriptor: DescriptorArray): + # When Then + repr_str = str(descriptor) - # # Expect - # assert repr_str == "" + # Expect + assert repr_str == "" - # def test_copy(self, descriptor: DescriptorArray): - # # When Then - # descriptor_copy = descriptor.__copy__() + def test_copy(self, descriptor: DescriptorArray): + # When Then + descriptor_copy = descriptor.__copy__() - # # Expect - # assert type(descriptor_copy) == DescriptorArray - # assert descriptor_copy._array.value == descriptor._array.value - # assert descriptor_copy._array.unit == descriptor._array.unit + # Expect + assert type(descriptor_copy) == DescriptorArray + assert 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 Then From 1ca4049de428d21739916fa420a447065dbaab2b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 27 Jan 2025 16:06:41 +0100 Subject: [PATCH 27/48] update tests more --- .../Objects/variable/test_descriptor_array.py | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 730fd765..2f790e75 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -202,56 +202,72 @@ def test_copy(self, descriptor: DescriptorArray): # Expect assert type(descriptor_copy) == DescriptorArray - assert descriptor_copy._array.values == descriptor._array.values + 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 Then - # descriptor_dict = descriptor.as_data_dict() + 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", + } + + # 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"), + ("1000", "dimensionless"), + ("10dm^2", "m^2")], + ids=["scientific_notation", "numbers", "unit_prefix"]) + def test_base_unit(self, unit_string, expected): + # When + descriptor = DescriptorArray(name="name", value=[[1.0, 2.0], [3.0, 4.0]], unit=unit_string) - # # Expect - # assert descriptor_dict == { - # "name": "name", - # "value": 1.0, - # "unit": "m", - # "variance": 0.1, - # "description": "description", - # "url": "url", - # "display_name": "display_name", - # "unique_name": "DescriptorArray_0", - # } - - # @pytest.mark.parametrize("unit_string, expected", [ - # ("1e+9", "dimensionless"), - # ("1000", "dimensionless"), - # ("10dm^2", "m^2")], - # ids=["scientific_notation", "numbers", "unit_prefix"]) - # def test_base_unit(self, unit_string, expected): - # # When - # descriptor = DescriptorArray(name="name", value=1, unit=unit_string) + # Then + base_unit = descriptor._base_unit() - # # Then - # base_unit = descriptor._base_unit() + # Expect + assert base_unit == expected - # # Expect - # assert base_unit == expected + @pytest.mark.parametrize("test, expected", [ + (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test + name", 3, "m", 0.11)), + (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test + name", 102, "cm", 1000.01))], + ids=["regular", "unit_conversion"]) + def test_addition(self, descriptor: DescriptorArray, test, expected): + # When Then + result = test + descriptor +# [[2.0, 3.0], [3.0, 4.0]] + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.array_equal(result.variance, expected.variance) + + assert descriptor.unit == 'm' - # @pytest.mark.parametrize("test, expected", [ - # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test + name", 3, "m", 0.11)), - # (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test + name", 102, "cm", 1000.01))], - # ids=["regular", "unit_conversion"]) - # def test_addition(self, descriptor: DescriptorArray, test, expected): - # # When Then - # result = test + descriptor + # value=[[1., 2.], [3., 4.]], + # unit="m", + # variance=[[0.1, 0.2], [0.3, 0.4]], - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == expected.value - # assert result.unit == expected.unit - # assert result.variance == expected.variance - - # assert descriptor.unit == 'm' # def test_addition_with_array(self): # # When From 1edf993f4745fe8bc09f5b385be516aae9c53e12 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Wed, 19 Feb 2025 15:45:01 +0100 Subject: [PATCH 28/48] minor spelling fixup --- src/easyscience/Objects/variable/descriptor_array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 5b3aa56c..cd97a930 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -196,7 +196,7 @@ def variance(self, variance: Union[list, np.ndarray]) -> None: @property def error(self) -> Optional[np.ndarray]: """ - The standard deviations , calculated as the square root of variances. + The standard deviations, calculated as the square root of variances. :return: A numpy array of standard deviations, or None if variances are not set. """ @@ -269,6 +269,7 @@ def set_array(obj, scalar): def __copy__(self) -> DescriptorArray: return super().__copy__() + def __repr__(self) -> str: """ Return a string representation of the DescriptorArray, showing its name, value, variance, and unit. From 9f17ef066f2a4a36b75414e7961d10abb660a066 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Wed, 19 Feb 2025 16:07:31 +0100 Subject: [PATCH 29/48] work on tests for array addition --- .../Objects/variable/descriptor_array.py | 2 +- .../Objects/variable/test_descriptor_array.py | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index cd97a930..ae9cfe90 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -317,7 +317,7 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: - def __add__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: + def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 2f790e75..dbf4f1ae 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -6,6 +6,7 @@ import numpy as np from easyscience.Objects.variable.descriptor_array import DescriptorArray +from easyscience.Objects.variable.descriptor_number import DescriptorNumber from easyscience import global_object class TestDescriptorArray: @@ -248,9 +249,25 @@ def test_base_unit(self, unit_string, expected): assert base_unit == expected @pytest.mark.parametrize("test, expected", [ - (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test + name", 3, "m", 0.11)), - (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test + name", 102, "cm", 1000.01))], - ids=["regular", "unit_conversion"]) + (DescriptorNumber("test", 2, "m", 0.01), + DescriptorArray("test + name", + [[3.0, 4.0], [5.0, 6.0]], + "m", + [[0.11, 0.11], [0.11, 0.11]])), + (DescriptorNumber("test", 1, "cm", 10), + DescriptorArray("test + name", + [[102.0, 103.0], [104.0, 95.0]], + "m", + [[0.11, 0.11], [0.11, 0.11]])), + (DescriptorArray("test", + [[2.0, 3.0], [4.0, -5.0]], + "cm", + [[0.02, 0.02], [0.02, 0.02]]), + DescriptorArray("test + name", + [[102.0, 203.0], [304.0, 395.0]], + "m", + [[0.12, 0.12], [0.12, 0.12]]))], + ids=["number_regular", "number_unit_conversion", "array_conversion"]) def test_addition(self, descriptor: DescriptorArray, test, expected): # When Then result = test + descriptor From 54f4524eec625f6898bd7f7c4c7a65092f2ba0db Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Thu, 20 Feb 2025 16:54:44 +0100 Subject: [PATCH 30/48] manually perform broadcasting without considering correlations, and raise a warning --- .../Objects/variable/descriptor_array.py | 18 ++++++++-- .../Objects/variable/test_descriptor_array.py | 36 ++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index ae9cfe90..ba8b81c7 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -1,4 +1,5 @@ from __future__ import annotations +from warnings import warn import numbers from typing import Any @@ -351,8 +352,21 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr other_converted.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_converted.full_value + # Addition with a DescriptorNumber that has a variance WILL introduce + # correlations between the elements of the DescriptorArray. + # See, https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 + # However, DescriptorArray does not consider the covariance between + # elements of the array. Hence, the broadcasting is "manually" + # performed to work around `scipp` and a warning raised to the end user. + if (self._array.variances is not None or other.variance is not None): + warn( + 'Correlations introduced by this operation will not be considered.\ + See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) + + broadcasted = sc.broadcast(other_converted.full_value, + dims=self.full_value.dims, + shape=self.full_value.shape).copy() # Ceky copy() to force scipp to perform the broadcast here + new_full_value = self.full_value + broadcasted elif isinstance(other, DescriptorArray): try: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index dbf4f1ae..a3b11a7b 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -248,37 +248,49 @@ def test_base_unit(self, unit_string, expected): # Expect assert base_unit == expected - @pytest.mark.parametrize("test, expected", [ + @pytest.mark.parametrize("test, expected, raises_warning", [ (DescriptorNumber("test", 2, "m", 0.01), DescriptorArray("test + name", [[3.0, 4.0], [5.0, 6.0]], "m", - [[0.11, 0.11], [0.11, 0.11]])), + [[0.11, 0.21], [0.31, 0.41]]), + True), (DescriptorNumber("test", 1, "cm", 10), DescriptorArray("test + name", - [[102.0, 103.0], [104.0, 95.0]], - "m", - [[0.11, 0.11], [0.11, 0.11]])), + [[101.0, 201.0], [301.0, 401.0]], + "cm", + [[1010.0, 2010.0], [3010.0, 4010.0]]), + True), (DescriptorArray("test", [[2.0, 3.0], [4.0, -5.0]], "cm", - [[0.02, 0.02], [0.02, 0.02]]), + [[1.0, 2.0], [3.0, 4.0]]), DescriptorArray("test + name", [[102.0, 203.0], [304.0, 395.0]], - "m", - [[0.12, 0.12], [0.12, 0.12]]))], + "cm", + [[1001.0, 2002.0], [3003.0, 4004.0]]), + False)], ids=["number_regular", "number_unit_conversion", "array_conversion"]) - def test_addition(self, descriptor: DescriptorArray, test, expected): + def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warning): # When Then - result = test + descriptor + + if raises_warning: + with pytest.warns(UserWarning) as record: + result = test + descriptor + assert len(record) == 1 + assert 'Correlations introduced' in record[0].message.args[0] + else: + result = test + descriptor + + + # [[2.0, 3.0], [3.0, 4.0]] # Expect assert type(result) == DescriptorArray assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit - assert np.array_equal(result.variance, expected.variance) - + assert np.allclose(result.variance, expected.variance) assert descriptor.unit == 'm' # value=[[1., 2.], [3., 4.]], From 7a19de5095d15098796a99a00a39f7320a209f70 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Fri, 21 Feb 2025 11:24:12 +0100 Subject: [PATCH 31/48] investigate issue where a DescriptorArray is converted to a set of numpy arrays and then broadcast, somehow --- .../Objects/variable/descriptor_array.py | 19 +++-- .../Objects/variable/test_descriptor_array.py | 79 ++++++++++++++----- 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index ba8b81c7..1d55a58c 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -88,6 +88,8 @@ def __init__( if self.unit is not None: self.convert_unit(self._base_unit()) + self.__array_ufunc__ = None + @classmethod def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorArray: """ @@ -345,6 +347,7 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr new_value = self._array.values + other new_full_value = sc.array(dims=['row', 'column'], values=new_value, unit=self.unit, variances=self._array.variances) + print(new_full_value) elif isinstance(other, DescriptorNumber): try: @@ -362,7 +365,6 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr warn( 'Correlations introduced by this operation will not be considered.\ See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) - broadcasted = sc.broadcast(other_converted.full_value, dims=self.full_value.dims, shape=self.full_value.shape).copy() # Ceky copy() to force scipp to perform the broadcast here @@ -388,9 +390,9 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array - - - + + + def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. @@ -416,6 +418,7 @@ def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndar else: # Delegate to `__add__` for other types (e.g., list, np.ndarray, scalar) + print("reverse adding") return self.__add__(other) @@ -447,15 +450,15 @@ def __rsub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Numbe else: return NotImplemented - def __neg__(self) -> DescriptorNumber: + def __neg__(self) -> DescriptorArray: new_value = -self.full_value - descriptor_array = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array - def __abs__(self) -> DescriptorNumber: + def __abs__(self) -> DescriptorArray: new_value = abs(self.full_value) - descriptor_array = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index a3b11a7b..66fbf2ba 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -24,6 +24,20 @@ def descriptor(self): ) return descriptor + @pytest.fixture + def descriptor_dimensionless(self): + descriptor = DescriptorArray( + name="name", + value=[[1., 2.], [3., 4.], [5., 6.]], + unit="dimensionless", + variance=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + description="description", + url="url", + display_name="display_name", + parent=None, + ) + return descriptor + @pytest.fixture def clear(self): global_object.map._clear() @@ -125,7 +139,7 @@ def test_from_scipp(self): def test_full_value(self, descriptor: DescriptorArray): # When Then Expect - assert descriptor.full_value == sc.array(dims=['row','column'],values=[[1,2],[3,4]], unit='m') + assert descriptor.full_value == sc.array(dims=['row','column'], values=[[1.0, 2.0], [3.0, 4.0]], unit='m') def test_set_full_value(self, descriptor: DescriptorArray): with pytest.raises(AttributeError): @@ -270,33 +284,60 @@ def test_base_unit(self, unit_string, expected): "cm", [[1001.0, 2002.0], [3003.0, 4004.0]]), False)], - ids=["number_regular", "number_unit_conversion", "array_conversion"]) + ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "array_conversion"]) def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warning): # When Then - if raises_warning: with pytest.warns(UserWarning) as record: result = test + descriptor - assert len(record) == 1 + result_reverse = descriptor + test + assert len(record) == 2 assert 'Correlations introduced' in record[0].message.args[0] else: result = test + descriptor - - - -# [[2.0, 3.0], [3.0, 4.0]] + result_reverse = descriptor + test # Expect assert type(result) == DescriptorArray assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit + assert result_reverse.unit == test.unit assert np.allclose(result.variance, expected.variance) assert descriptor.unit == 'm' - # value=[[1., 2.], [3., 4.]], - # unit="m", - # variance=[[0.1, 0.2], [0.3, 0.4]], + @pytest.mark.parametrize("test, expected", [ + (np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]]), + DescriptorArray("test + name", + [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), + ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], + DescriptorArray("test + name", + [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), + (1, + DescriptorArray("test + name", + [[2.0, 3.0], [4.0, 5.0], [6.0, 7.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) + ], + ids=["numpy_array", "list", "number"]) + def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): + # When Then + print(type(test)) + print(test) + result_reverse = test + descriptor_dimensionless + #result = descriptor_dimensionless + test + # Expect + #assert type(result) == DescriptorArray + print(result_reverse) + assert type(result_reverse) == DescriptorArray + return + assert np.array_equal(result.value, expected.value) + assert np.allclose(result.variance, expected.variance) + assert descriptor_dimensionless.unit == 'dimensionless' # def test_addition_with_array(self): # # When @@ -319,13 +360,15 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn # assert result_reverse.unit == "dimensionless" # assert result_reverse.variance == 0.1 - # @pytest.mark.parametrize("test", [1.0, DescriptorArray("test", 2, "s",)], ids=["add_array_to_unit", "incompatible_units"]) - # def test_addition_exception(self, descriptor: DescriptorArray, test): - # # When Then Expect - # with pytest.raises(UnitError): - # result = descriptor + test - # with pytest.raises(UnitError): - # result_reverse = test + descriptor + @pytest.mark.parametrize("test", [ + DescriptorNumber("test", 2, "s"), + DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) + def test_addition_exception(self, descriptor: DescriptorArray, test): + # When Then Expect + with pytest.raises(UnitError): + result = descriptor + test + with pytest.raises(UnitError): + result_reverse = test + descriptor # @pytest.mark.parametrize("test, expected", [ # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test - name", 1, "m", 0.11)), From a6f02b14751d9dd2ceff86076068026c1a9d06e1 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Tue, 25 Feb 2025 09:08:37 +0100 Subject: [PATCH 32/48] manually implement numpy __add__ ufunc compatibility --- .../Objects/variable/descriptor_array.py | 56 ++++++++++-- .../Objects/variable/test_descriptor_array.py | 86 ++++++++++++------- 2 files changed, 106 insertions(+), 36 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 1d55a58c..1e872bd8 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -88,8 +88,6 @@ def __init__( if self.unit is not None: self.convert_unit(self._base_unit()) - self.__array_ufunc__ = None - @classmethod def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorArray: """ @@ -390,9 +388,35 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array - - - + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + """ + Override a subset of the array_ufuncs in Numpy to prioritize our custom + implementations. For example, `ufunc add` will be replaced by our own + `__add__` method to ensure a DescriptorArray is _always_ returned. + + The Numpy ufuncs are only called when performing reverse addition of a + Numpy array with a DescriptorArray, e.g., + `np.array([1, 2, 3]) + DescriptorArray(...)` + Hence, we manually check and refer to the corresponding function. + """ + if method == '__call__': + print(ufunc.__name__) + if ufunc.__name__ == 'add': + assert len(inputs) == 2, "`add` takes two inputs" + in0, in1 = inputs + other = in1 if isinstance(in0, DescriptorArray) else in0 + return self.__add__(other) + return NotImplemented + + def __array_function__(self, func, types, args, kwargs): + """ + DescriptorArray does not generally support Numpy array functions. + For example, `np.sin(descriptorArray: DescriptorArray)` should fail. + Modify this function if you want to add such functionality. + """ + return NotImplemented + def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. @@ -418,7 +442,6 @@ def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndar else: # Delegate to `__add__` for other types (e.g., list, np.ndarray, scalar) - print("reverse adding") return self.__add__(other) @@ -462,6 +485,27 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array + # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # new_value = self.full_value * other + # elif type(other) is DescriptorArray: + # new_value = self.full_value * other.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.convert_unit(descriptor_number._base_unit()) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + + # def __rmul__(self, other: numbers.Number) -> DescriptorArray: + # if isinstance(other, numbers.Number): + # new_value = other * self.full_value + # else: + # return NotImplemented + # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + # descriptor_number.name = descriptor_number.unique_name + # return descriptor_number + # TODO: add arithmetic operations # They should be allowed between DescriptorArray and numbers, and between DescriptorArray and DescriptorArray. diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 66fbf2ba..eec69949 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -139,7 +139,13 @@ def test_from_scipp(self): def test_full_value(self, descriptor: DescriptorArray): # When Then Expect - assert descriptor.full_value == sc.array(dims=['row','column'], values=[[1.0, 2.0], [3.0, 4.0]], unit='m') + other = sc.array(dims=('row','column'), + values=[[1.0, 2.0], [3.0, 4.0]], + unit='m', + variances=[[0.1, 0.2], [0.3, 0.4]]) + print(other.shape, descriptor.full_value.shape) + print(descriptor.full_value.dims, other.dims) + assert descriptor.full_value == other def test_set_full_value(self, descriptor: DescriptorArray): with pytest.raises(AttributeError): @@ -159,7 +165,6 @@ def test_convert_unit(self, descriptor: DescriptorArray): # Expect assert descriptor._array.unit == 'mm' - print(descriptor._array.variances) assert np.array_equal(descriptor._array.values,[[1000,2000],[3000,4000]]) assert np.array_equal(descriptor._array.variances,[[100000,200000],[300000,400000]]) @@ -174,7 +179,6 @@ def test_set_variance(self, descriptor: DescriptorArray): # Expect assert np.array_equal(descriptor.variance, np.array([[0.2, 0.3], [0.4, 0.5]])) - assert np.array_equal(descriptor.error, np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]]))) def test_error(self, descriptor: DescriptorArray): @@ -185,8 +189,6 @@ def test_error(self, descriptor: DescriptorArray): def test_set_error(self, descriptor: DescriptorArray): # When Then descriptor.error = np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]])) - print(descriptor.variance) - print(descriptor.error) # Expect assert np.allclose(descriptor.error, np.sqrt(np.array([[0.2, 0.3], [0.4, 0.5]]))) assert np.allclose(descriptor.variance, np.array([[0.2, 0.3], [0.4, 0.5]])) @@ -194,8 +196,6 @@ def test_set_error(self, descriptor: DescriptorArray): def test_value(self, descriptor: DescriptorArray): # When Then Expect - print(descriptor.value) - assert np.array_equal(descriptor.value, np.array([[1, 2], [3, 4]])) def test_set_value(self, descriptor: DescriptorArray): @@ -244,8 +244,6 @@ def test_as_data_dict(self, clear, descriptor: DescriptorArray): 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"), @@ -301,24 +299,24 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit - assert result_reverse.unit == test.unit + assert result_reverse.unit == descriptor.unit assert np.allclose(result.variance, expected.variance) assert descriptor.unit == 'm' @pytest.mark.parametrize("test, expected", [ (np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]]), - DescriptorArray("test + name", + DescriptorArray("test", [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], "dimensionless", [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], - DescriptorArray("test + name", + DescriptorArray("test", [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], "dimensionless", [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), (1, - DescriptorArray("test + name", + DescriptorArray("test", [[2.0, 3.0], [4.0, 5.0], [6.0, 7.0]], "dimensionless", [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) @@ -326,19 +324,57 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn ids=["numpy_array", "list", "number"]) def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): # When Then - print(type(test)) - print(test) result_reverse = test + descriptor_dimensionless - #result = descriptor_dimensionless + test + result = descriptor_dimensionless + test # Expect - #assert type(result) == DescriptorArray - print(result_reverse) + assert type(result) == DescriptorArray assert type(result_reverse) == DescriptorArray - return assert np.array_equal(result.value, expected.value) assert np.allclose(result.variance, expected.variance) assert descriptor_dimensionless.unit == 'dimensionless' - + + @pytest.mark.parametrize("test", [ + DescriptorNumber("test", 2, "s"), + DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) + def test_addition_exception(self, descriptor: DescriptorArray, test): + # When Then Expect + with pytest.raises(UnitError): + result = descriptor + test + with pytest.raises(UnitError): + result_reverse = test + descriptor + + @pytest.mark.parametrize("function,test", [ + (np.add, np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]])), + (np.add, 1) + ], + ids=["numpy_array", "integer"]) + def test_numpy_ufuncs(self, descriptor_dimensionless, function, test): + """ + The Numpy ufunc versions of add, sub, mul, div, abs and neg + needs to work in order ensure compatibility with + Numpy functions, like `np.sin`, should not work on a + DescriptorArray. + """ + result = function(descriptor_dimensionless, test) + result_reverse = function(test, descriptor_dimensionless) + assert type(result) == DescriptorArray + + @pytest.mark.parametrize("function", [ + np.sin, + np.cos, + np.exp + ], + ids=["sin", "cos", "exp"]) + def test_numpy_ufuncs_exception(self, descriptor_dimensionless, function): + (np.add,np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]])), + """ + Not implemented ufuncs should return NotImplemented. + """ + test = np.array([[1, 2], [3, 4]]) + with pytest.raises(TypeError) as e: + function(descriptor_dimensionless, test) + assert 'returned NotImplemented from' in str(e) + # def test_addition_with_array(self): # # When # descriptor = DescriptorArray(name="name", value=1, variance=0.1) @@ -359,16 +395,6 @@ def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, # assert result_reverse.value == 2.0 # assert result_reverse.unit == "dimensionless" # assert result_reverse.variance == 0.1 - - @pytest.mark.parametrize("test", [ - DescriptorNumber("test", 2, "s"), - DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) - def test_addition_exception(self, descriptor: DescriptorArray, test): - # When Then Expect - with pytest.raises(UnitError): - result = descriptor + test - with pytest.raises(UnitError): - result_reverse = test + descriptor # @pytest.mark.parametrize("test, expected", [ # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test - name", 1, "m", 0.11)), From 2d9978e568fe10c4acee8f3f66e4b3b07782dcae Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Tue, 25 Feb 2025 10:50:40 +0100 Subject: [PATCH 33/48] add wrapper function for handling type conversions for basic operations --- .../Objects/variable/descriptor_array.py | 122 +++++++++++------- .../Objects/variable/test_descriptor_array.py | 83 ++++++++++++ 2 files changed, 159 insertions(+), 46 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 1e872bd8..5e1c52bd 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -1,4 +1,5 @@ from __future__ import annotations +import operator from warnings import warn import numbers @@ -315,25 +316,22 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict - - - - def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number], operator: str) -> DescriptorArray: """ - Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. + Perform element-wise operations with another DescriptorNumber, DescriptorArray, numpy array, list, or number. - :param other: The object to add. Must be a DescriptorArray or DescriptorNumber with compatible units, + :param other: The object to operate on. Must be a DescriptorArray or DescriptorNumber with compatible units, or a numpy array/list with the same shape if the DescriptorArray is dimensionless. - :return: A new DescriptorArray representing the result of the addition. + :return: A new DescriptorArray representing the result of the operation. """ if isinstance(other, numbers.Number): if self.unit not in [None, "dimensionless"]: - raise UnitError("Numbers can only be added to dimensionless values") - new_full_value = self.full_value + other # scipp can handle addition with numbers + raise UnitError("Numbers can only be used together with dimensionless values") + new_full_value = operator(self.full_value, other) elif isinstance(other, (list, np.ndarray)): if self.unit not in [None, "dimensionless"]: - raise UnitError("Addition with numpy arrays or lists is only allowed for dimensionless values") + raise UnitError("Operations with numpy arrays or lists are only allowed for dimensionless values") # Convert `other` to numpy array if it's a list if isinstance(other, list): @@ -343,52 +341,73 @@ def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarr if other.shape != self._array.values.shape: raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") - new_value = self._array.values + other + new_value = operator(self._array.values, other) new_full_value = sc.array(dims=['row', 'column'], values=new_value, unit=self.unit, variances=self._array.variances) - print(new_full_value) elif isinstance(other, DescriptorNumber): try: other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None - # Addition with a DescriptorNumber that has a variance WILL introduce + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be multiplied") from None + # Operations with a DescriptorNumber that has a variance WILL introduce # correlations between the elements of the DescriptorArray. # See, https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 # However, DescriptorArray does not consider the covariance between # elements of the array. Hence, the broadcasting is "manually" # performed to work around `scipp` and a warning raised to the end user. if (self._array.variances is not None or other.variance is not None): - warn( - 'Correlations introduced by this operation will not be considered.\ - See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) + warn('Correlations introduced by this operation will not be considered.\ + See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) broadcasted = sc.broadcast(other_converted.full_value, dims=self.full_value.dims, - shape=self.full_value.shape).copy() # Ceky copy() to force scipp to perform the broadcast here - new_full_value = self.full_value + broadcasted + shape=self.full_value.shape).copy() # Cheeky copy() to force scipp to perform the broadcast here + new_full_value = operator(self.full_value, broadcasted) elif isinstance(other, DescriptorArray): try: other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be multiplied") from None # Ensure dimensions match if self.full_value.dims != other_converted.full_value.dims: raise ValueError(f"Dimensions of the DescriptorArrays do not match: " f"{self.full_value.dims} vs {other_converted.full_value.dims}") - new_full_value = self.full_value + other_converted.full_value + new_full_value = operator(self.full_value, other_converted.full_value) else: return NotImplemented - descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array + def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number], operator: str) -> DescriptorArray: + """ + Handle reverse operations for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. + Ensures unit compatibility when `other` is a DescriptorNumber. + """ + if isinstance(other, DescriptorArray): + # Delegate reverse multiplication to `other`, respecting unit compatibility + return operator(other, self) + elif isinstance(other, DescriptorNumber): + # Ensure unit compatibility for DescriptorNumber + original_unit = self.unit + try: + self.convert_unit(other.unit) # Convert `self` to `other`'s unit + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + + result = operator(self, other) + # Revert `self` to its original unit + self.convert_unit(original_unit) + return result + else: + # Delegate to operation to __self__ for other types (e.g., list, np.ndarray, scalar) + return operator(self, other) + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """ Override a subset of the array_ufuncs in Numpy to prioritize our custom @@ -401,49 +420,42 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): Hence, we manually check and refer to the corresponding function. """ if method == '__call__': - print(ufunc.__name__) if ufunc.__name__ == 'add': assert len(inputs) == 2, "`add` takes two inputs" in0, in1 = inputs other = in1 if isinstance(in0, DescriptorArray) else in0 return self.__add__(other) + elif ufunc.__name__ == 'multiply': + assert len(inputs) == 2, "`multiply` takes two inputs" + in0, in1 = inputs + other = in1 if isinstance(in0, DescriptorArray) else in0 + return self.__mul__(other) return NotImplemented def __array_function__(self, func, types, args, kwargs): """ DescriptorArray does not generally support Numpy array functions. - For example, `np.sin(descriptorArray: DescriptorArray)` should fail. + For example, `np.argwhere(descriptorArray: DescriptorArray)` should fail. Modify this function if you want to add such functionality. """ return NotImplemented + + def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. + + :param other: The object to add. Must be a DescriptorArray or DescriptorNumber with compatible units, + or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the addition. + """ + return self._smooth_operator(other, operator.add) def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. Ensures unit compatibility when `other` is a DescriptorNumber. """ - if isinstance(other, DescriptorArray): - # Delegate reverse addition to `other`, respecting unit compatibility - return other.__add__(self) - - elif isinstance(other, DescriptorNumber): - # Ensure unit compatibility for DescriptorNumber - original_unit = self.unit - try: - self.convert_unit(other.unit) # Convert `self` to `other`'s unit - except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None - - result = self.__add__(other) - - # Revert `self` to its original unit - self.convert_unit(original_unit) - return result - - else: - # Delegate to `__add__` for other types (e.g., list, np.ndarray, scalar) - return self.__add__(other) - + return self._rsmooth_operator(other, operator.add) def __sub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ @@ -485,6 +497,24 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array + + def __mul__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise multiplication with another DescriptorNumber, DescriptorArray, numpy array, list, or number. + + :param other: The object to multiply. Must be a DescriptorArray or DescriptorNumber with compatible units, + or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the addition. + """ + return self._smooth_operator(other, operator.mul) + + def __rmul__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + """ + Handle reverse multiplication for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. + Ensures unit compatibility when `other` is a DescriptorNumber. + """ + return self._rsmooth_operator(other, operator.mul) + # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: # if isinstance(other, numbers.Number): # new_value = self.full_value * other diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index eec69949..d2aacc44 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -343,6 +343,89 @@ def test_addition_exception(self, descriptor: DescriptorArray, test): with pytest.raises(UnitError): result_reverse = test + descriptor + @pytest.mark.parametrize("test, expected, raises_warning", [ + (DescriptorNumber("test", 2, "m", 0.01), + DescriptorArray("test * name", + [[2.0, 4.0], [6.0, 8.0]], + "m^2", + [[0.41, 0.84], [1.29, 1.76]]), + True), + (DescriptorNumber("test", 1, "cm", 10), + DescriptorArray("test * name", + [[100.0, 200.0], [300.0, 400.0]], + "cm^2", + [[101000.0, 402000.0], [903000.0, 1604000.0]]), + True), + (DescriptorArray("test", + [[2.0, 3.0], [4.0, -5.0]], + "cm", + [[1.0, 2.0], [3.0, 4.0]]), + DescriptorArray("test * name", + [[200.0, 600.0], [1200.0, -2000.0]], + "cm^2", + [[14000.0, 98000.0], [318000.0, 740000.0]]), + False)], + ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "array_conversion"]) + def test_multiplication(self, descriptor: DescriptorArray, test, expected, raises_warning): + # When Then + if raises_warning: + with pytest.warns(UserWarning) as record: + result = test * descriptor + result_reverse = descriptor * test + assert len(record) == 2 + assert 'Correlations introduced' in record[0].message.args[0] + else: + result = test * descriptor + result_reverse = descriptor * test + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert result_reverse.unit == 'm^2' + assert descriptor.unit == 'm' + + + @pytest.mark.parametrize("test, expected", [ + (np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]]), + DescriptorArray("test", + [[2.0, 6.0], [12.0, -20.0], [11.0, -2.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), + ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], + DescriptorArray("test", + [[2.0, 6.0], [12.0, -20.0], [30.0, -48.0]], + "dimensionless", + [[0.4, 1.8], [0.3, 0.4], [0.5, 0.6]])), + (1.5, + DescriptorArray("test", + [[1.5, 3.0], [4.5, 6.0], [7.5, 9.0]], + "dimensionless", + [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]])) + ], + ids=["numpy_array", "list", "number"]) + def test_multiplication_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): + # When Then + result_reverse = test * descriptor_dimensionless + result = descriptor_dimensionless * test + # Expect + assert type(result) == DescriptorArray + assert type(result_reverse) == DescriptorArray + assert np.array_equal(result.value, expected.value) + assert np.allclose(result.variance, expected.variance) + assert descriptor_dimensionless.unit == 'dimensionless' + + @pytest.mark.parametrize("test", [ + DescriptorNumber("test", 2, "s"), + DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) + def test_addition_exception(self, descriptor: DescriptorArray, test): + # When Then Expect + with pytest.raises(UnitError): + result = descriptor * test + with pytest.raises(UnitError): + result_reverse = test * descriptor + @pytest.mark.parametrize("function,test", [ (np.add, np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]])), (np.add, 1) From bc93a906d1db83dee3d4b4122689ec8939946876 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Tue, 25 Feb 2025 11:29:47 +0100 Subject: [PATCH 34/48] drop support for operations with Numpy arrays --- .../Objects/variable/descriptor_array.py | 40 +++++---------- .../Objects/variable/test_descriptor_array.py | 51 ++++++------------- 2 files changed, 28 insertions(+), 63 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 5e1c52bd..00a2b252 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -329,20 +329,19 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, raise UnitError("Numbers can only be used together with dimensionless values") new_full_value = operator(self.full_value, other) - elif isinstance(other, (list, np.ndarray)): + elif isinstance(other, list): if self.unit not in [None, "dimensionless"]: raise UnitError("Operations with numpy arrays or lists are only allowed for dimensionless values") # Convert `other` to numpy array if it's a list - if isinstance(other, list): - other = np.array(other) + other = np.array(other) # Ensure dimensions match if other.shape != self._array.values.shape: raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") new_value = operator(self._array.values, other) - new_full_value = sc.array(dims=['row', 'column'], values=new_value, unit=self.unit, variances=self._array.variances) + new_full_value = sc.array(dims=self._array.dims, values=new_value, unit=self.unit, variances=self._array.variances) elif isinstance(other, DescriptorNumber): try: @@ -360,8 +359,8 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, warn('Correlations introduced by this operation will not be considered.\ See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) broadcasted = sc.broadcast(other_converted.full_value, - dims=self.full_value.dims, - shape=self.full_value.shape).copy() # Cheeky copy() to force scipp to perform the broadcast here + dims=self._array.dims, + shape=self._array.shape).copy() # Cheeky copy() to force scipp to perform the broadcast here new_full_value = operator(self.full_value, broadcasted) elif isinstance(other, DescriptorArray): @@ -380,6 +379,7 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, else: return NotImplemented + descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_full_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array @@ -408,28 +408,12 @@ def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list # Delegate to operation to __self__ for other types (e.g., list, np.ndarray, scalar) return operator(self, other) - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """ - Override a subset of the array_ufuncs in Numpy to prioritize our custom - implementations. For example, `ufunc add` will be replaced by our own - `__add__` method to ensure a DescriptorArray is _always_ returned. - - The Numpy ufuncs are only called when performing reverse addition of a - Numpy array with a DescriptorArray, e.g., - `np.array([1, 2, 3]) + DescriptorArray(...)` - Hence, we manually check and refer to the corresponding function. - """ - if method == '__call__': - if ufunc.__name__ == 'add': - assert len(inputs) == 2, "`add` takes two inputs" - in0, in1 = inputs - other = in1 if isinstance(in0, DescriptorArray) else in0 - return self.__add__(other) - elif ufunc.__name__ == 'multiply': - assert len(inputs) == 2, "`multiply` takes two inputs" - in0, in1 = inputs - other = in1 if isinstance(in0, DescriptorArray) else in0 - return self.__mul__(other) + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + """ + DescriptorArray does not generally support Numpy array functions. + For example, `np.argwhere(descriptorArray: DescriptorArray)` should fail. + Modify this function if you want to add such functionality. + """ return NotImplemented def __array_function__(self, func, types, args, kwargs): diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index d2aacc44..17d42413 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -305,11 +305,6 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn @pytest.mark.parametrize("test, expected", [ - (np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]]), - DescriptorArray("test", - [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], - "dimensionless", - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray("test", [[3.0, 5.0], [7.0, -1.0], [11.0, -2.0]], @@ -321,7 +316,7 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn "dimensionless", [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) ], - ids=["numpy_array", "list", "number"]) + ids=["list", "number"]) def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): # When Then result_reverse = test + descriptor_dimensionless @@ -388,11 +383,6 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise @pytest.mark.parametrize("test, expected", [ - (np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]]), - DescriptorArray("test", - [[2.0, 6.0], [12.0, -20.0], [11.0, -2.0]], - "dimensionless", - [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], DescriptorArray("test", [[2.0, 6.0], [12.0, -20.0], [30.0, -48.0]], @@ -404,7 +394,7 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise "dimensionless", [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]])) ], - ids=["numpy_array", "list", "number"]) + ids=["list", "number"]) def test_multiplication_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): # When Then result_reverse = test * descriptor_dimensionless @@ -419,35 +409,26 @@ def test_multiplication_dimensionless(self, descriptor_dimensionless: Descriptor @pytest.mark.parametrize("test", [ DescriptorNumber("test", 2, "s"), DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) - def test_addition_exception(self, descriptor: DescriptorArray, test): + def test_operation_exception(self, descriptor: DescriptorArray, test): # When Then Expect - with pytest.raises(UnitError): - result = descriptor * test - with pytest.raises(UnitError): - result_reverse = test * descriptor - - @pytest.mark.parametrize("function,test", [ - (np.add, np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]])), - (np.add, 1) - ], - ids=["numpy_array", "integer"]) - def test_numpy_ufuncs(self, descriptor_dimensionless, function, test): - """ - The Numpy ufunc versions of add, sub, mul, div, abs and neg - needs to work in order ensure compatibility with - Numpy functions, like `np.sin`, should not work on a - DescriptorArray. - """ - result = function(descriptor_dimensionless, test) - result_reverse = function(test, descriptor_dimensionless) - assert type(result) == DescriptorArray + import operator + operators = [operator.add, + operator.mul] + + for operator in operators: # This can probably be done better w. fixture + with pytest.raises(UnitError): + result = operator(descriptor, test) + with pytest.raises(UnitError): + result_reverse = operator(test, descriptor) @pytest.mark.parametrize("function", [ np.sin, np.cos, - np.exp + np.exp, + np.add, + np.multiply ], - ids=["sin", "cos", "exp"]) + ids=["sin", "cos", "exp", "add", "multiply"]) def test_numpy_ufuncs_exception(self, descriptor_dimensionless, function): (np.add,np.array([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]])), """ From 75483ef8ba7a6e36d52ae6f63c544542c5686e4d Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Tue, 25 Feb 2025 11:39:32 +0100 Subject: [PATCH 35/48] let scipp handle array multiplication for uncertainty propagation --- src/easyscience/Objects/variable/descriptor_array.py | 11 +++++------ .../Objects/variable/test_descriptor_array.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 00a2b252..ce2a0b7a 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -333,15 +333,14 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, if self.unit not in [None, "dimensionless"]: raise UnitError("Operations with numpy arrays or lists are only allowed for dimensionless values") - # Convert `other` to numpy array if it's a list - other = np.array(other) # Ensure dimensions match - if other.shape != self._array.values.shape: + if np.shape(other) != self._array.values.shape: raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") - - new_value = operator(self._array.values, other) - new_full_value = sc.array(dims=self._array.dims, values=new_value, unit=self.unit, variances=self._array.variances) + + other = sc.array(dims=self._array.dims, values=other) + new_full_value = operator(self._array, other) + # new_full_value = sc.array(dims=self._array.dims, values=new_value.values, unit=self.unit, variances=new_value.variances) elif isinstance(other, DescriptorNumber): try: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 17d42413..dac2ec85 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -387,7 +387,7 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise DescriptorArray("test", [[2.0, 6.0], [12.0, -20.0], [30.0, -48.0]], "dimensionless", - [[0.4, 1.8], [0.3, 0.4], [0.5, 0.6]])), + [[0.4, 1.8], [4.8, 10.0], [18.0, 38.4]])), (1.5, DescriptorArray("test", [[1.5, 3.0], [4.5, 6.0], [7.5, 9.0]], From e5a15744da2a7b74e99d32d263b0fda4822d9583 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Wed, 26 Feb 2025 16:44:20 +0100 Subject: [PATCH 36/48] add division; revorked unit handling to make division work --- .../Objects/variable/descriptor_array.py | 163 ++++++++----- .../Objects/variable/test_descriptor_array.py | 217 ++++++++++++++++-- 2 files changed, 310 insertions(+), 70 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index ce2a0b7a..de0643d1 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -1,5 +1,5 @@ from __future__ import annotations -import operator +import operator as op from warnings import warn import numbers @@ -316,12 +316,13 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict - def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number], operator: str) -> DescriptorArray: + def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], operator: str, units_must_match: bool = True) -> DescriptorArray: """ - Perform element-wise operations with another DescriptorNumber, DescriptorArray, numpy array, list, or number. + Perform element-wise operations with another DescriptorNumber, DescriptorArray, list, or number. :param other: The object to operate on. Must be a DescriptorArray or DescriptorNumber with compatible units, - or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + or a list with the same shape if the DescriptorArray is dimensionless. + :param operator: The operation to perform :return: A new DescriptorArray representing the result of the operation. """ if isinstance(other, numbers.Number): @@ -333,21 +334,20 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, if self.unit not in [None, "dimensionless"]: raise UnitError("Operations with numpy arrays or lists are only allowed for dimensionless values") - # Ensure dimensions match if np.shape(other) != self._array.values.shape: raise ValueError(f"Shape of {other=} must match the shape of DescriptorArray values") other = sc.array(dims=self._array.dims, values=other) - new_full_value = operator(self._array, other) - # new_full_value = sc.array(dims=self._array.dims, values=new_value.values, unit=self.unit, variances=new_value.variances) - + new_full_value = operator(self._array, other) # Let scipp handle operation for uncertainty propagation + elif isinstance(other, DescriptorNumber): try: other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be multiplied") from None + if units_must_match: + raise UnitError(f"Values with units {self.unit} and {other.unit} are not compatible") from None # Operations with a DescriptorNumber that has a variance WILL introduce # correlations between the elements of the DescriptorArray. # See, https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 @@ -367,7 +367,8 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, other_converted = other.__copy__() other_converted.convert_unit(self.unit) except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be multiplied") from None + if units_must_match: + raise UnitError(f"Values with units {self.unit} and {other.unit} are incompatible") from None # Ensure dimensions match if self.full_value.dims != other_converted.full_value.dims: @@ -383,13 +384,14 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, descriptor_array.name = descriptor_array.unique_name return descriptor_array - def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number], operator: str) -> DescriptorArray: + def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], operator: str, units_must_match: bool = True) -> DescriptorArray: """ - Handle reverse operations for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. + Handle reverse operations for DescriptorArrays, DescriptorNumbers, lists, and scalars. Ensures unit compatibility when `other` is a DescriptorNumber. """ + reversed_operator = lambda a, b : operator(b, a) if isinstance(other, DescriptorArray): - # Delegate reverse multiplication to `other`, respecting unit compatibility + # This is probably never called return operator(other, self) elif isinstance(other, DescriptorNumber): # Ensure unit compatibility for DescriptorNumber @@ -397,15 +399,19 @@ def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list try: self.convert_unit(other.unit) # Convert `self` to `other`'s unit except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None - - result = operator(self, other) + # Only allowed operations with different units are + # multiplication and division. We try to convert + # the units for mul/div, but if the conversion + # fails it's no big deal. + if units_must_match: + raise UnitError(f"Values with units {self.unit} and {other.unit} are incompatible") from None + result = self._smooth_operator(other, reversed_operator, units_must_match) # Revert `self` to its original unit self.convert_unit(original_unit) return result else: - # Delegate to operation to __self__ for other types (e.g., list, np.ndarray, scalar) - return operator(self, other) + # Delegate to operation to __self__ for other types (e.g., list, scalar) + return self._smooth_operator(other, reversed_operator, units_must_match) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """ @@ -423,50 +429,120 @@ def __array_function__(self, func, types, args, kwargs): """ return NotImplemented - def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + def __add__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: """ - Perform element-wise addition with another DescriptorNumber, DescriptorArray, numpy array, list, or number. + Perform element-wise addition with another DescriptorNumber, DescriptorArray, list, or number. :param other: The object to add. Must be a DescriptorArray or DescriptorNumber with compatible units, - or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + or a list with the same shape if the DescriptorArray is dimensionless. :return: A new DescriptorArray representing the result of the addition. """ - return self._smooth_operator(other, operator.add) + return self._smooth_operator(other, op.add) - def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: + def __radd__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: """ - Handle reverse addition for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. + Handle reverse addition for DescriptorArrays, DescriptorNumbers, lists, and scalars. Ensures unit compatibility when `other` is a DescriptorNumber. """ - return self._rsmooth_operator(other, operator.add) + return self._rsmooth_operator(other, op.add) def __sub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: """ - Perform element-wise subtraction with another DescriptorArray, numpy array, list, or number. + Perform element-wise subtraction with another DescriptorArray, list, or number. :param other: The object to subtract. Must be a DescriptorArray with compatible units, - or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + or a list with the same shape if the DescriptorArray is dimensionless. :return: A new DescriptorArray representing the result of the subtraction. """ - if isinstance(other, (DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number)): + if isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): # Leverage __neg__ and __add__ for subtraction - return self.__add__(-other) + if isinstance(other, list): + # Use numpy to negate all elements of the list + value = (-np.array(other)).tolist() + else: + value = -other + return self.__add__(value) else: return NotImplemented - def __rsub__(self, other: Union[DescriptorArray, list, np.ndarray, numbers.Number]) -> DescriptorArray: + def __rsub__(self, other: Union[DescriptorArray, list, numbers.Number]) -> DescriptorArray: """ - Perform element-wise subtraction with another DescriptorArray, numpy array, list, or number. + Perform element-wise subtraction with another DescriptorArray, list, or number. :param other: The object to subtract. Must be a DescriptorArray with compatible units, - or a numpy array/list with the same shape if the DescriptorArray is dimensionless. + or a list with the same shape if the DescriptorArray is dimensionless. :return: A new DescriptorArray representing the result of the subtraction. """ - if isinstance(other, (DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number)): - # Leverage __neg__ and __add__ for subtraction - return -(self.__radd__(-other)) + if isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): + if isinstance(other, list): + # Use numpy to negate all elements of the list + value = (-np.array(other)).tolist() + else: + value = -other + return -(self.__radd__(value)) else: return NotImplemented + + def __mul__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise multiplication with another DescriptorNumber, DescriptorArray, list, or number. + + :param other: The object to multiply. Must be a DescriptorArray or DescriptorNumber with compatible units, + or a list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the addition. + """ + if not isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): + return NotImplemented + return self._smooth_operator(other, op.mul, units_must_match=False) + + def __rmul__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: + """ + Handle reverse multiplication for DescriptorArrays, DescriptorNumbers, lists, and scalars. + Ensures unit compatibility when `other` is a DescriptorNumber. + """ + if not isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): + return NotImplemented + return self._rsmooth_operator(other, op.mul, units_must_match=False) + + def __truediv__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise division with another DescriptorNumber, DescriptorArray, list, or number. + + :param other: The object to use as a denominator. Must be a DescriptorArray or DescriptorNumber with compatible units, + or a list with the same shape if the DescriptorArray is dimensionless. + :return: A new DescriptorArray representing the result of the addition. + """ + if not isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): + return NotImplemented + + if isinstance(other, numbers.Number): + original_other = other + elif isinstance(other, (numbers.Number, list)): + original_other = np.array(other) + elif isinstance(other, DescriptorNumber): + original_other = other.value + elif isinstance(other, DescriptorArray): + original_other = other.full_value.values + + if np.any(original_other == 0): + raise ZeroDivisionError('Cannot divide by zero') + return self._smooth_operator(other, op.truediv, units_must_match=False) + + def __rtruediv__(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number]) -> DescriptorArray: + """ + Handle reverse division for DescriptorArrays, DescriptorNumbers, lists, and scalars. + Ensures unit compatibility when `other` is a DescriptorNumber. + """ + if not isinstance(other, (DescriptorArray, DescriptorNumber, list, numbers.Number)): + return NotImplemented + + if np.any(self.full_value.values == 0): + raise ZeroDivisionError('Cannot divide by zero') + + # First use __div__ to compute `self / other` + # but first converting to the units of other + inverse_result = self._rsmooth_operator(other, op.truediv, units_must_match=False) + return inverse_result def __neg__(self) -> DescriptorArray: new_value = -self.full_value @@ -480,23 +556,6 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array - - def __mul__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: - """ - Perform element-wise multiplication with another DescriptorNumber, DescriptorArray, numpy array, list, or number. - - :param other: The object to multiply. Must be a DescriptorArray or DescriptorNumber with compatible units, - or a numpy array/list with the same shape if the DescriptorArray is dimensionless. - :return: A new DescriptorArray representing the result of the addition. - """ - return self._smooth_operator(other, operator.mul) - - def __rmul__(self, other: Union[DescriptorArray, DescriptorNumber, list, np.ndarray, numbers.Number]) -> DescriptorArray: - """ - Handle reverse multiplication for DescriptorArrays, DescriptorNumbers, numpy arrays, lists, and scalars. - Ensures unit compatibility when `other` is a DescriptorNumber. - """ - return self._rsmooth_operator(other, operator.mul) # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: # if isinstance(other, numbers.Number): diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index dac2ec85..34776113 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -319,8 +319,8 @@ def test_addition(self, descriptor: DescriptorArray, test, expected, raises_warn ids=["list", "number"]) def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): # When Then - result_reverse = test + descriptor_dimensionless - result = descriptor_dimensionless + test + result = test + descriptor_dimensionless + result_reverse = descriptor_dimensionless + test # Expect assert type(result) == DescriptorArray assert type(result_reverse) == DescriptorArray @@ -328,16 +328,78 @@ def test_addition_dimensionless(self, descriptor_dimensionless: DescriptorArray, assert np.allclose(result.variance, expected.variance) assert descriptor_dimensionless.unit == 'dimensionless' - @pytest.mark.parametrize("test", [ - DescriptorNumber("test", 2, "s"), - DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) - def test_addition_exception(self, descriptor: DescriptorArray, test): - # When Then Expect - with pytest.raises(UnitError): - result = descriptor + test - with pytest.raises(UnitError): - result_reverse = test + descriptor - + @pytest.mark.parametrize("test, expected, raises_warning", [ + (DescriptorNumber("test", 2, "m", 0.01), + DescriptorArray("test + name", + [[1.0, 0.0], [-1.0, -2.0]], + "m", + [[0.11, 0.21], [0.31, 0.41]]), + True), + (DescriptorNumber("test", 1, "cm", 10), + DescriptorArray("test + name", + [[-99.0, -199.0], [-299.0, -399.0]], + "cm", + [[1010.0, 2010.0], [3010.0, 4010.0]]), + True), + (DescriptorArray("test", + [[2.0, 3.0], [4.0, -5.0]], + "cm", + [[1.0, 2.0], [3.0, 4.0]]), + DescriptorArray("test + name", + [[-98.0, -197.0], [-296.0, -405.0]], + "cm", + [[1001.0, 2002.0], [3003.0, 4004.0]]), + False)], + ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "array_conversion"]) + def test_subtraction(self, descriptor: DescriptorArray, test, expected, raises_warning): + # When Then + if raises_warning: + with pytest.warns(UserWarning) as record: + result = test - descriptor + result_reverse = descriptor - test + assert len(record) == 2 + assert 'Correlations introduced' in record[0].message.args[0] + else: + result = test - descriptor + result_reverse = descriptor - test + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert result_reverse.unit == descriptor.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor.unit == 'm' + # Convert units and check that reverse result is the same + result_reverse.convert_unit(result.unit) + assert np.array_equal(result.value, -result_reverse.value) + + @pytest.mark.parametrize("test, expected", [ + ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], + DescriptorArray("test", + [[1.0, 1.0], [1.0, -9.0], [1.0, -14.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])), + (1, + DescriptorArray("test", + [[0.0, -1.0], [-2.0, -3.0], [-4.0, -5.0]], + "dimensionless", + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])) + ], + ids=["list", "number"]) + def test_subtraction_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): + # When Then + result = test - descriptor_dimensionless + result_reverse = descriptor_dimensionless - test + # Expect + assert type(result) == DescriptorArray + assert type(result_reverse) == DescriptorArray + assert np.array_equal(result.value, expected.value) + assert np.array_equal(result.value, -result_reverse.value) + assert np.allclose(result.variance, expected.variance) + assert descriptor_dimensionless.unit == 'dimensionless' + + @pytest.mark.parametrize("test, expected, raises_warning", [ (DescriptorNumber("test", 2, "m", 0.01), DescriptorArray("test * name", @@ -351,6 +413,12 @@ def test_addition_exception(self, descriptor: DescriptorArray, test): "cm^2", [[101000.0, 402000.0], [903000.0, 1604000.0]]), True), + (DescriptorNumber("test", 1, "kg", 10), + DescriptorArray("test * name", + [[1.0, 2.0], [3.0, 4.0]], + "kg*m", + [[10.1, 40.2], [90.3, 160.4]]), + True), (DescriptorArray("test", [[2.0, 3.0], [4.0, -5.0]], "cm", @@ -360,7 +428,10 @@ def test_addition_exception(self, descriptor: DescriptorArray, test): "cm^2", [[14000.0, 98000.0], [318000.0, 740000.0]]), False)], - ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "array_conversion"]) + ids=["descriptor_number_regular", + "descriptor_number_unit_conversion", + "descriptor_number_different_units", + "array_conversion"]) def test_multiplication(self, descriptor: DescriptorArray, test, expected, raises_warning): # When Then if raises_warning: @@ -378,7 +449,6 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit assert np.allclose(result.variance, expected.variance) - assert result_reverse.unit == 'm^2' assert descriptor.unit == 'm' @@ -397,15 +467,127 @@ def test_multiplication(self, descriptor: DescriptorArray, test, expected, raise ids=["list", "number"]) def test_multiplication_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): # When Then - result_reverse = test * descriptor_dimensionless - result = descriptor_dimensionless * test + result = test * descriptor_dimensionless + result_reverse = descriptor_dimensionless * test # Expect assert type(result) == DescriptorArray assert type(result_reverse) == DescriptorArray assert np.array_equal(result.value, expected.value) assert np.allclose(result.variance, expected.variance) assert descriptor_dimensionless.unit == 'dimensionless' + + @pytest.mark.parametrize("test, expected, raises_warning", [ + (DescriptorNumber("test", 2, "m", 0.01), + DescriptorArray("test / name", + [[2.0, 1.0], [2.0/3.0, 0.5]], + "dimensionless", + [[0.41, 0.0525], + [(0.01 + 0.3 * 2**2 / 3.0**2) / 3.0**2, + (0.01 + 0.4 * 2**2 / 4.0**2) / 4.0**2]]), + True), + (DescriptorNumber("test", 1, "cm", 10), + DescriptorArray("test / name", + [[1.0/100.0, 1.0/200.0], [1.0/300.0, 1.0/400.0]], + "dimensionless", + [[1.01e-3, (1e-3 + 0.2 * 0.01**2/2**2) / 2**2], + [(1e-3 + 0.3 * 0.01**2/3**2) / 3**2,(1e-3 + 0.4 * 0.01**2 / 4**2) / 4**2]]), + True), + (DescriptorNumber("test", 1, "kg", 10), + DescriptorArray("test / name", + [[1.0, 0.5], [1.0/3.0, 0.25]], + "kg/m", + [[10.1, ( 10 + 0.2 * 1/2**2 ) / 2**2], + [( 10 + 0.3 * 1/3**2 ) / 3**2, ( 10 + 0.4 * 1/4**2 ) / 4**2 ]]), + True), + (DescriptorArray("test", + [[2.0, 3.0], [4.0, -5.0]], + "cm^2", + [[1.0, 2.0], [3.0, 4.0]]), + DescriptorArray("test / name", + [[2e-4, 1.5e-4], [4.0/3.0*1e-4, -1.25e-4]], + "m", + [[1.4e-8, 6.125e-9], + [( 3.0e-8 + 0.3 * (0.0004)**2 / 3**2 ) / 3**2, + ( 4.0e-8 + 0.4 * (0.0005)**2 / 4**2 ) / 4**2]]), + False)], + ids=["descriptor_number_regular", + "descriptor_number_unit_conversion", + "descriptor_number_different_units", + "array_conversion"]) + def test_division(self, descriptor: DescriptorArray, test, expected, raises_warning): + # When Then + if raises_warning: + with pytest.warns(UserWarning) as record: + result = test / descriptor + result_reverse = descriptor / test + assert len(record) == 2 + assert 'Correlations introduced' in record[0].message.args[0] + else: + result = test / descriptor + result_reverse = descriptor / test + # Expect + print(result) + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.allclose(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor.unit == 'm' + + @pytest.mark.parametrize("test, expected", [ + ([[2.0, 3.0], [4.0, -5.0], [6.0, -8.0]], + DescriptorArray("test", + [[2.0/1.0, 3.0/2.0], [4.0/3.0, -5.0/4.0], [6.0/5.0, -8.0/6.0]], + "dimensionless", + [[0.1 * 2.0**2, 0.2 * 3.0**2 / 2**4], + [0.3 * 4.0**2 / 3.0**4, 0.4 * 5.0**2 / 4**4], + [0.5 * 6.0**2 / 5**4, 0.6 * 8.0**2 / 6**4]])), + (2, + DescriptorArray("test", + [[2.0, 1.0], [2.0/3.0, 0.5], [2.0/5.0, 1.0/3.0]], + "dimensionless", + [[0.1 * 2.0**2, 0.2 / 2**2], + [0.3 * 2**2 / 3**4, 0.4 * 2**2 / 4**4], + [0.5 * 2**2 / 5**4, 0.6 * 2**2 / 6**4]])) + ], + ids=["list", "number"]) + def test_division_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): + # When Then + result = test / descriptor_dimensionless + result_reverse = descriptor_dimensionless / test + # Expect + assert type(result) == DescriptorArray + assert type(result_reverse) == DescriptorArray + assert np.allclose(result.value, expected.value) + assert np.allclose(result.value, 1 / result_reverse.value) + assert np.allclose(result.variance, expected.variance) + + assert descriptor_dimensionless.unit == 'dimensionless' + + @pytest.mark.parametrize("test", [ + [[2.0, 3.0], [4.0, -5.0], [6.0, 0.0]], + 0.0, + DescriptorNumber("test", 0, "cm", 10), + DescriptorArray("test", + [[1.5, 0.0], [4.5, 6.0], [7.5, 9.0]], + "dimensionless", + [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]])], + ids=["list", "number", "DescriptorNumber", "DescriptorArray"]) + def test_division_exception(self, descriptor_dimensionless: DescriptorArray, test): + # When Then + with pytest.raises(ZeroDivisionError): + descriptor_dimensionless / test + + # Also test reverse division where `self` is a DescriptorArray with a zero + zero_descriptor = DescriptorArray("test", + [[1.5, 0.0], [4.5, 6.0], [7.5, 0.0]], + "dimensionless", + [[0.225, 0.45], [0.675, 0.9], [1.125, 1.35]]) + with pytest.raises(ZeroDivisionError): + test / zero_descriptor + + @pytest.mark.parametrize("test", [ DescriptorNumber("test", 2, "s"), DescriptorArray("test", [[1, 2], [3, 4]], "s")], ids=["add_array_to_unit", "incompatible_units"]) @@ -413,8 +595,7 @@ def test_operation_exception(self, descriptor: DescriptorArray, test): # When Then Expect import operator operators = [operator.add, - operator.mul] - + operator.sub] for operator in operators: # This can probably be done better w. fixture with pytest.raises(UnitError): result = operator(descriptor, test) From cfeaaa9fdba41446bd5d1498f471203e663d1e6d Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Thu, 27 Feb 2025 16:52:16 +0100 Subject: [PATCH 37/48] add pow --- .../Objects/variable/descriptor_array.py | 40 +++++++++ .../Objects/variable/test_descriptor_array.py | 88 ++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index de0643d1..9464ce73 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -543,14 +543,54 @@ def __rtruediv__(self, other: Union[DescriptorArray, DescriptorNumber, list, num # but first converting to the units of other inverse_result = self._rsmooth_operator(other, op.truediv, units_must_match=False) return inverse_result + + def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorArray: + if not isinstance(other, (numbers.Number, DescriptorNumber)): + return NotImplemented + + if isinstance(other, numbers.Number): + exponent = other + elif type(other) is DescriptorNumber: + if other.unit != 'dimensionless': + raise UnitError('Exponents must be dimensionless') + if other.variance is not None: + raise ValueError('Exponents must not have variance') + exponent = other.value + else: + return NotImplemented + try: + new_value = self.full_value**exponent + except Exception as message: + raise message from None + if np.any(np.isnan(new_value.values)): + raise ValueError('The result of the exponentiation is not a number') + descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name = descriptor_number.unique_name + return descriptor_number + + def __rpow__(self, other: numbers.Number) -> numbers.Number: + """ + Defers reverse pow with a descriptor array, `a ** array`. + Exponentiation with regards to an array does not make sense, + and is not implemented. + """ + return NotImplemented def __neg__(self) -> DescriptorArray: + """ + Negate all values in the DescriptorArray. + """ new_value = -self.full_value descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) descriptor_array.name = descriptor_array.unique_name return descriptor_array def __abs__(self) -> DescriptorArray: + """ + Replace all elements in the DescriptorArray with their + absolute values. Note that this is different from the + norm of the DescriptorArray. + """ new_value = abs(self.full_value) descriptor_array = DescriptorArray.from_scipp(name=self.name, full_value=new_value) descriptor_array.name = descriptor_array.unique_name diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 34776113..4c6c347e 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -526,7 +526,6 @@ def test_division(self, descriptor: DescriptorArray, test, expected, raises_warn result = test / descriptor result_reverse = descriptor / test # Expect - print(result) assert type(result) == DescriptorArray assert result.name == result.unique_name assert np.allclose(result.value, expected.value) @@ -586,7 +585,94 @@ def test_division_exception(self, descriptor_dimensionless: DescriptorArray, tes with pytest.raises(ZeroDivisionError): test / zero_descriptor + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "dimensionless"), + DescriptorArray("test ** name", + [[1.0, 4.0], [9.0, 16.0]], + "m^2", + [[4 * 0.1 * 1, 4 * 0.2 * 2**2], + [4 * 0.3 * 3**2, 4 * 0.4 * 4**2]])), + (DescriptorNumber("test", 3, "dimensionless"), + DescriptorArray("test ** name", + [[1.0, 8.0], [27, 64.0]], + "m^3", + [[9 * 0.1, 9 * 0.2 * 2**4], + [9 * 0.3 * 3**4, 9 * 0.4 * 4**4]])), + (DescriptorNumber("test", 0.0, "dimensionless"), + DescriptorArray("test ** name", + [[1.0, 1.0], [1.0, 1.0]], + "dimensionless", + [[0.0, 0.0], [0.0, 0.0]])), + (0.0, + DescriptorArray("test ** name", + [[1.0, 1.0], [1.0, 1.0]], + "dimensionless", + [[0.0, 0.0], [0.0, 0.0]])) + ], + ids=["descriptor_number_squared", + "descriptor_number_cubed", + "descriptor_number_zero", + "number_zero"]) + def test_power(self, descriptor: DescriptorArray, test, expected): + # When Then + result = descriptor ** test + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor.unit == 'm' + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 0.1, "dimensionless"), + DescriptorArray("test ** name", + [[1, 2**0.1], [3**0.1, 4**0.1], [5**0.1, 6**0.1]], + "dimensionless", + [[0.1**2 * 0.1 * 1, 0.1**2 * 0.2 * 2**(-1.8)], + [0.1**2 * 0.3 * 3**(-1.8), 0.1**2 * 0.4 * 4**(-1.8)], + [0.1**2 * 0.5 * 5**(-1.8), 0.1**2 * 0.6 * 6**(-1.8)]])), + (DescriptorNumber("test", 2.0, "dimensionless"), + DescriptorArray("test ** name", + [[1.0, 4.0], [9.0, 16.0], [25.0, 36.0]], + "dimensionless", + [[0.4, 3.2], [10.8, 25.6], [50., 86.4]])), + ], + ids=["descriptor_number_fractional", "descriptor_number_integer"]) + def test_power_dimensionless(self, descriptor_dimensionless: DescriptorArray, test, expected): + # When Then + result = descriptor_dimensionless ** test + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.allclose(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor_dimensionless.unit == 'dimensionless' + + + @pytest.mark.parametrize("test, exception", [ + (DescriptorNumber("test", 2, "m"), UnitError), + (DescriptorNumber("test", 2, "dimensionless", 10), ValueError), + (DescriptorNumber("test", np.nan, "dimensionless"), UnitError), + (DescriptorNumber("test", np.nan, "dimensionless"), UnitError), + (DescriptorNumber("test", 1.5, "dimensionless"), UnitError), + (DescriptorNumber("test", 0.5, "dimensionless"), UnitError) # Square roots are not legal + ], + ids=["units", + "variance", + "scipp_nan", + "nan_result", + "non_integer_exponent_on_units", + "square_root_on_units" + ]) + def test_power_exception(self, descriptor: DescriptorArray, test, exception): + # When Then + with pytest.raises(exception): + result = descriptor ** 2 ** test + with pytest.raises(TypeError): + # Exponentiation with an array does not make sense + test ** descriptor @pytest.mark.parametrize("test", [ DescriptorNumber("test", 2, "s"), From 0bdbb3f4b7e03808c6f30e75454f9e789008eb70 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Thu, 27 Feb 2025 17:01:13 +0100 Subject: [PATCH 38/48] apply linter changes --- .../Objects/variable/descriptor_array.py | 221 +++--------------- 1 file changed, 32 insertions(+), 189 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 9464ce73..4da4b932 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -1,13 +1,13 @@ from __future__ import annotations -import operator as op -from warnings import warn import numbers +import operator as op from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Union +from warnings import warn import numpy as np import scipp as sc @@ -316,7 +316,10 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: raw_dict['variance'] = self._array.variances return raw_dict - def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], operator: str, units_must_match: bool = True) -> DescriptorArray: + def _smooth_operator(self, + other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], + operator: str, + units_must_match: bool = True) -> DescriptorArray: """ Perform element-wise operations with another DescriptorNumber, DescriptorArray, list, or number. @@ -356,10 +359,12 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, # performed to work around `scipp` and a warning raised to the end user. if (self._array.variances is not None or other.variance is not None): warn('Correlations introduced by this operation will not be considered.\ - See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049 for further detailes', UserWarning) + See https://content.iospress.com/articles/journal-of-neutron-research/jnr220049\ + for further detailes', UserWarning) + # Cheeky copy() of broadcasted scipp array to force scipp to perform the broadcast here broadcasted = sc.broadcast(other_converted.full_value, dims=self._array.dims, - shape=self._array.shape).copy() # Cheeky copy() to force scipp to perform the broadcast here + shape=self._array.shape).copy() new_full_value = operator(self.full_value, broadcasted) elif isinstance(other, DescriptorArray): @@ -384,12 +389,16 @@ def _smooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, descriptor_array.name = descriptor_array.unique_name return descriptor_array - def _rsmooth_operator(self, other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], operator: str, units_must_match: bool = True) -> DescriptorArray: + def _rsmooth_operator(self, + other: Union[DescriptorArray, DescriptorNumber, list, numbers.Number], + operator: str, + units_must_match: bool = True) -> DescriptorArray: """ Handle reverse operations for DescriptorArrays, DescriptorNumbers, lists, and scalars. Ensures unit compatibility when `other` is a DescriptorNumber. """ - reversed_operator = lambda a, b : operator(b, a) + def reversed_operator(a, b): + return operator(b, a) if isinstance(other, DescriptorArray): # This is probably never called return operator(other, self) @@ -545,6 +554,13 @@ def __rtruediv__(self, other: Union[DescriptorArray, DescriptorNumber, list, num return inverse_result def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorArray: + """ + Perform element-wise exponentiation with another DescriptorNumber or number. + + :param other: The object to use as a denominator. Must be a number or DescriptorNumber with + no unit or variance. + :return: A new DescriptorArray representing the result of the addition. + """ if not isinstance(other, (numbers.Number, DescriptorNumber)): return NotImplemented @@ -596,189 +612,16 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array + def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: + """ + Perform matrix multiplication with with another DesciptorArray or list. - # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # new_value = self.full_value * other - # elif type(other) is DescriptorArray: - # new_value = self.full_value * other.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.convert_unit(descriptor_number._base_unit()) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __rmul__(self, other: numbers.Number) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # new_value = other * self.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - -# TODO: add arithmetic operations -# They should be allowed between DescriptorArray and numbers, and between DescriptorArray and DescriptorArray. -# The result should be a new DescriptorArray with the same unit as the first argument. - - - - # def __add__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # if self.unit != 'dimensionless': - # raise UnitError('Numbers can only be added to dimensionless values') - # new_value = self.full_value + other - # elif type(other) is DescriptorArray: - # original_unit = other.unit - # try: - # 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) - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __radd__(self, other: numbers.Number) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # if self.unit != 'dimensionless': - # raise UnitError('Numbers can only be added to dimensionless values') - # new_value = other + self.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __sub__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # if self.unit != 'dimensionless': - # raise UnitError('Numbers can only be subtracted from dimensionless values') - # new_value = self.full_value - other - # elif type(other) is DescriptorArray: - # original_unit = other.unit - # try: - # 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) - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __rsub__(self, other: numbers.Number) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # if self.unit != 'dimensionless': - # raise UnitError('Numbers can only be subtracted from dimensionless values') - # new_value = other - self.full_value - # else: - # return NotImplemented - # descriptor = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor.name = descriptor.unique_name - # return descriptor - - # def __mul__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # new_value = self.full_value * other - # elif type(other) is DescriptorArray: - # new_value = self.full_value * other.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.convert_unit(descriptor_number._base_unit()) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __rmul__(self, other: numbers.Number) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # new_value = other * self.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __truediv__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # original_other = other - # if other == 0: - # raise ZeroDivisionError('Cannot divide by zero') - # new_value = self.full_value / other - # elif type(other) is DescriptorArray: - # original_other = other.value - # if original_other == 0: - # raise ZeroDivisionError('Cannot divide by zero') - # new_value = self.full_value / other.full_value - # other.value = original_other - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.convert_unit(descriptor_number._base_unit()) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __rtruediv__(self, other: numbers.Number) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # if self.value == 0: - # raise ZeroDivisionError('Cannot divide by zero') - # new_value = other / self.full_value - # else: - # return NotImplemented - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __pow__(self, other: Union[DescriptorArray, numbers.Number]) -> DescriptorArray: - # if isinstance(other, numbers.Number): - # exponent = other - # elif type(other) is DescriptorArray: - # if other.unit != 'dimensionless': - # raise UnitError('Exponents must be dimensionless') - # if other.variance is not None: - # raise ValueError('Exponents must not have variance') - # exponent = other.value - # else: - # return NotImplemented - # try: - # new_value = self.full_value**exponent - # except Exception as message: - # raise message from None - # if np.isnan(new_value.value): - # raise ValueError('The result of the exponentiation is not a number') - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __rpow__(self, other: numbers.Number) -> numbers.Number: - # if isinstance(other, numbers.Number): - # if self.unit != 'dimensionless': - # raise UnitError('Exponents must be dimensionless') - # if self.variance is not None: - # raise ValueError('Exponents must not have variance') - # new_value = other**self.value - # else: - # return NotImplemented - # return new_value - - # def __neg__(self) -> DescriptorArray: - # new_value = -self.full_value - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number - - # def __abs__(self) -> DescriptorArray: - # new_value = abs(self.full_value) - # descriptor_number = DescriptorArray.from_scipp(name=self.name, full_value=new_value) - # descriptor_number.name = descriptor_number.unique_name - # return descriptor_number + :param other: The object to use as a denominator. Must be a DescriptorArray + or a list, of compatible shape. + :return: A new DescriptorArray representing the result of the addition. + """ + if not isinstance(other, (DescriptorArray, list)): + return NotImplemented def _base_unit(self) -> str: string = str(self._array.unit) From 997d5f9482d78d7809de3ac72cce893590ce5c60 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Thu, 27 Feb 2025 17:09:55 +0100 Subject: [PATCH 39/48] add tests for abs and neg --- .../Objects/variable/test_descriptor_array.py | 264 +++--------------- 1 file changed, 42 insertions(+), 222 deletions(-) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 4c6c347e..e1cebc78 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -706,231 +706,51 @@ def test_numpy_ufuncs_exception(self, descriptor_dimensionless, function): function(descriptor_dimensionless, test) assert 'returned NotImplemented from' in str(e) - # def test_addition_with_array(self): - # # When - # descriptor = DescriptorArray(name="name", value=1, variance=0.1) - - # # Then - # result = descriptor + 1.0 - # result_reverse = 1.0 + descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == 2.0 - # assert result.unit == "dimensionless" - # assert result.variance == 0.1 - - # assert type(result_reverse) == DescriptorArray - # assert result_reverse.name == result_reverse.unique_name - # assert result_reverse.value == 2.0 - # assert result_reverse.unit == "dimensionless" - # assert result_reverse.variance == 0.1 - - # @pytest.mark.parametrize("test, expected", [ - # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test - name", 1, "m", 0.11)), - # (DescriptorArray("test", 2, "cm", 0.01), DescriptorArray("test - name", -98, "cm", 1000.01))], - # ids=["regular", "unit_conversion"]) - # def test_subtraction(self, descriptor: DescriptorArray, test, expected): - # # When Then - # result = test - descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == expected.value - # assert result.unit == expected.unit - # assert result.variance == expected.variance - - # assert descriptor.unit == 'm' - - # def test_subtraction_with_array(self): - # # When - # descriptor = DescriptorArray(name="name", value=2, variance=0.1) - - # # Then - # result = descriptor - 1.0 - # result_reverse = 1.0 - descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == 1.0 - # assert result.unit == "dimensionless" - # assert result.variance == 0.1 - - # assert type(result_reverse) == DescriptorArray - # assert result_reverse.name == result_reverse.unique_name - # assert result_reverse.value == -1.0 - # assert result_reverse.unit == "dimensionless" - # assert result_reverse.variance == 0.1 - - # @pytest.mark.parametrize("test", [1.0, DescriptorArray("test", 2, "s",)], ids=["sub_array_to_unit", "incompatible_units"]) - # def test_subtraction_exception(self, descriptor: DescriptorArray, test): - # # When Then Expect - # with pytest.raises(UnitError): - # result = test - descriptor - # with pytest.raises(UnitError): - # result_reverse = descriptor - test - - # @pytest.mark.parametrize("test, expected", [ - # (DescriptorArray("test", 2, "m", 0.01,), DescriptorArray("test * name", 2, "m^2", 0.41)), - # (DescriptorArray("test", 2, "dm", 0.01), DescriptorArray("test * name", 0.2, "m^2", 0.0041))], - # ids=["regular", "base_unit_conversion"]) - # def test_multiplication(self, descriptor: DescriptorArray, test, expected): - # # When Then - # result = test * descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == expected.value - # assert result.unit == expected.unit - # assert result.variance == pytest.approx(expected.variance) - - # def test_multiplication_with_array(self, descriptor: DescriptorArray): - # # When Then - # result = descriptor * 2.0 - # result_reverse = 2.0 * descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == 2.0 - # assert result.unit == "m" - # assert result.variance == 0.4 - - # assert type(result_reverse) == DescriptorArray - # assert result_reverse.name == result_reverse.unique_name - # assert result_reverse.value == 2.0 - # assert result_reverse.unit == "m" - # assert result_reverse.variance == 0.4 - - # @pytest.mark.parametrize("test, expected, expected_reverse", [ - # (DescriptorArray("test", 2, "m^2", 0.01,), DescriptorArray("name / test", 0.5, "1/m", 0.025625), DescriptorArray("test / name", 2, "m", 0.41)), - # (2, DescriptorArray("name / 2", 0.5, "m", 0.025), DescriptorArray("2 / name", 2, "1/m", 0.4))], - # ids=["DescriptorArray", "scalar"]) - # def test_division(self, descriptor: DescriptorArray, test, expected, expected_reverse): - # # When Then - # result = descriptor / test - # result_reverse = test / descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == expected.value - # assert result.unit == expected.unit - # assert result.variance == pytest.approx(expected.variance) - - # assert type(result_reverse) == DescriptorArray - # assert result_reverse.name == result_reverse.unique_name - # assert result_reverse.value == expected_reverse.value - # assert result_reverse.unit == expected_reverse.unit - # assert result_reverse.variance == pytest.approx(expected_reverse.variance) - - # @pytest.mark.parametrize("test", [0, DescriptorArray("test", 0, "m", 0.01)], ids=["zero", "zero_descriptor"]) - # def test_division_exception(self, descriptor: DescriptorArray, test): - # # When Then Expect - # with pytest.raises(ZeroDivisionError): - # result = descriptor / test - - # def test_division_exception_reverse(self): - # # When - # descriptor = DescriptorArray(name="name", value=0, variance=0.1) - - # # Then Expect - # with pytest.raises(ZeroDivisionError): - # result = 2 / descriptor - - # @pytest.mark.parametrize("test, expected", [ - # (DescriptorArray("test", 2), DescriptorArray("name ** test", 4, unit="m^2", variance=1.6)), - # (2, DescriptorArray("name ** 2", 4, unit="m^2", variance=1.6)), - # (-2, DescriptorArray("name ** -2", 0.25, unit="1/m^2", variance=0.00625))], - # ids=["DescriptorArray", "scalar", "negative_array"]) - # def test_power_of_descriptor(self, test, expected): - # # When - # descriptor = DescriptorArray(name="name", value=2, unit="m", variance=0.1) - - # # Then - # result = descriptor ** test - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == expected.value - # assert result.unit == expected.unit - # assert result.variance == expected.variance - - # def test_power_of_dimensionless_descriptor(self): - # # When - # descriptor = DescriptorArray(name="name", value=2, unit="dimensionless", variance=0.1) - - # # Then - # result = descriptor ** 0.5 - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == 1.4142135623730951 - # assert result.unit == "dimensionless" - # assert result.variance == pytest.approx(0.0125) - - # @pytest.mark.parametrize("descriptor, exponent, exception", [ - # (DescriptorArray("name", 2), DescriptorArray("test", 2, unit="m"), UnitError), - # (DescriptorArray("name", 2), DescriptorArray("test", 2, variance=0.1), ValueError), - # (DescriptorArray("name", 2, unit="m"), 0.5, UnitError), - # (DescriptorArray("name", -2), 0.5, ValueError)], - # ids=["descriptor_unit", "descriptor_variance", "fractional_of_unit", "fractonal_of_negative"]) - # def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): - # # When Then Expect - # with pytest.raises(exception): - # result = descriptor ** exponent + def test_negation(self, descriptor): + # When + # Then + result = -descriptor + # Expect + expected = DescriptorArray( + name="name", + value=[[-1., -2.], [-3., -4.]], + unit="m", + variance=[[0.1, 0.2], [0.3, 0.4]], + description="description", + url="url", + display_name="display_name", + parent=None, + ) + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor.unit == 'm' - # def test_descriptor_as_exponentiation(self): - # # When - # descriptor = DescriptorArray(name="name", value=2) - # # Then - # result = 2 ** descriptor + def test_abs(self, descriptor): + # When + negated = DescriptorArray( + name="name", + value=[[-1., -2.], [-3., -4.]], + unit="m", + variance=[[0.1, 0.2], [0.3, 0.4]], + description="description", + url="url", + display_name="display_name", + parent=None, + ) - # # Expect - # assert result == 4 + # Then + result = abs(negated) - # @pytest.mark.parametrize("exponent, exception", [ - # (DescriptorArray("test", 2, unit="m"), UnitError), - # (DescriptorArray("test", 2, variance=0.1), ValueError)], - # ids=["descriptor_unit", "descriptor_variance"]) - # def test_descriptor_as_exponentiation_exception(self, exponent, exception): - # # When Then Expect - # with pytest.raises(exception): - # result = 2 ** exponent - - # def test_negation(self): - # # When - # descriptor = DescriptorArray(name="name", unit="m", value=2, variance=0.1) - - # # Then - # result = -descriptor - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == -2 - # assert result.unit == "m" - # assert result.variance == 0.1 - - # def test_abs(self): - # # When - # descriptor = DescriptorArray(name="name", unit="m", value=-2, variance=0.1) - - # # Then - # result = abs(descriptor) - - # # Expect - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert result.value == 2 - # assert result.unit == "m" - # assert result.variance == 0.1 + # Expect + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, descriptor.value) + assert result.unit == descriptor.unit + assert np.allclose(result.variance, descriptor.variance) + assert descriptor.unit == 'm' From c84fd9ee93aaef8efe4a64671afbb127dcda896a Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Thu, 27 Feb 2025 17:48:06 +0100 Subject: [PATCH 40/48] allow for multiplication and division with dimensionless lists and numbers --- .../Objects/variable/descriptor_array.py | 8 ++-- .../Objects/variable/test_descriptor_array.py | 43 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 4da4b932..682ca8f4 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -329,13 +329,14 @@ def _smooth_operator(self, :return: A new DescriptorArray representing the result of the operation. """ if isinstance(other, numbers.Number): - if self.unit not in [None, "dimensionless"]: + # Does not need to be dimensionless for multiplication and division + if self.unit not in [None, "dimensionless"] and units_must_match: raise UnitError("Numbers can only be used together with dimensionless values") new_full_value = operator(self.full_value, other) elif isinstance(other, list): - if self.unit not in [None, "dimensionless"]: - raise UnitError("Operations with numpy arrays or lists are only allowed for dimensionless values") + if self.unit not in [None, "dimensionless"] and units_must_match: + raise UnitError("Operations with lists are only allowed for dimensionless values") # Ensure dimensions match if np.shape(other) != self._array.values.shape: @@ -622,6 +623,7 @@ def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: """ if not isinstance(other, (DescriptorArray, list)): return NotImplemented + # Dimensions must match along def _base_unit(self) -> str: string = str(self._array.unit) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index e1cebc78..b884c7a5 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -427,11 +427,29 @@ def test_subtraction_dimensionless(self, descriptor_dimensionless: DescriptorArr [[200.0, 600.0], [1200.0, -2000.0]], "cm^2", [[14000.0, 98000.0], [318000.0, 740000.0]]), - False)], + False), + ([[2.0, 3.0], [4.0, -5.0]], + DescriptorArray("test * name", + [[2.0, 6.0], [12.0, -20.0]], + "m", + [[0.1 * 2**2, 0.2 * 3**2], + [0.3 * 4**2, 0.4 * 5**2]]), + False), + (2.0, + DescriptorArray("test * name", + [[2.0, 4.0], [6.0, 8.0]], + "m", + [[0.1 * 2**2, 0.2 * 2**2], + [0.3 * 2**2, 0.4 * 2**2]]), + False) + + ], ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "descriptor_number_different_units", - "array_conversion"]) + "array_conversion", + "list", + "number"]) def test_multiplication(self, descriptor: DescriptorArray, test, expected, raises_warning): # When Then if raises_warning: @@ -509,11 +527,28 @@ def test_multiplication_dimensionless(self, descriptor_dimensionless: Descriptor [[1.4e-8, 6.125e-9], [( 3.0e-8 + 0.3 * (0.0004)**2 / 3**2 ) / 3**2, ( 4.0e-8 + 0.4 * (0.0005)**2 / 4**2 ) / 4**2]]), - False)], + False), + ([[2.0, 3.0], [4.0, -5.0]], + DescriptorArray("test / name", + [[2, 1.5], [4.0/3.0, -1.25]], + "1/m", + [[0.1 * 2**2 / 1**4, 0.2 * 3.0**2 / 2.0**4], + [0.3 * 4**2 / 3**4, 0.4 * 5.0**2 / 4**4]]), + False), + (2.0, + DescriptorArray("test / name", + [[2, 1.0], [2.0/3.0, 0.5]], + "1/m", + [[0.1 * 2**2 / 1**4, 0.2 * 2.0**2 / 2.0**4], + [0.3 * 2**2 / 3**4, 0.4 * 2.0**2 / 4.0**4]]), + False) + ], ids=["descriptor_number_regular", "descriptor_number_unit_conversion", "descriptor_number_different_units", - "array_conversion"]) + "array_conversion", + "list", + "number"]) def test_division(self, descriptor: DescriptorArray, test, expected, raises_warning): # When Then if raises_warning: From 0e007a15f192eac8680409dc64ceaf3ff850a841 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Fri, 28 Feb 2025 11:45:26 +0100 Subject: [PATCH 41/48] fix scipp error --- .../Objects/variable/descriptor_array.py | 16 ++++++++++++++-- .../Objects/variable/test_descriptor_array.py | 5 ++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 682ca8f4..474a6b3b 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -74,7 +74,7 @@ def __init__( except Exception as message: raise UnitError(message) # TODO: handle 1xn and nx1 arrays - self._array = sc.array(dims=['row','column'],values=value, unit=unit, variances=variance) + self._array = sc.array(dims=['row','column'], values=value, unit=unit, variances=variance) super().__init__( name=name, @@ -623,7 +623,19 @@ def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: """ if not isinstance(other, (DescriptorArray, list)): return NotImplemented - # Dimensions must match along + + if isinstance(other, DescriptorArray): + shape = other.full_value.shape + elif isinstance(other, list): + shape = np.shape(other) + + # Dimensions must match for matrix multiplication + if shape[0] != self._array.values.shape[-1]: + raise ValueError(f"Last dimension of {other=} must match the first dimension of DescriptorArray values") + + other = sc.array(dims=self._array.dims, values=other) + new_full_value = operator(self._array, other) # Let scipp handle operation for uncertainty propagation + def _base_unit(self) -> str: string = str(self._array.unit) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index b884c7a5..0645cb1b 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock import scipp as sc from scipp import UnitError +from scipp.testing import assert_identical import numpy as np @@ -143,9 +144,7 @@ def test_full_value(self, descriptor: DescriptorArray): values=[[1.0, 2.0], [3.0, 4.0]], unit='m', variances=[[0.1, 0.2], [0.3, 0.4]]) - print(other.shape, descriptor.full_value.shape) - print(descriptor.full_value.dims, other.dims) - assert descriptor.full_value == other + assert_identical(descriptor.full_value, other) def test_set_full_value(self, descriptor: DescriptorArray): with pytest.raises(AttributeError): From dbc72e17512152b34beeaff4ec91d5816112f3cf Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Fri, 28 Feb 2025 15:24:29 +0100 Subject: [PATCH 42/48] add sum function --- .../Objects/variable/descriptor_array.py | 133 ++++++++++++++---- .../Objects/variable/test_descriptor_array.py | 98 ++++++++++++- 2 files changed, 204 insertions(+), 27 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 474a6b3b..b103d774 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -23,13 +23,13 @@ class DescriptorArray(DescriptorBase): """ - A `Descriptor` for Array values with units. The internal representation is a scipp array. + A `Descriptor` for Array values with units. The internal representation is a scipp array. """ def __init__( self, name: str, - value: numbers.Number, + value: Union[list, np.ndarray], unit: Optional[Union[str, sc.Unit]] = '', variance: Optional[numbers.Number] = None, unique_name: Optional[str] = None, @@ -37,20 +37,22 @@ def __init__( url: Optional[str] = None, display_name: Optional[str] = None, parent: Optional[Any] = None, + dims: Optional[list] = None ): """Constructor for the DescriptorArray class param name: Name of the descriptor - param value: Value of the descriptor + param value: List containing the values of the descriptor param unit: Unit of the descriptor - param variance: Variance of the descriptor + param variance: Variances 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 dims: List of dimensions to pass to scipp. Will be autogenerated if not supplied. .. note:: Undo/Redo functionality is implemented for the attributes `variance`, `error`, `unit` and `value`. """ - + if not isinstance(value, (list, np.ndarray)): raise TypeError(f"{value=} must be a list or numpy array.") if isinstance(value, list): @@ -69,12 +71,20 @@ def __init__( if not isinstance(unit, sc.Unit) and not isinstance(unit, str): raise TypeError(f'{unit=} must be a scipp unit or a string representing a valid scipp unit') + + if dims is None: + # Autogenerate dimensions if not supplied + dims = ['dim'+str(i) for i in range(len(value.shape))] + if not len(dims) == len(value.shape): + raise ValueError(f"Length of dims ({dims=}) does not match length of value {value=}.") + self._dims = dims + try: - self._array = sc.array(dims=['row', 'column'], values=value, unit=unit, variances=variance) + self._array = sc.array(dims=dims, values=value, unit=unit, variances=variance) except Exception as message: raise UnitError(message) # TODO: handle 1xn and nx1 arrays - self._array = sc.array(dims=['row','column'], values=value, unit=unit, variances=variance) + self._array = sc.array(dims=dims, values=value, unit=unit, variances=variance) super().__init__( name=name, @@ -145,6 +155,30 @@ def value(self, value: Union[list, np.ndarray]) -> None: raise ValueError(f"{value=} must have the same shape as the existing array values.") self._array.values = value + + @property + def dims(self) -> list: + """ + Get the dims used for the underlying scipp array. + + :return: dims of self. + """ + return self._dims + + @dims.setter + def dims(self, dims: Union[list, np.ndarray]) -> None: + """ + Set the dims of self. Ensures that the input has a shape compatible with self.full_value. + + :param value: list of dims. + """ + if not isinstance(dims, (list, np.ndarray)): + raise TypeError(f"{value=} must be a list or numpy array.") + + if len(dims) != len(self._dims): + raise ValueError(f"{dims=} must have the same shape as the existing dims") + + self._dims = dims @property def unit(self) -> str: @@ -613,28 +647,79 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array - def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: - """ - Perform matrix multiplication with with another DesciptorArray or list. - :param other: The object to use as a denominator. Must be a DescriptorArray - or a list, of compatible shape. - :return: A new DescriptorArray representing the result of the addition. + def __getitem__(self, a): + """Defer slicing to scipp""" + return self.full_value.__getitem__(a) + + def __delitem__(self, a): + """Defer slicing to scipp""" + return self.full_value.__delitem__(a) + + def __setitem__(self, a, b: Union[number, DescriptorNumber]): + """Defer slicing to scipp""" + # TODO handle variances and units... + return self.full_value.__setitem__(a, b) + + def trace(self) -> DescriptorNumber: """ - if not isinstance(other, (DescriptorArray, list)): - return NotImplemented + Computes the trace over the descriptor array. + Only works for matrices where all dimensions + are equal. + """ + shape = np.array(self.full_value.shape) + if not np.all(shape == shape[0]): + raise ValueError('Trace can only be taken over arrays where all dimensions are of equal length') - if isinstance(other, DescriptorArray): - shape = other.full_value.shape - elif isinstance(other, list): - shape = np.shape(other) + trace = DescriptorNumber("trace", 0.0, unit=self.unit) + for i in range(self.full_value.shape[0]): + dim = self.full_value.dims[i] + print(trace.full_value) + trace.full_value += self.full_value[dim, i] + return sumn + + def sum(self, dim: Optional[Union[str, list]] = None) -> DescriptorNumber: + """ + Uses scipp to sum over the requested dims. + :param dim: The dim(s) in the scipp array to sum over. If `None`, will sum over all dims. + """ - # Dimensions must match for matrix multiplication - if shape[0] != self._array.values.shape[-1]: - raise ValueError(f"Last dimension of {other=} must match the first dimension of DescriptorArray values") + new_full_value = self.full_value.sum(dim=dim) - other = sc.array(dims=self._array.dims, values=other) - new_full_value = operator(self._array, other) # Let scipp handle operation for uncertainty propagation + # If fully reduced the result will be a DescriptorNumber, + # otherwise a DescriptorArray + if dim is None: + constructor = DescriptorNumber.from_scipp + else: + constructor = DescriptorArray.from_scipp + + descriptor = constructor(name=self.name, full_value=new_full_value) + descriptor.name = descriptor.unique_name + return descriptor + + # This is to be implemented at a later time + # def __matmul__(self, other: [DescriptorArray, list]) -> DescriptorArray: + # """ + # Perform matrix multiplication with with another DesciptorArray or list. + + # :param other: The object to use as a denominator. Must be a DescriptorArray + # or a list, of compatible shape. + # :return: A new DescriptorArray representing the result of the addition. + # """ + # if not isinstance(other, (DescriptorArray, list)): + # return NotImplemented + + # if isinstance(other, DescriptorArray): + # shape = other.full_value.shape + # elif isinstance(other, list): + # shape = np.shape(other) + + # # Dimensions must match for matrix multiplication + # if shape[0] != self._array.values.shape[-1]: + # raise ValueError(f"Last dimension of {other=} must match the first dimension of DescriptorArray values") + # + # other = sc.array(dims=self._array.dims, values=other) + # new_full_value = operator(self._array, other) # Let scipp handle operation for uncertainty propagation def _base_unit(self) -> str: diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 0645cb1b..6091872b 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -138,9 +138,9 @@ def test_from_scipp(self): # with pytest.raises(TypeError): # DescriptorArray.from_scipp(name="name", full_value=full_value) - def test_full_value(self, descriptor: DescriptorArray): + def tvigateDownest_full_value(self, descriptor: DescriptorArray): # When Then Expect - other = sc.array(dims=('row','column'), + other = sc.array(dims=('dim0','dim1'), values=[[1.0, 2.0], [3.0, 4.0]], unit='m', variances=[[0.1, 0.2], [0.3, 0.4]]) @@ -763,7 +763,6 @@ def test_negation(self, descriptor): assert np.allclose(result.variance, expected.variance) assert descriptor.unit == 'm' - def test_abs(self, descriptor): # When negated = DescriptorArray( @@ -788,3 +787,96 @@ def test_abs(self, descriptor): assert np.allclose(result.variance, descriptor.variance) assert descriptor.unit == 'm' + def test_trace(self, descriptor: DescriptorArray): + shape = np.array(descriptor.full_value.shape) + print(shape, shape[0], shape == shape[0]) + result = descriptor.trace() + expected = 1 + assert type(result) == DescriptorArray + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + assert descriptor.unit == 'm' + + # def test_trace_fail(self, descriptor: DescriptorArray): + # """Should fail for non-square matrices""" + # result = descriptor.trace() + # expected = 1 + # assert type(result) == DescriptorArray + # assert result.name == result.unique_name + # assert np.array_equal(result.value, expected.value) + # assert result.unit == expected.unit + # assert np.allclose(result.variance, expected.variance) + # assert descriptor.unit == 'm' + + @pytest.mark.parametrize("test, expected", [ + (DescriptorArray("test + name", + [[3.0, 4.0], [5.0, 6.0]], + "m", + [[0.11, 0.21], [0.31, 0.41]]), + DescriptorNumber("test", 18, "m", 1.04)), + (DescriptorArray("test + name", + [[101.0, 201.0], [301.0, 401.0]], + "cm", + [[1010.0, 2010.0], [3010.0, 4010.0]]), + DescriptorNumber("test", 1004.0, "cm", 10040.)), + (DescriptorArray("test", + [[2.0, 3.0]], + "dimensionless", + [[1.0, 2.0]]), + DescriptorNumber("test", 5.0, "dimensionless", 3.0)), + (DescriptorArray("test", + [[2.0, 3.0]], + "dimensionless"), + DescriptorNumber("test", 5.0, "dimensionless")), + ], + ids=["descriptor_array_m", "d=descriptor_array_cm", "descriptor_array_dimensionless", "descriptor_array_dim_varless"]) + def test_sum(self, test, expected): + result = test.sum() + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + if test.variance is not None: + assert np.allclose(result.variance, expected.variance) + + @pytest.mark.parametrize("expected, dim", [ + (DescriptorArray("test", + [4.0, 6.0], + "m", + [0.4, 0.6]), + 'dim0'), + (DescriptorArray("test", + [3.0, 7.0], + "m", + [0.3, 0.7]), + 'dim1'), + ], + ids=["descriptor_array_dim0", "descriptor_array_dim1"]) + def test_sum_over_subset(self, descriptor, expected, dim): + result = descriptor.sum(dim) + assert type(result) == type(expected) + assert result.name == result.unique_name + assert np.array_equal(result.value, expected.value) + assert result.unit == expected.unit + assert np.allclose(result.variance, expected.variance) + + @pytest.mark.parametrize("test, dims", [ + (DescriptorArray("test", [1.], "dimensionless", [1.]), ['dim0']), + (DescriptorArray("test", [[1., 1.]], "dimensionless", [[1., 1.]]), ['dim0', 'dim1']), + (DescriptorArray("test", [[1.], [1.]], "dimensionless", [[1.], [1.]]), ['dim0', 'dim1']), + (DescriptorArray("test", [[[1., 1., 1.]]], "dimensionless", [[[1., 1., 1.]]]), ['dim0', 'dim1', 'dim2']), + (DescriptorArray("test", [[[1.]], [[1.]], [[1.]]], "dimensionless", [[[1.]], [[1.]], [[1.]]]), ['dim0', 'dim1', 'dim2']), + ], + ids=["1x1", "1x2", "2x1", "1x3", "3x1"]) + def test_array_generate_dims(self, test, dims): + assert test.dims == dims + + def test_array_set_dims_exception(self, descriptor): + with pytest.raises(ValueError) as e: + descriptor.dims = ['too_few'] + assert "must have the same shape" + with pytest.raises(ValueError) as e: + DescriptorArray("test", [[1.]], "m", [[1.]], dims=['dim']) + assert "Length of dims" in str(e) From 063e632198492544ac5bf650d362480e3f5be1c8 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Fri, 28 Feb 2025 16:39:58 +0100 Subject: [PATCH 43/48] start working on trace and slicing --- .../Objects/variable/descriptor_array.py | 23 +++++++------ .../Objects/variable/test_descriptor_array.py | 32 ++++++++++++++----- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index b103d774..1a98a672 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -647,7 +647,6 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array - def __getitem__(self, a): """Defer slicing to scipp""" return self.full_value.__getitem__(a) @@ -664,26 +663,30 @@ def __setitem__(self, a, b: Union[number, DescriptorNumber]): def trace(self) -> DescriptorNumber: """ Computes the trace over the descriptor array. - Only works for matrices where all dimensions - are equal. + Only works for matrices where all dimensions are equal. """ shape = np.array(self.full_value.shape) if not np.all(shape == shape[0]): - raise ValueError('Trace can only be taken over arrays where all dimensions are of equal length') - - trace = DescriptorNumber("trace", 0.0, unit=self.unit) + raise ValueError('\ + Trace can only be taken over arrays where all dimensions are of equal length') + print(shape) + trace = sc.scalar(0.0, unit=self.unit, variance=None) for i in range(self.full_value.shape[0]): dim = self.full_value.dims[i] - print(trace.full_value) - trace.full_value += self.full_value[dim, i] - return sumn + # TODO how do I do [i, i]? + # [i, i, i]? etc + diagonal = self.full_value[i, i] + trace = trace + diagonal + + descriptor = DescriptorNumber.from_scipp(name=self.name, full_value=trace) + descriptor.name = descriptor.unique_name + return descriptor def sum(self, dim: Optional[Union[str, list]] = None) -> DescriptorNumber: """ Uses scipp to sum over the requested dims. :param dim: The dim(s) in the scipp array to sum over. If `None`, will sum over all dims. """ - new_full_value = self.full_value.sum(dim=dim) # If fully reduced the result will be a DescriptorNumber, diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 6091872b..0d0b576a 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -787,17 +787,33 @@ def test_abs(self, descriptor): assert np.allclose(result.variance, descriptor.variance) assert descriptor.unit == 'm' - def test_trace(self, descriptor: DescriptorArray): - shape = np.array(descriptor.full_value.shape) - print(shape, shape[0], shape == shape[0]) - result = descriptor.trace() - expected = 1 - assert type(result) == DescriptorArray + @pytest.mark.parametrize("test, expected", [ + (DescriptorArray("test + name", + [[3.0, 4.0], [5.0, 6.0]], + "m", + [[0.11, 0.21], [0.31, 0.41]]), + DescriptorNumber("test", 18, "m", 1.04)), + (DescriptorArray("test + name", + [[101.0, 201.0], [301.0, 401.0]], + "dimensionless", + [[1010.0, 2010.0], [3010.0, 4010.0]]), + DescriptorNumber("test", 1004.0, "dimensionless", 10040.)), + (DescriptorArray("test", np.ones((9, 9)), "dimensionless", np.ones((9, 9))), + DescriptorNumber("test", 9.0, "dimensionless", 3.0)), + (DescriptorArray("test", np.ones((3, 3, 3)), "dimensionless", np.ones((3, 3, 3))), + DescriptorNumber("test", 3.0, "dimensionless", 3.0)), + (DescriptorArray("test", [[2.0]], "dimensionless"), + DescriptorNumber("test", 2.0, "dimensionless")) + ], + ids=["2d_unit", "2d_dimensionless", "2d_large", "3d_dimensionless", "1d_dimensionless"]) + def test_trace(self, test: DescriptorArray, expected: DescriptorNumber): + result = test.trace() + assert type(result) == DescriptorNumber assert result.name == result.unique_name assert np.array_equal(result.value, expected.value) assert result.unit == expected.unit - assert np.allclose(result.variance, expected.variance) - assert descriptor.unit == 'm' + if test.variance is not None: + assert np.allclose(result.variance, expected.variance) # def test_trace_fail(self, descriptor: DescriptorArray): # """Should fail for non-square matrices""" From e71f21f100480561ce0edd7cece4f75fa0e25234 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Mon, 3 Mar 2025 11:48:08 +0100 Subject: [PATCH 44/48] add trace operation --- .../Objects/variable/descriptor_array.py | 22 ++++++++++--------- .../Objects/variable/test_descriptor_array.py | 6 ++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 1a98a672..0c0d1db0 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -173,7 +173,7 @@ def dims(self, dims: Union[list, np.ndarray]) -> None: :param value: list of dims. """ if not isinstance(dims, (list, np.ndarray)): - raise TypeError(f"{value=} must be a list or numpy array.") + raise TypeError(f"{dims=} must be a list or numpy array.") if len(dims) != len(self._dims): raise ValueError(f"{dims=} must have the same shape as the existing dims") @@ -655,7 +655,7 @@ def __delitem__(self, a): """Defer slicing to scipp""" return self.full_value.__delitem__(a) - def __setitem__(self, a, b: Union[number, DescriptorNumber]): + def __setitem__(self, a, b: Union[numbers.Number, DescriptorNumber]): """Defer slicing to scipp""" # TODO handle variances and units... return self.full_value.__setitem__(a, b) @@ -666,17 +666,19 @@ def trace(self) -> DescriptorNumber: Only works for matrices where all dimensions are equal. """ shape = np.array(self.full_value.shape) - if not np.all(shape == shape[0]): + N = shape[0] + if not np.all(shape == N): raise ValueError('\ Trace can only be taken over arrays where all dimensions are of equal length') - print(shape) + trace = sc.scalar(0.0, unit=self.unit, variance=None) - for i in range(self.full_value.shape[0]): - dim = self.full_value.dims[i] - # TODO how do I do [i, i]? - # [i, i, i]? etc - diagonal = self.full_value[i, i] - trace = trace + diagonal + for i in range(N): + # Index through all the dims to get + # the value i on the diagonal + diagonal_element = self.full_value + for dim in self.full_value.dims: + diagonal_element = diagonal_element[dim, i] + trace = trace + diagonal_element descriptor = DescriptorNumber.from_scipp(name=self.name, full_value=trace) descriptor.name = descriptor.unique_name diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 0d0b576a..87d22d5b 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -792,14 +792,14 @@ def test_abs(self, descriptor): [[3.0, 4.0], [5.0, 6.0]], "m", [[0.11, 0.21], [0.31, 0.41]]), - DescriptorNumber("test", 18, "m", 1.04)), + DescriptorNumber("test", 9, "m", 0.52)), (DescriptorArray("test + name", [[101.0, 201.0], [301.0, 401.0]], "dimensionless", [[1010.0, 2010.0], [3010.0, 4010.0]]), - DescriptorNumber("test", 1004.0, "dimensionless", 10040.)), + DescriptorNumber("test", 502.0, "dimensionless", 5020.0)), (DescriptorArray("test", np.ones((9, 9)), "dimensionless", np.ones((9, 9))), - DescriptorNumber("test", 9.0, "dimensionless", 3.0)), + DescriptorNumber("test", 9.0, "dimensionless", 9.0)), (DescriptorArray("test", np.ones((3, 3, 3)), "dimensionless", np.ones((3, 3, 3))), DescriptorNumber("test", 3.0, "dimensionless", 3.0)), (DescriptorArray("test", [[2.0]], "dimensionless"), From 4cef1d542cbf870c4ac95410137c9a4e3c63ae47 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Mon, 3 Mar 2025 11:52:11 +0100 Subject: [PATCH 45/48] add exception test for trace --- .../Objects/variable/test_descriptor_array.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 87d22d5b..2fa09ed5 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -815,6 +815,23 @@ def test_trace(self, test: DescriptorArray, expected: DescriptorNumber): if test.variance is not None: assert np.allclose(result.variance, expected.variance) + @pytest.mark.parametrize("test", [ + DescriptorArray("test + name", + [[3.0, 4.0]], + "m", + [[0.11, 0.21]]), + + DescriptorArray("test + name", + [[3.0, 4.0], [1.0, 1.0], [1.0, 1.0]], + "dimensionless", + [[0.11, 0.21], [1., 1.], [1., 1.]]) + ], + ids=["2x1_unit", "3x2_dimensionless"]) + def test_trace_exception(self, test: DescriptorArray): + with pytest.raises(ValueError) as e: + test.trace() + assert "Trace can only be taken" in str(e) + # def test_trace_fail(self, descriptor: DescriptorArray): # """Should fail for non-square matrices""" # result = descriptor.trace() From 972779cd3aa123052b8f29c46decdf38d0add7d3 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Mon, 3 Mar 2025 15:40:41 +0100 Subject: [PATCH 46/48] try implementing set item and realize we need views --- .../Objects/variable/descriptor_array.py | 57 +++++-- .../Objects/variable/test_descriptor_array.py | 144 ++++++++++++++++-- 2 files changed, 181 insertions(+), 20 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 0c0d1db0..bde44dab 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -647,18 +647,59 @@ def __abs__(self) -> DescriptorArray: descriptor_array.name = descriptor_array.unique_name return descriptor_array - def __getitem__(self, a): - """Defer slicing to scipp""" - return self.full_value.__getitem__(a) + def __getitem__(self, a) -> Union[DescriptorArray]: + """ + Slice using scipp syntax. + Defer slicing to scipp. + """ + descriptor = DescriptorArray.from_scipp(name=self.name, full_value=self.full_value.__getitem__(a)) + descriptor.name = descriptor.unique_name + return descriptor def __delitem__(self, a): - """Defer slicing to scipp""" + """ + Defer slicing to scipp. + This should fail, since scipp does not support __delitem__. + """ return self.full_value.__delitem__(a) - def __setitem__(self, a, b: Union[numbers.Number, DescriptorNumber]): + def __setitem__(self, a, b: Union[numbers.Number, list, DescriptorNumber, DescriptorArray]): """Defer slicing to scipp""" # TODO handle variances and units... - return self.full_value.__setitem__(a, b) + if not isinstance(b, (numbers.Number, list, DescriptorNumber, DescriptorArray)): + return NotImplemented + + if isinstance(b, (numbers.Number, list)): + if self.unit not in [None, "dimensionless"]: + raise UnitError( + "Unitless values can only be assigned to dimensionless arrays") + if self.full_value.variances is not None: + raise ValueError( + "Values without variances can only be assigned to arrays without variances") + + if isinstance(b, numbers.Number): + other = b + elif isinstance(b, list): + other = np.array(b) + elif isinstance(b, (DescriptorNumber, DescriptorArray)): + original_unit = b.unit + try: + b.convert_unit(self.unit) # Convert item to be set to current unit + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} are incompatible") from None + other = b.full_value.copy() + # Restore b + b.convert_unit(original_unit) + + # Set data + #new_full_value = self.full_value.copy() + new_full_value = self.full_value + new_full_value.__setitem__(a, other) + + self._array = sc.array(dims=self.dims, + values=new_full_value.values, + unit=self.unit, + variances=new_full_value.variances) def trace(self) -> DescriptorNumber: """ @@ -736,7 +777,3 @@ def _base_unit(self) -> str: elif letter not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '-']: return string[i:] return '' - - - - # TODO: add matrix multiplication and division using numpy. diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 2fa09ed5..924bd0c9 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -832,16 +832,140 @@ def test_trace_exception(self, test: DescriptorArray): test.trace() assert "Trace can only be taken" in str(e) - # def test_trace_fail(self, descriptor: DescriptorArray): - # """Should fail for non-square matrices""" - # result = descriptor.trace() - # expected = 1 - # assert type(result) == DescriptorArray - # assert result.name == result.unique_name - # assert np.array_equal(result.value, expected.value) - # assert result.unit == expected.unit - # assert np.allclose(result.variance, expected.variance) - # assert descriptor.unit == 'm' + def test_slicing(self, descriptor: DescriptorArray): + # When + first_value = descriptor['dim0', 0] + last_value = descriptor['dim0', -1] + second_array = descriptor['dim1', :] + + # Then + assert type(first_value) == DescriptorArray + assert type(last_value) == DescriptorArray + assert type(second_array) == DescriptorArray + + assert first_value.name != descriptor.unique_name + assert last_value.name != descriptor.unique_name + assert second_array.name != descriptor.unique_name + + assert np.array_equal(first_value.full_value.values, descriptor.full_value['dim0', 0].values) + assert np.array_equal(last_value.full_value.values, descriptor.full_value['dim0', -1].values) + assert np.array_equal(second_array.full_value.values, descriptor.full_value['dim1', :].values) + + assert np.array_equal(first_value.full_value.variances, descriptor.full_value['dim0', 0].variances) + assert np.array_equal(last_value.full_value.variances, descriptor.full_value['dim0', -1].variances) + assert np.array_equal(second_array.full_value.variances, descriptor.full_value['dim1', :].variances) + + assert np.array_equal(first_value.full_value.unit, descriptor.unit) + assert np.array_equal(last_value.full_value.unit, descriptor.unit) + assert np.array_equal(second_array.full_value.unit, descriptor.unit) + + def test_slice_deletion(self, descriptor: DescriptorArray): + with pytest.raises(AttributeError) as e: + del descriptor['dim0', 0] + assert 'has no attribute' in str(e) + + @pytest.mark.parametrize("test", [ + DescriptorNumber("test", 9, "m", 0.52), + DescriptorNumber("test", 10., "cm", 9.), + ], + ids=["DescriptorNumber", "convert_unit"]) + def test_slice_assignment(self, descriptor: DescriptorArray, test): + # When + original_unit = test.unit + descriptor_copy = descriptor.full_value.copy() + descriptor['dim0', 0][0] = test + + # Then + assert descriptor.unit == 'm' + assert test.unit == original_unit + test.convert_unit(descriptor.unit) + assert descriptor.full_value['dim0', 0] == test.full_value.value + assert descriptor.variances[0, 0] == test.full_value.variance + assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) + assert np.allclose(descriptor_copy.variances[:, 1], descriptor.full_value.variances[:, 1]) + + @pytest.mark.parametrize("test", [0.1], + ids=["numbers.Number"]) + def test_slice_assignment_dimensionless(self, test): + # When + descriptor = DescriptorArray( + name="name", + value=[[1., 2.], [3., 4.], [5., 6.]], + unit="dimensionless", + variance=None, + description="description", + url="url", + display_name="display_name", + parent=None, + ) + descriptor['dim0', 0][0] = test + print(descriptor['dim0', 0]) + print(descriptor['dim0', 0][0]) + print(descriptor) + + # Then + assert descriptor.unit == 'dimensionless' + assert descriptor.full_value.values[0, 0] == test + assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) + + @pytest.mark.parametrize("test", [ + DescriptorArray("test + name", + [3.0, 4.0], + "m", + [0.1, 0.2]), + DescriptorArray("test + name", + [3.0, 4.0], + "cm", + [0.1, 0.2]) + ], + ids=["DescriptorArray", "convert_unit"]) + def test_slice_assignment_array(self, descriptor: DescriptorArray, test): + # When + original_unit = test.unit + descriptor_copy = descriptor.full_value.copy() + descriptor['dim1', 0] = test + # Then + assert descriptor.unit == 'm' + assert test.unit == original_unit + test.convert_unit(descriptor.unit) + assert np.allclose(descriptor.full_value.values[:, 0], test.full_value.values) + assert np.allclose(descriptor.full_value.variances[:, 0], test.full_value.variances) + assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) + assert np.allclose(descriptor_copy.variances[:, 1], descriptor.full_value.variances[:, 1]) + + @pytest.mark.parametrize("test", [ + [3.0, 4.0, 5.0] + ], + ids=["list"]) + def test_slice_assignment_array_dimensionless(self, test): + # When + descriptor = DescriptorArray( + name="name", + value=[[1., 2.], [3., 4.], [5., 6.]], + unit="dimensionless", + variance=None, + description="description", + url="url", + display_name="display_name", + parent=None, + ) + descriptor['dim1', 0] = test + + # Then + assert descriptor.unit == 'dimensionless' + assert np.allclose(descriptor.full_value.values[:, 0], np.array(test)) + + @pytest.mark.parametrize("test", [ + 1.0, + [3.0, 4.0, 5.0] + ], + ids=["number", "list"]) + def test_slice_assignment_exception(self, descriptor_dimensionless: DescriptorArray, test): + # When + with pytest.raises(ValueError) as e: + descriptor_dimensionless['dim0', :] = test + assert "Values without variances" in str(e) + @pytest.mark.parametrize("test, expected", [ (DescriptorArray("test + name", From a52653009e37025daad6d1b42a3ddf6dda2b4e46 Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Mon, 3 Mar 2025 16:02:30 +0100 Subject: [PATCH 47/48] do not allow __setitem__ --- .../Objects/variable/descriptor_array.py | 45 ++------- .../Objects/variable/test_descriptor_array.py | 96 +------------------ 2 files changed, 11 insertions(+), 130 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index bde44dab..6ea4806e 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -664,42 +664,15 @@ def __delitem__(self, a): return self.full_value.__delitem__(a) def __setitem__(self, a, b: Union[numbers.Number, list, DescriptorNumber, DescriptorArray]): - """Defer slicing to scipp""" - # TODO handle variances and units... - if not isinstance(b, (numbers.Number, list, DescriptorNumber, DescriptorArray)): - return NotImplemented - - if isinstance(b, (numbers.Number, list)): - if self.unit not in [None, "dimensionless"]: - raise UnitError( - "Unitless values can only be assigned to dimensionless arrays") - if self.full_value.variances is not None: - raise ValueError( - "Values without variances can only be assigned to arrays without variances") - - if isinstance(b, numbers.Number): - other = b - elif isinstance(b, list): - other = np.array(b) - elif isinstance(b, (DescriptorNumber, DescriptorArray)): - original_unit = b.unit - try: - b.convert_unit(self.unit) # Convert item to be set to current unit - except UnitError: - raise UnitError(f"Values with units {self.unit} and {other.unit} are incompatible") from None - other = b.full_value.copy() - # Restore b - b.convert_unit(original_unit) - - # Set data - #new_full_value = self.full_value.copy() - new_full_value = self.full_value - new_full_value.__setitem__(a, other) - - self._array = sc.array(dims=self.dims, - values=new_full_value.values, - unit=self.unit, - variances=new_full_value.variances) + """ + __setitem via slice is not allowed, since we currently do not give back a + view to the DescriptorArray upon calling __getitem__. + """ + raise AttributeError( + f'{self.__class__.__name__} cannot be edited via slicing. Edit the underlyinf scipp\ + array via the `full_value` property, or create a\ + new {self.__class__.__name__}.' + ) def trace(self) -> DescriptorNumber: """ diff --git a/tests/unit_tests/Objects/variable/test_descriptor_array.py b/tests/unit_tests/Objects/variable/test_descriptor_array.py index 924bd0c9..7be5dd17 100644 --- a/tests/unit_tests/Objects/variable/test_descriptor_array.py +++ b/tests/unit_tests/Objects/variable/test_descriptor_array.py @@ -864,97 +864,6 @@ def test_slice_deletion(self, descriptor: DescriptorArray): del descriptor['dim0', 0] assert 'has no attribute' in str(e) - @pytest.mark.parametrize("test", [ - DescriptorNumber("test", 9, "m", 0.52), - DescriptorNumber("test", 10., "cm", 9.), - ], - ids=["DescriptorNumber", "convert_unit"]) - def test_slice_assignment(self, descriptor: DescriptorArray, test): - # When - original_unit = test.unit - descriptor_copy = descriptor.full_value.copy() - descriptor['dim0', 0][0] = test - - # Then - assert descriptor.unit == 'm' - assert test.unit == original_unit - test.convert_unit(descriptor.unit) - assert descriptor.full_value['dim0', 0] == test.full_value.value - assert descriptor.variances[0, 0] == test.full_value.variance - assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) - assert np.allclose(descriptor_copy.variances[:, 1], descriptor.full_value.variances[:, 1]) - - @pytest.mark.parametrize("test", [0.1], - ids=["numbers.Number"]) - def test_slice_assignment_dimensionless(self, test): - # When - descriptor = DescriptorArray( - name="name", - value=[[1., 2.], [3., 4.], [5., 6.]], - unit="dimensionless", - variance=None, - description="description", - url="url", - display_name="display_name", - parent=None, - ) - descriptor['dim0', 0][0] = test - print(descriptor['dim0', 0]) - print(descriptor['dim0', 0][0]) - print(descriptor) - - # Then - assert descriptor.unit == 'dimensionless' - assert descriptor.full_value.values[0, 0] == test - assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) - - @pytest.mark.parametrize("test", [ - DescriptorArray("test + name", - [3.0, 4.0], - "m", - [0.1, 0.2]), - DescriptorArray("test + name", - [3.0, 4.0], - "cm", - [0.1, 0.2]) - ], - ids=["DescriptorArray", "convert_unit"]) - def test_slice_assignment_array(self, descriptor: DescriptorArray, test): - # When - original_unit = test.unit - descriptor_copy = descriptor.full_value.copy() - descriptor['dim1', 0] = test - # Then - assert descriptor.unit == 'm' - assert test.unit == original_unit - test.convert_unit(descriptor.unit) - assert np.allclose(descriptor.full_value.values[:, 0], test.full_value.values) - assert np.allclose(descriptor.full_value.variances[:, 0], test.full_value.variances) - assert np.allclose(descriptor_copy.values[:, 1], descriptor.full_value.values[:, 1]) - assert np.allclose(descriptor_copy.variances[:, 1], descriptor.full_value.variances[:, 1]) - - @pytest.mark.parametrize("test", [ - [3.0, 4.0, 5.0] - ], - ids=["list"]) - def test_slice_assignment_array_dimensionless(self, test): - # When - descriptor = DescriptorArray( - name="name", - value=[[1., 2.], [3., 4.], [5., 6.]], - unit="dimensionless", - variance=None, - description="description", - url="url", - display_name="display_name", - parent=None, - ) - descriptor['dim1', 0] = test - - # Then - assert descriptor.unit == 'dimensionless' - assert np.allclose(descriptor.full_value.values[:, 0], np.array(test)) - @pytest.mark.parametrize("test", [ 1.0, [3.0, 4.0, 5.0] @@ -962,10 +871,9 @@ def test_slice_assignment_array_dimensionless(self, test): ids=["number", "list"]) def test_slice_assignment_exception(self, descriptor_dimensionless: DescriptorArray, test): # When - with pytest.raises(ValueError) as e: + with pytest.raises(AttributeError) as e: descriptor_dimensionless['dim0', :] = test - assert "Values without variances" in str(e) - + assert "cannot be edited via slicing" in str(e) @pytest.mark.parametrize("test, expected", [ (DescriptorArray("test + name", From bed71a957c23ed2144c5dcd5ab3a1a89e63f6bdd Mon Sep 17 00:00:00 2001 From: Eric Lindgren Date: Mon, 3 Mar 2025 16:07:36 +0100 Subject: [PATCH 48/48] add some docstrings and cleanup --- .../Objects/variable/descriptor_array.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/easyscience/Objects/variable/descriptor_array.py b/src/easyscience/Objects/variable/descriptor_array.py index 6ea4806e..4a6f1123 100644 --- a/src/easyscience/Objects/variable/descriptor_array.py +++ b/src/easyscience/Objects/variable/descriptor_array.py @@ -264,7 +264,6 @@ def error(self, error: Union[list, np.ndarray]) -> None: self._array.variances = error**2 else: self._array.variances = None - def convert_unit(self, unit_str: str) -> None: """ @@ -300,12 +299,12 @@ def set_array(obj, scalar): # Update the array self._array = new_array - - # Just to get return type right def __copy__(self) -> DescriptorArray: + """ + Return a copy of the current DescriptorArray. + """ return super().__copy__() - def __repr__(self) -> str: """ Return a string representation of the DescriptorArray, showing its name, value, variance, and unit. @@ -344,6 +343,11 @@ def __repr__(self) -> str: def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Dict representation of the current DescriptorArray. + The dict contains the value, unit and variances, in addition + to the properties of DescriptorBase. + """ raw_dict = super().as_dict(skip=skip) raw_dict['value'] = self._array.values raw_dict['unit'] = str(self._array.unit) @@ -742,6 +746,10 @@ def sum(self, dim: Optional[Union[str, list]] = None) -> DescriptorNumber: def _base_unit(self) -> str: + """ + Returns the base unit of the current array. + For example, if the unit is `100m`, returns `m`. + """ string = str(self._array.unit) for i, letter in enumerate(string): if letter == 'e':