From 9464ec43e16607a13b7b42c3b46ed0b8e4d21143 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Wed, 29 Nov 2023 15:54:35 +0100 Subject: [PATCH 1/7] Remove calculator-specific drives To make micromagneticdata more general we remove all calculator specific reading functionality and move it to the respective adapter packages. This avoids changing micromagneticdata when we add a new calculator and simplifies outside contributions/extensions of Ubermag --- micromagneticdata/__init__.py | 2 - micromagneticdata/drive.py | 66 ++++++++++++---- micromagneticdata/mumax3drive.py | 127 ------------------------------- micromagneticdata/oommfdrive.py | 119 ----------------------------- 4 files changed, 53 insertions(+), 261 deletions(-) delete mode 100644 micromagneticdata/mumax3drive.py delete mode 100644 micromagneticdata/oommfdrive.py diff --git a/micromagneticdata/__init__.py b/micromagneticdata/__init__.py index b344be1..7d89c6b 100644 --- a/micromagneticdata/__init__.py +++ b/micromagneticdata/__init__.py @@ -8,8 +8,6 @@ from .combined_drive import CombinedDrive as CombinedDrive from .data import Data as Data from .drive import Drive as Drive -from .mumax3drive import Mumax3Drive as Mumax3Drive -from .oommfdrive import OOMMFDrive as OOMMFDrive __version__ = importlib.metadata.version(__package__) diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index 4f0d536..358e904 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -1,12 +1,12 @@ import abc import copy +import importlib import json import numbers import pathlib import discretisedfield as df import ipywidgets -import ubermagtable as ut import ubermagutil as uu import ubermagutil.typesystem as ts @@ -76,22 +76,56 @@ class Drive(md.AbstractDrive): """ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): - """Create a new OOMMFDrive or Mumax3Drive depending on the directory structure. + """Create a new drive depending on the calculator. - If a subdirectory .out exists a Mumax3Drive is created else an - OOMMFDrive. + Details of a drive can be calculator specific. Therefore, the individual + adapter classes have to provide the implementation for finding the relevant + data. The information about the adapter is inferred from the 'info.json' file + created by the driver. It can also be passed explicitly. + Simulations run with ubermag <= 2023.11 did not write 'adapter'. For those the + legacy behaviour is kept to determine whether it is an OOMMF or a Mumax3 + simulation, which are the only calculators supported by then. """ - if pathlib.Path(f"{dirname}/{name}/drive-{number}/{name}.out").exists(): - return super().__new__(md.Mumax3Drive) - else: - return super().__new__(md.OOMMFDrive) + if "adapter" in kwargs: + adapter = kwargs["adapter"] + elif (f := pathlib.Path(f"{dirname}/{name}/drive-{number}/info.json")).exists(): + info_json = json.load(f.open()) + if "adapter" in info_json: + adapter = info_json["adapter"] + else: + # info files written with ubermag.__version__ <= 2023.11 do not contain + # 'adapter'; legacy data loading + if pathlib.Path(f"{dirname}/{name}/drive-{number}/{name}.out").exists(): + adapter = "mumax3c" + else: + adapter = "oommfc" + # msg = textwrap.dedent( + # """ + # This simulation has been created with an older version of + # Ubermag and needs manual modification. In the 'info.json' file + # you have to add information about which ubermag package was used + # to run the simulation in the form: '"adapter": ""', e.g. + # when you used OOMMF via oommfc add '"adapter": "oommfc"'. + # """ + # ) + # raise RuntimeError(msg) + # else: + # raise RuntimeError( + # "No 'adapter' has been passed and the adapter could not be determined" + # " automatically because no 'info.json' was found." + # ) + + adapter_module = importlib.import_module(f"{adapter}._output_collecting_util") + return super().__new__(adapter_module.Drive) def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs): # use kwargs to not expose the following additional internal arguments to users self._step_file_list = kwargs.pop("step_files", []) self._table = kwargs.pop("table", None) + kwargs.pop("adapter", None) + super().__init__(**kwargs) self.dirname = dirname self.drive_path = pathlib.Path(f"{dirname}/{name}/drive-{number}") @@ -153,12 +187,18 @@ def _table_path(self): @property def table(self): - if not self.use_cache: - return ut.Table.fromfile(str(self._table_path), x=self.x) + if self.use_cache and self._table is not None: + return self._table + + # extract 'oommfc' from 'oommfc._output_collecting_util' + adapter = self.__class__.__module__.split(".")[0] + adapter_module = importlib.import_module(f"{adapter}._output_collecting_util") + table = adapter_module.table_from_file(str(self._table_path), x=self.x) + + if self.use_cache: + self._table = table - if self._table is None: - self._table = ut.Table.fromfile(str(self._table_path), x=self.x) - return self._table + return table @property @abc.abstractmethod diff --git a/micromagneticdata/mumax3drive.py b/micromagneticdata/mumax3drive.py deleted file mode 100644 index ed4de34..0000000 --- a/micromagneticdata/mumax3drive.py +++ /dev/null @@ -1,127 +0,0 @@ -import pathlib - -import ubermagutil as uu - -import micromagneticdata as md -from .abstract_drive import AbstractDrive - - -@uu.inherit_docs -class Mumax3Drive(md.Drive): - """Drive class for Mumax3Drives (created automatically). - - This class provides utility for the analysis of individual mumax3 drives. It should - not be created explicitly. Instead, use ``micromagneticdata.Drive`` which - automatically creates a ``drive`` object of the correct sub-type. - - Parameters - ---------- - name : str - - System's name. - - number : int - - Drive number. - - dirname : str, optional - - Directory in which system's data is saved. Defults to ``'./'``. - - x : str, optional - - Independent variable column name. Defaults to ``None`` and depending on - the driver used, one is found automatically. - - use_cache : bool, optional - - If ``True`` the Drive object will read tabular data and the names and number of - magnetisation files only once. Note: this prevents Drive to detect new data when - looking at the output of a running simulation. If set to ``False`` the data is - read every time the user accesses it. Defaults to ``False``. - - Raises - ------ - IOError - - If the drive directory cannot be found. - - Examples - -------- - 1. Getting drive object. - - >>> import os - >>> import micromagneticdata as md - ... - >>> dirname = os.path.join(os.path.dirname(__file__), 'tests', 'test_sample') - >>> drive = md.Drive(name='rectangle', number=1, dirname=dirname) - >>> drive - Mumax3Drive(...) - - """ - - def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs): - self._mumax_output_path = pathlib.Path( - f"{dirname}/{name}/drive-{number}/{name}.out" - ) # required to initialise self.x in super - if not self._mumax_output_path.exists(): - raise OSError( - f"Output directory {self._mumax_output_path!r} does not exist." - ) - - super().__init__(name, number, dirname, x, use_cache, **kwargs) - - @AbstractDrive.x.setter - def x(self, value): - if value is None: - # self.info["driver"] in ["TimeDriver", "RelaxDriver", "MinDriver"]: - self._x = "t" - else: - # self.table reads self.x so self._x has to be defined first - if hasattr(self, "_x"): - # store old value to reset in case value is invalid - _x = self._x - self._x = value - if value not in self.table.data.columns: - self._x = _x - raise ValueError(f"Column {value=} does not exist in data.") - - @property - def _table_path(self): - return self._mumax_output_path / "table.txt" - - @property - def _step_file_glob(self): - return self._mumax_output_path.glob("*.ovf") - - @property - def calculator_script(self): - with (self.drive_path / f"{self.name}.mx3").open() as f: - return f.read() - - def __repr__(self): - """Representation string. - - Returns - ------- - str - - Representation string. - - Examples - -------- - 1. Representation string. - - >>> import os - >>> import micromagneticdata as md - ... - >>> dirname = os.path.join(os.path.dirname(__file__), 'tests', 'test_sample') - >>> drive = md.Drive(name='rectangle', number=1, dirname=dirname) - >>> drive - Mumax3Drive(name='rectangle', number=1, dirname='...test_sample', x='t') - - """ - return ( - f"Mumax3Drive(name='{self.name}', number={self.number}, " - f"dirname='{self.dirname}', x='{self.x}')" - ) diff --git a/micromagneticdata/oommfdrive.py b/micromagneticdata/oommfdrive.py deleted file mode 100644 index 8f7ff75..0000000 --- a/micromagneticdata/oommfdrive.py +++ /dev/null @@ -1,119 +0,0 @@ -import ubermagutil as uu - -import micromagneticdata as md -from .abstract_drive import AbstractDrive - - -@uu.inherit_docs -class OOMMFDrive(md.Drive): - """Drive class for OOMMFDrives (created automatically). - - This class provides utility for the analysis of individual OOMMF drives. It should - not be created explicitly. Instead, use ``micromagneticdata.Drive`` which - automatically creates a ``drive`` object of the correct sub-type. - - Parameters - ---------- - name : str - - System's name. - - number : int - - Drive number. - - dirname : str, optional - - Directory in which system's data is saved. Defults to ``'./'``. - - x : str, optional - - Independent variable column name. Defaults to ``None`` and depending on - the driver used, one is found automatically. - - use_cache : bool, optional - - If ``True`` the Drive object will read tabular data and the names and number of - magnetisation files only once. Note: this prevents Drive to detect new data when - looking at the output of a running simulation. If set to ``False`` the data is - read every time the user accesses it. Defaults to ``False``. - - Raises - ------ - IOError - - If the drive directory cannot be found. - - Examples - -------- - 1. Getting drive object. - - >>> import os - >>> import micromagneticdata as md - ... - >>> dirname = os.path.join(os.path.dirname(__file__), 'tests', 'test_sample') - >>> drive = md.Drive(name='rectangle', number=0, dirname=dirname) - - """ - - def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs): - super().__init__(name, number, dirname, x, use_cache, **kwargs) - - @AbstractDrive.x.setter - def x(self, value): - if value is None: - if self.info["driver"] == "TimeDriver": - self._x = "t" - elif self.info["driver"] == "MinDriver": - self._x = "iteration" - elif self.info["driver"] == "HysteresisDriver": - self._x = "B_hysteresis" - else: - # self.table reads self.x so self._x has to be defined first - if hasattr(self, "_x"): - # store old value to reset in case value is invalid - _x = self._x - self._x = value - if value not in self.table.data.columns: - self._x = _x - raise ValueError(f"Column {value=} does not exist in data.") - - @property - def _table_path(self): - return self.drive_path / f"{self.name}.odt" - - @property - def _step_file_glob(self): - return self.drive_path.glob(f"{self.name}*.omf") - - @property - def calculator_script(self): - with (self.drive_path / f"{self.name}.mif").open() as f: - return f.read() - - def __repr__(self): - """Representation string. - - Returns - ------- - str - - Representation string. - - Examples - -------- - 1. Representation string. - - >>> import os - >>> import micromagneticdata as md - ... - >>> dirname = os.path.join(os.path.dirname(__file__), 'tests', 'test_sample') - >>> drive = md.Drive(name='rectangle', number=0, dirname=dirname) - >>> drive - OOMMFDrive(name='rectangle', number=0, dirname='...test_sample', x='t') - - """ - return ( - f"OOMMFDrive(name='{self.name}', number={self.number}, " - f"dirname='{self.dirname}', x='{self.x}')" - ) From ebc2405fc4aada640d714a0ff40fb9843323eef4 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Wed, 29 Nov 2023 22:27:23 +0100 Subject: [PATCH 2/7] Error message if 'adapter' cannot be inferred automatically --- micromagneticdata/drive.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index 358e904..e496173 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -110,11 +110,11 @@ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): # """ # ) # raise RuntimeError(msg) - # else: - # raise RuntimeError( - # "No 'adapter' has been passed and the adapter could not be determined" - # " automatically because no 'info.json' was found." - # ) + else: + raise RuntimeError( + "No 'adapter' has been passed and the adapter could not be determined" + " automatically because no 'info.json' was found." + ) adapter_module = importlib.import_module(f"{adapter}._output_collecting_util") return super().__new__(adapter_module.Drive) From d909a1185a73bac8f560e9e2ef66057a0777a2d6 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Sat, 23 Mar 2024 15:00:07 +0100 Subject: [PATCH 3/7] Entry points to read data using a suitable adapter class --- micromagneticdata/drive.py | 68 ++++++++++++++++++++++++-------------- pyproject.toml | 1 + 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index e496173..97b7947 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -1,9 +1,9 @@ import abc import copy -import importlib import json import numbers import pathlib +import sys import discretisedfield as df import ipywidgets @@ -12,6 +12,11 @@ import micromagneticdata as md +if sys.version_info.minor < 10: + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points + @uu.inherit_docs @ts.typesystem( @@ -78,14 +83,20 @@ class Drive(md.AbstractDrive): def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): """Create a new drive depending on the calculator. - Details of a drive can be calculator specific. Therefore, the individual - adapter classes have to provide the implementation for finding the relevant - data. The information about the adapter is inferred from the 'info.json' file - created by the driver. It can also be passed explicitly. + Details of a drive can be calculator specific. Therefore, the individual adapter + classes have to provide the implementation for finding the relevant data. The + information about the adapter is inferred from the 'info.json' file created by + the driver, e.g. when using OOMMF via oommfc, the json file contains::: + + "adapter": "oommfc" + + A suitable adapter can also be passed explicitly. + + Simulations computed with ubermag <= 2023.11 did not write 'adapter' in the + 'info.json'. For those the legacy behaviour is kept to determine whether it is + an OOMMF or a Mumax3 simulation, which were the only supported calculators at + that time. - Simulations run with ubermag <= 2023.11 did not write 'adapter'. For those the - legacy behaviour is kept to determine whether it is an OOMMF or a Mumax3 - simulation, which are the only calculators supported by then. """ if "adapter" in kwargs: adapter = kwargs["adapter"] @@ -100,24 +111,24 @@ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): adapter = "mumax3c" else: adapter = "oommfc" - # msg = textwrap.dedent( - # """ - # This simulation has been created with an older version of - # Ubermag and needs manual modification. In the 'info.json' file - # you have to add information about which ubermag package was used - # to run the simulation in the form: '"adapter": ""', e.g. - # when you used OOMMF via oommfc add '"adapter": "oommfc"'. - # """ - # ) - # raise RuntimeError(msg) else: raise RuntimeError( "No 'adapter' has been passed and the adapter could not be determined" " automatically because no 'info.json' was found." ) - adapter_module = importlib.import_module(f"{adapter}._output_collecting_util") - return super().__new__(adapter_module.Drive) + drive_entry_points = entry_points( + group="micromagneticdata.output_collecting.Drive" + ) + + try: + CalculatorDrive = drive_entry_points[adapter].load() + except KeyError: + raise RuntimeError( + f"'{adapter}' must be installed to read the drive." + ) from None + + return super().__new__(CalculatorDrive) def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs): # use kwargs to not expose the following additional internal arguments to users @@ -151,6 +162,15 @@ def dirname(self, dirname): ) self._dirname = str(dirname) + @property + def _adapter(self): + """ + Name of the adapter package, e.g. oommfc, used to read the drive. + The information can be used to load additional entry points from that package. + """ + # extract package name + return self.__class__.__module__.split(".")[0] + @property def use_cache(self): """Use caching for scalar data and the list of magnetisation files. @@ -190,10 +210,10 @@ def table(self): if self.use_cache and self._table is not None: return self._table - # extract 'oommfc' from 'oommfc._output_collecting_util' - adapter = self.__class__.__module__.split(".")[0] - adapter_module = importlib.import_module(f"{adapter}._output_collecting_util") - table = adapter_module.table_from_file(str(self._table_path), x=self.x) + read_table = entry_points( + group="micromagneticdata.output_collecting.read_table" + )[self._adapter].load() + table = read_table(str(self._table_path), x=self.x) if self.use_cache: self._table = table diff --git a/pyproject.toml b/pyproject.toml index 2531b3c..d15aeda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ classifiers = [ dependencies = [ "discretisedfield>=0.92.0", "ubermagtable>=0.62.0" + "importlib_metadata; python_version<'3.10'" ] [project.optional-dependencies] From 92f094d600a0d3e967549b5f34bcd667c1e4eb4b Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Tue, 9 Jun 2026 09:48:03 +0200 Subject: [PATCH 4/7] Fix syntax --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d15aeda..6a31a23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,8 @@ classifiers = [ dependencies = [ "discretisedfield>=0.92.0", - "ubermagtable>=0.62.0" - "importlib_metadata; python_version<'3.10'" + "ubermagtable>=0.62.0", + "importlib_metadata; python_version<'3.10'", ] [project.optional-dependencies] From bb31e60ae2b56a7a3ca4cbb6cc329a79a6994bd7 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Fri, 12 Jun 2026 16:30:40 +0200 Subject: [PATCH 5/7] Rename plugins --- micromagneticdata/drive.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index 97b7947..1667b5f 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -118,7 +118,7 @@ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): ) drive_entry_points = entry_points( - group="micromagneticdata.output_collecting.Drive" + group="micromagneticdata.plugins.CalculatorDrive" ) try: @@ -210,9 +210,9 @@ def table(self): if self.use_cache and self._table is not None: return self._table - read_table = entry_points( - group="micromagneticdata.output_collecting.read_table" - )[self._adapter].load() + read_table = entry_points(group="micromagneticdata.plugins.read_table")[ + self._adapter + ].load() table = read_table(str(self._table_path), x=self.x) if self.use_cache: From 07743e7dd67335a08ee4f4dec66517f1e273da2b Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Wed, 17 Jun 2026 18:01:04 +0200 Subject: [PATCH 6/7] WIP restructure tests --- micromagneticdata/abstract_drive.py | 5 - micromagneticdata/drive.py | 22 +- micromagneticdata/testing/__init__.py | 0 micromagneticdata/testing/drive.py | 44 ++ micromagneticdata/tests/test_drive.py | 532 ++++++++++++---------- micromagneticdata/tests/test_drive_old.py | 8 + 6 files changed, 349 insertions(+), 262 deletions(-) create mode 100644 micromagneticdata/testing/__init__.py create mode 100644 micromagneticdata/testing/drive.py create mode 100644 micromagneticdata/tests/test_drive_old.py diff --git a/micromagneticdata/abstract_drive.py b/micromagneticdata/abstract_drive.py index 7a9442f..025d539 100644 --- a/micromagneticdata/abstract_drive.py +++ b/micromagneticdata/abstract_drive.py @@ -26,11 +26,6 @@ class AbstractDrive(abc.ABC): def __init__(self, callbacks=None): self._callbacks = callbacks or [] - @abc.abstractmethod - def __repr__(self): - """Representation string.""" - pass # pragma: no cover - @property def x(self): """Independent variable name. diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index 1667b5f..fb8f923 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -1,9 +1,9 @@ import abc import copy +import importlib.metadata import json import numbers import pathlib -import sys import discretisedfield as df import ipywidgets @@ -12,11 +12,6 @@ import micromagneticdata as md -if sys.version_info.minor < 10: - from importlib_metadata import entry_points -else: - from importlib.metadata import entry_points - @uu.inherit_docs @ts.typesystem( @@ -117,7 +112,7 @@ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): " automatically because no 'info.json' was found." ) - drive_entry_points = entry_points( + drive_entry_points = importlib.metadata.entry_points( group="micromagneticdata.plugins.CalculatorDrive" ) @@ -149,6 +144,12 @@ def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs self.number = number self.x = x + def __repr__(self): + return ( + f"{self.__class__.__name__}(name='{self.name}', number={self.number}, " + f"dirname='{self.dirname}', x='{self.x}')" + ) + @property def dirname(self): """Directory containing the system's data.""" @@ -210,9 +211,9 @@ def table(self): if self.use_cache and self._table is not None: return self._table - read_table = entry_points(group="micromagneticdata.plugins.read_table")[ - self._adapter - ].load() + read_table = importlib.metadata.entry_points( + group="micromagneticdata.plugins.read_table" + )[self._adapter].load() table = read_table(str(self._table_path), x=self.x) if self.use_cache: @@ -422,7 +423,6 @@ def slider(self, description="step", **kwargs): def __lshift__(self, other): if isinstance(other, md.Drive): - # no use of self.__class__ to allow combining Mumax3 and OOMMF runs return md.CombinedDrive(self, other) elif isinstance(other, md.CombinedDrive): return md.CombinedDrive(self, *other.drives) diff --git a/micromagneticdata/testing/__init__.py b/micromagneticdata/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/micromagneticdata/testing/drive.py b/micromagneticdata/testing/drive.py new file mode 100644 index 0000000..690068b --- /dev/null +++ b/micromagneticdata/testing/drive.py @@ -0,0 +1,44 @@ +"""Tests for plugins""" + +import discretisedfield as df +import pytest +import ubermagtable as ut + + +def test_x(drive, drive_x): + assert isinstance(drive.x, str) + assert drive.x == drive_x + + with pytest.raises(ValueError): + drive.x = "not_a_valid_column_name" + + +def test_set_x(drive, drive_x, new_drive_column): + assert drive.x == drive_x + drive.x = new_drive_column + + assert drive.x == new_drive_column + + +def test_calculator_script(drive, calculator_script_content): + assert isinstance(drive.calculator_script, str) + assert calculator_script_content in drive.calculator_script + + +def test_table(drive): + assert isinstance(drive.table, ut.Table) + assert drive.table.x == drive.x + + +def test_m0(drive): + assert isinstance(drive.m0, df.Field) + + +def test_getitem(drive): + for i in range(drive.n): + assert isinstance(drive[i], df.Field) + + +def test_iter(drive): + for m in drive: + assert isinstance(m, df.Field) diff --git a/micromagneticdata/tests/test_drive.py b/micromagneticdata/tests/test_drive.py index 4dd60e0..0adc4ad 100644 --- a/micromagneticdata/tests/test_drive.py +++ b/micromagneticdata/tests/test_drive.py @@ -1,3 +1,6 @@ +# from micromagneticdata.testing.drive import * # noqa: F403 + +import importlib.metadata import os from pathlib import Path @@ -9,284 +12,321 @@ import xarray as xr from discretisedfield.tests.test_field import check_hv -import micromagneticdata as md +import micromagneticdata as mdata -class TestDrive: - def setup_method(self): - self.dirname = os.path.join(os.path.dirname(__file__), "test_sample") - self.name = "rectangle" - self.data = md.Data(name=self.name, dirname=self.dirname) +@pytest.fixture +def drive(): + """Fixture that returns a single drive. - def test_init(self): - # str for dirname - drive = md.Drive(name=self.name, number=0, dirname=self.dirname) - assert isinstance(drive, md.Drive) + Parametrize the fixture to test different types of drives. + """ + pass - # Path for dirname - drive2 = md.Drive(name=self.name, number=0, dirname=Path(self.dirname)) - assert isinstance(drive2, md.Drive) - assert drive.name == drive2.name - assert drive.number == drive2.number - assert drive.dirname == drive2.dirname - # Exception - with pytest.raises(IOError): - drive = md.Drive(name=self.name, number=11, dirname=self.dirname) +@pytest.fixture +def drive_x(): + pass - def test_repr(self): - for drive in self.data: - assert isinstance(repr(drive), str) - assert "Drive" in repr(drive) - def test_x(self): - for drive in self.data: - assert isinstance(drive.x, str) - assert drive.x in ["t", "iteration", "B_hysteresis"] +@pytest.fixture +def new_drive_column(): + pass - self.data[0].x = "mx" - # Exception - with pytest.raises(ValueError): - self.data[0].x = "wrong" - - def test_info(self): - for i, drive in enumerate(self.data): - assert isinstance(drive.info, dict) - assert drive.info["drive_number"] == i - - def test_mif(self): - for i in [0, 5, 6]: - drive = self.data[i] - assert isinstance(drive.calculator_script, str) - assert "MIF" in drive.calculator_script - - def test_mx3(self): - for i in [1, 2, 3, 4]: - drive = self.data[i] - assert isinstance(drive.calculator_script, str) - assert "tableadd" in drive.calculator_script - - def test_valid(self): - dirname = os.path.join(os.path.dirname(__file__), "test_sample") - name = "hysteresis" - data = md.Data(name=name, dirname=dirname) - m0_field = data[0].m0 - test_points = [ - m0_field.mesh.point2index(m0_field.mesh.region.pmin), - m0_field.mesh.point2index(m0_field.mesh.region.center), - m0_field.mesh.point2index(m0_field.mesh.region.pmax), - ] - expected_validity = [False, True, False] - for point, expected in zip(test_points, expected_validity): - actual_valid = m0_field.valid[point] - assert actual_valid == expected - drive = data[0] - for d in drive: - for point, expected in zip(test_points, expected_validity): - actual_valid = d.valid[point] - assert actual_valid == expected - - def test_m0(self): - for drive in self.data: - assert isinstance(drive.m0, df.Field) - - def test_table(self): - for drive in self.data: - assert isinstance(drive.table, ut.Table) - assert drive.table.x == drive.x - - def test_n(self): - for drive in self.data: - assert isinstance(drive.n, int) - assert self.data[0].n == 25 - - def test_getitem_int(self): - for i in range(self.data[0].n): - assert isinstance(self.data[0][i], df.Field) - - def test_getitem_slice(self): - drive = self.data[0] - assert drive.n == 25 - - sel = drive[:] - assert isinstance(sel, md.Drive) - assert sel.n == 25 - assert len(list(sel)) == 25 - assert sel.use_cache - - sel = drive[:1] - assert isinstance(sel, md.Drive) - assert sel.n == 1 - assert len(list(sel)) == 1 - assert sel.use_cache - - sel = drive[:-3] - assert isinstance(sel, md.Drive) - assert sel.n == 22 - assert len(list(sel)) == 22 - assert sel.use_cache - - sel = drive[4:8] - assert isinstance(sel, md.Drive) - assert sel.n == 4 - assert len(list(sel)) == 4 - assert sel.use_cache - - sel = drive[::2] - assert isinstance(sel, md.Drive) - assert sel.n == 13 - assert len(list(sel)) == 13 - assert sel.use_cache - - def test_iter(self): - for drive in self.data: - for m in drive: - assert isinstance(m, df.Field) - - assert len(list(self.data[0])) == 25 - - def test_ovf2vtk(self, tmp_path): - self.data[0].ovf2vtk(dirname=tmp_path) - - def test_slider(self): - for drive in self.data: - assert isinstance(drive.slider(), ipywidgets.IntSlider) - - def test_lshift(self): - # TimeDriver: 0, 1, 2, 5 - # MinDriver: 4, 6 - # RelaxDriver: 3 - # HysteresisDriver: 7 [CURRENTLY MISSING IN THE DATASET] - for d1, d2 in [(0, 1), (6, 6), (3, 3)]: - combined = self.data[d1] << self.data[d2] - assert isinstance(combined, md.CombinedDrive) - assert len(combined.drives) == 2 - assert combined.info["driver"] == self.data[d1].info["driver"] - assert combined.x == self.data[d1].x - assert len(combined.table.data) == combined.n - - for d1, d2 in [(0, 6), (3, 6), (4, 6)]: - # TODO - # (0, 3), (0, 4) should be added and fail - # (4, 6) mixes OOMMF and Mumax3 min drive which does not work because - # they have different independent variables - with pytest.raises(ValueError): - self.data[d1] << self.data[d2] - with pytest.raises(TypeError): - self.data[0] << 1 - - def test_to_xarray(self): - for drive in self.data: - assert isinstance(drive.to_xarray(), xr.DataArray) - assert all( - item in drive.to_xarray().attrs.items() for item in drive.info.items() - ) - if len(drive._step_files) != 1: - assert len(drive.to_xarray()[drive.table.x]) == len(drive._step_files) - assert np.allclose( - drive.to_xarray()[drive.table.x].values, - drive.table.data[drive.table.x].to_numpy(), - ) +##################### - if drive.info["driver"] == "HysteresisDriver": - assert all( - np.allclose( - drive.to_xarray()[f"B{i}_hysteresis"].values, - drive.table.data[f"B{i}_hysteresis"].to_numpy(), - ) - for i in "xyz" - ) - def test_hv(self): - # time drive - check_hv( - self.data[0].hv(kdims=["y", "z"], vdims=["y", "z"]), - ["DynamicMap [x,t]", "Image [y,z]", "VectorField [y,z]"], - ) - check_hv( - self.data[0].hv.scalar(kdims=["y", "z"]), - ["DynamicMap [x,vdims,t]", "Image [y,z]"], +@pytest.fixture +def sample_drive(monkeypatch): + def mock_entry_points(*args, **kwargs): + print("inside the mock") + return importlib.metadata.EntryPoints( + [ + importlib.metadata.EntryPoint( + name="test", + group="micromagneticdata.plugins.CalculatorDrive", + value="micromagneticdata.tests.test_drive:SampleDrive", + ) + ] ) - with pytest.raises(NotImplementedError): - check_hv(self.data[0].hv.scalar(kdims=["x", "t"]), ...) + monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points) + # length 25 + return SampleDrive("abc", 0, adapter="test") + + +@pytest.fixture +def self(): + pass + + +##################### + + +class SampleDrive(mdata.Drive): + @mdata.AbstractDrive.x.setter + def x(self, value): + self._x = value + + @property + def _table_path(self): + return self.drive_path / "table.csv" + + @property + def _step_file_glob(self): + return self.drive_path.glob("m-*.hdf5") + + @property + def calculator_script(): + return "test calculator" - # min drive - check_hv( - self.data[4] - .register_callback(lambda f: f.sel("z")) - .hv.vector(kdims=["x", "y"]), - ["VectorField [x,y]"], + +##################### + +# TODO test plugin registration + + +def test_init(sample_drive): + # str for dirname + sample_drive = sample_drive + drive = mdata.Drive(name=sample_drive.name, number=0, dirname=sample_drive.dirname) + assert isinstance(drive, mdata.Drive) + + # Path for dirname + drive2 = mdata.Drive( + name=sample_drive.name, number=0, dirname=Path(sample_drive.dirname) + ) + assert isinstance(drive2, mdata.Drive) + assert drive.name == drive2.name + assert drive.number == drive2.number + assert drive.dirname == drive2.dirname + + # Exception + with pytest.raises(IOError): + drive = mdata.Drive( + name=sample_drive.name, number=11, dirname=sample_drive.dirname ) - # min drive with steps - check_hv( - self.data[6].hv.vector(kdims=["x", "y"]), - ["DynamicMap [z,iteration]", "VectorField [x,y]"], + +def test_n(drive): + assert isinstance(drive.n, int) + + +def test_repr(drive): + assert isinstance(repr(drive), str) + assert "Drive" in repr(drive) + + +def test_info(self): + for i, drive in enumerate(self.data): + assert isinstance(drive.info, dict) + assert drive.info["drive_number"] == i + + +def test_valid(drive): + dirname = os.path.join(os.path.dirname(__file__), "test_sample") + name = "hysteresis" + data = mdata.Data(name=name, dirname=dirname) + m0_field = data[0].m0 + test_points = [ + m0_field.mesh.point2index(m0_field.mesh.region.pmin), + m0_field.mesh.point2index(m0_field.mesh.region.center), + m0_field.mesh.point2index(m0_field.mesh.region.pmax), + ] + expected_validity = [False, True, False] + for point, expected in zip(test_points, expected_validity): + actual_valid = m0_field.valid[point] + assert actual_valid == expected + + drive = data[0] + for d in drive: + for point, expected in zip(test_points, expected_validity): + actual_valid = d.valid[point] + assert actual_valid == expected + + +def test_n_reference_data(sample_drive): + assert sample_drive.n == 25 + + +def test_iter_reference_data(sample_drive): + assert len(list(sample_drive)) == 25 + + +def test_getitem_slice(sample_drive): + assert sample_drive.n == 25 + + sel = sample_drive[:] + assert isinstance(sel, mdata.Drive) + assert sel.n == 25 + assert len(list(sel)) == 25 + assert sel.use_cache + + sel = sample_drive[:1] + assert isinstance(sel, mdata.Drive) + assert sel.n == 1 + assert len(list(sel)) == 1 + assert sel.use_cache + + sel = sample_drive[:-3] + assert isinstance(sel, mdata.Drive) + assert sel.n == 22 + assert len(list(sel)) == 22 + assert sel.use_cache + + sel = sample_drive[4:8] + assert isinstance(sel, mdata.Drive) + assert sel.n == 4 + assert len(list(sel)) == 4 + assert sel.use_cache + + sel = sample_drive[::2] + assert isinstance(sel, mdata.Drive) + assert sel.n == 13 + assert len(list(sel)) == 13 + assert sel.use_cache + + +def test_to_xarray(drive): + assert isinstance(drive.to_xarray(), xr.DataArray) + assert all(item in drive.to_xarray().attrs.items() for item in drive.info.items()) + if len(drive._step_files) != 1: + assert len(drive.to_xarray()[drive.table.x]) == len(drive._step_files) + assert np.allclose( + drive.to_xarray()[drive.table.x].values, + drive.table.data[drive.table.x].to_numpy(), ) - def test_register_callback(self): - for drive in self.data: - drive_orientation = drive.register_callback(lambda field: field.orientation) - assert isinstance(drive_orientation, drive.__class__) - assert len(drive_orientation._callbacks) == 1 - for field in drive_orientation: - assert np.max(field.array) <= 1.0 - assert np.min(field.array) >= -1.0 - - drive = self.data[0] - processed = drive.register_callback(lambda f: f.orientation) - processed = processed.register_callback(lambda f: f.x) - for field in processed: - assert field.nvdim == 1 + +def test_hv_time_drive(self): + # time drive + check_hv( + self.data[0].hv(kdims=["y", "z"], vdims=["y", "z"]), + ["DynamicMap [x,t]", "Image [y,z]", "VectorField [y,z]"], + ) + check_hv( + self.data[0].hv.scalar(kdims=["y", "z"]), + ["DynamicMap [x,vdims,t]", "Image [y,z]"], + ) + + with pytest.raises(NotImplementedError): + check_hv(self.data[0].hv.scalar(kdims=["x", "t"]), ...) + + +def test_hv_min_drive(self): + # min drive + check_hv( + self.data[4] + .register_callback(lambda f: f.sel("z")) + .hv.vector(kdims=["x", "y"]), + ["VectorField [x,y]"], + ) + + +def test_hv_min_drive_steps(self): + # min drive with steps + check_hv( + self.data[6].hv.vector(kdims=["x", "y"]), + ["DynamicMap [z,iteration]", "VectorField [x,y]"], + ) + + +def test_lshift(self): + # TimeDriver: 0, 1, 2, 5 + # MinDriver: 4, 6 + # RelaxDriver: 3 + # HysteresisDriver: 7 [CURRENTLY MISSING IN THE DATASET] + for d1, d2 in [(0, 1), (6, 6), (3, 3)]: + combined = self.data[d1] << self.data[d2] + assert isinstance(combined, mdata.CombinedDrive) + assert len(combined.drives) == 2 + assert combined.info["driver"] == self.data[d1].info["driver"] + assert combined.x == self.data[d1].x + assert len(combined.table.data) == combined.n + + for d1, d2 in [(0, 6), (3, 6), (4, 6)]: + # TODO + # (0, 3), (0, 4) should be added and fail + # (4, 6) mixes OOMMF and Mumax3 min drive which does not work because + # they have different independent variables + with pytest.raises(ValueError): + self.data[d1] << self.data[d2] + with pytest.raises(TypeError): + self.data[0] << 1 + + +def test_register_callback(self): + for drive in self.data: + drive_orientation = drive.register_callback(lambda field: field.orientation) + assert isinstance(drive_orientation, drive.__class__) + assert len(drive_orientation._callbacks) == 1 + for field in drive_orientation: assert np.max(field.array) <= 1.0 assert np.min(field.array) >= -1.0 - assert len(processed.callbacks) == 2 - - def test_cache(self, monkeypatch): - ref = self.data[0] - drive = md.Drive(ref.name, ref.number, ref.dirname, ref.x, use_cache=True) + drive = self.data[0] + processed = drive.register_callback(lambda f: f.orientation) + processed = processed.register_callback(lambda f: f.x) + for field in processed: + assert field.nvdim == 1 + assert np.max(field.array) <= 1.0 + assert np.min(field.array) >= -1.0 - assert len(list(drive)) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) + assert len(processed.callbacks) == 2 - with monkeypatch.context() as m: - m.setattr(drive.__class__, "_step_file_glob", ["a.omf", "b.omf"]) - m.setattr(drive.__class__, "_table_path", "wrong_path") - assert len(drive._step_files) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) +def test_cache(sample_drive, monkeypatch): + drive = mdata.Drive( + sample_drive.name, + sample_drive.number, + sample_drive.dirname, + sample_drive.x, + use_cache=True, + ) - drive.use_cache = False + assert len(list(drive)) == 25 + assert isinstance(drive[0], df.Field) + assert isinstance(drive.table, ut.Table) - assert drive._step_files == ["a.omf", "b.omf"] - with pytest.raises(FileNotFoundError): - drive[0] - with pytest.raises(FileNotFoundError): - drive.table # noqa: B018 + with monkeypatch.context() as m: + m.setattr(drive.__class__, "_step_file_glob", ["a.omf", "b.omf"]) + m.setattr(drive.__class__, "_table_path", "wrong_path") - drive.use_cache = True # check new caching (no old cache) + assert len(drive._step_files) == 25 + assert isinstance(drive[0], df.Field) + assert isinstance(drive.table, ut.Table) - assert drive._step_files == ["a.omf", "b.omf"] - with pytest.raises(FileNotFoundError): - drive[0] - with pytest.raises(FileNotFoundError): - drive.table # noqa: B018 + drive.use_cache = False - # caching has effects outside monkeypatch context assert drive._step_files == ["a.omf", "b.omf"] with pytest.raises(FileNotFoundError): drive[0] - # no table object is cached - assert isinstance(drive.table, ut.Table) + with pytest.raises(FileNotFoundError): + drive.table # noqa: B018 - drive.use_cache = False # remove cached monkeypatch drive.use_cache = True # check new caching (no old cache) - assert len(list(drive)) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) + assert drive._step_files == ["a.omf", "b.omf"] + with pytest.raises(FileNotFoundError): + drive[0] + with pytest.raises(FileNotFoundError): + drive.table # noqa: B018 + + # caching has effects outside monkeypatch context + assert drive._step_files == ["a.omf", "b.omf"] + with pytest.raises(FileNotFoundError): + drive[0] + # no table object is cached + assert isinstance(drive.table, ut.Table) + + drive.use_cache = False # remove cached monkeypatch + drive.use_cache = True # check new caching (no old cache) + + assert len(list(drive)) == 25 + assert isinstance(drive[0], df.Field) + assert isinstance(drive.table, ut.Table) + + +def test_slider(drive): + assert isinstance(drive.slider(), ipywidgets.IntSlider) diff --git a/micromagneticdata/tests/test_drive_old.py b/micromagneticdata/tests/test_drive_old.py new file mode 100644 index 0000000..1b40e27 --- /dev/null +++ b/micromagneticdata/tests/test_drive_old.py @@ -0,0 +1,8 @@ +from micromagneticdata.testing.drive import * # noqa: F403 + + +class TestDrive: + def setup_method(self): + self.dirname = os.path.join(os.path.dirname(__file__), "test_sample") + self.name = "rectangle" + self.data = md.Data(name=self.name, dirname=self.dirname) From ee32602182466bde9cd6ec9e6fad6ff74831b854 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Thu, 18 Jun 2026 11:29:42 +0200 Subject: [PATCH 7/7] Refactor tests --- micromagneticdata/drive.py | 9 +- micromagneticdata/testing/drive.py | 10 +- micromagneticdata/tests/test_drive.py | 340 +++++++++++++++----------- 3 files changed, 205 insertions(+), 154 deletions(-) diff --git a/micromagneticdata/drive.py b/micromagneticdata/drive.py index fb8f923..8fbf03d 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -95,8 +95,12 @@ def __new__(cls, name, number, dirname=".", x=None, use_cache=False, **kwargs): """ if "adapter" in kwargs: adapter = kwargs["adapter"] + if not (drive_dir := pathlib.Path(f"{dirname}/{name}/drive-{number}")).is_dir(): + msg = f"Directory {drive_dir!r} does not exist." + raise OSError(msg) + elif (f := pathlib.Path(f"{dirname}/{name}/drive-{number}/info.json")).exists(): - info_json = json.load(f.open()) + info_json = json.loads(f.read_text()) if "adapter" in info_json: adapter = info_json["adapter"] else: @@ -135,9 +139,6 @@ def __init__(self, name, number, dirname="./", x=None, use_cache=False, **kwargs super().__init__(**kwargs) self.dirname = dirname self.drive_path = pathlib.Path(f"{dirname}/{name}/drive-{number}") - if not self.drive_path.exists(): - msg = f"Directory {self.drive_path!r} does not exist." - raise OSError(msg) self.use_cache = use_cache self.name = name diff --git a/micromagneticdata/testing/drive.py b/micromagneticdata/testing/drive.py index 690068b..3c20800 100644 --- a/micromagneticdata/testing/drive.py +++ b/micromagneticdata/testing/drive.py @@ -5,6 +5,10 @@ import ubermagtable as ut +def test_n(drive): + assert isinstance(drive.n, int) + + def test_x(drive, drive_x): assert isinstance(drive.x, str) assert drive.x == drive_x @@ -13,11 +17,11 @@ def test_x(drive, drive_x): drive.x = "not_a_valid_column_name" -def test_set_x(drive, drive_x, new_drive_column): +def test_set_x(drive, drive_x, new_drive_x): assert drive.x == drive_x - drive.x = new_drive_column + drive.x = new_drive_x - assert drive.x == new_drive_column + assert drive.x == new_drive_x def test_calculator_script(drive, calculator_script_content): diff --git a/micromagneticdata/tests/test_drive.py b/micromagneticdata/tests/test_drive.py index 0adc4ad..0bb6c58 100644 --- a/micromagneticdata/tests/test_drive.py +++ b/micromagneticdata/tests/test_drive.py @@ -1,72 +1,67 @@ -# from micromagneticdata.testing.drive import * # noqa: F403 - import importlib.metadata +import json import os from pathlib import Path import discretisedfield as df import ipywidgets import numpy as np +import pandas as pd import pytest import ubermagtable as ut import xarray as xr from discretisedfield.tests.test_field import check_hv import micromagneticdata as mdata +from micromagneticdata.testing.drive import * # noqa: F403 @pytest.fixture -def drive(): +def drive(tmp_path, monkeypatch): """Fixture that returns a single drive. Parametrize the fixture to test different types of drives. """ - pass - + # mock the plugin detection so that micromagneticadata can be tested without + # any plugins; adapter packages must not do that + monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points) -@pytest.fixture -def drive_x(): - pass + system_name = "test_system" + index = 0 + _create_drive(tmp_path, system_name, index, n_steps=25) + return SampleDrive(system_name, index, dirname=tmp_path) @pytest.fixture -def new_drive_column(): - pass - - -##################### +def drive_x(): + """Independent variable of the drive.""" + return "t" @pytest.fixture -def sample_drive(monkeypatch): - def mock_entry_points(*args, **kwargs): - print("inside the mock") - return importlib.metadata.EntryPoints( - [ - importlib.metadata.EntryPoint( - name="test", - group="micromagneticdata.plugins.CalculatorDrive", - value="micromagneticdata.tests.test_drive:SampleDrive", - ) - ] - ) - - monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points) - # length 25 - return SampleDrive("abc", 0, adapter="test") +def new_drive_x(): + """An other column in drive.table, that can be used as indendent variable.""" + return "mx" @pytest.fixture -def self(): - pass +def calculator_script_content(): + """Representative section of a calculator script.""" + return "run simulation" ##################### +# Mock data for tests in micromagneticadata; adapter packages don't need this section +# and should instead test with real data + class SampleDrive(mdata.Drive): @mdata.AbstractDrive.x.setter def x(self, value): + value = value or "t" + if value not in ["t", "mx", "my", "mz"]: + raise ValueError(f"Unsupported x={value}") self._x = value @property @@ -78,8 +73,79 @@ def _step_file_glob(self): return self.drive_path.glob("m-*.hdf5") @property - def calculator_script(): - return "test calculator" + def calculator_script(self): + return (self.drive_path / "script.txt").read_text() + + +def read_table(filename, x=None, rename=True): + return ut.Table( + pd.read_csv(filename), units={"t": "s", "mx": "", "my": "", "mz": ""}, x=x + ) + + +def mock_entry_points(group): + # micromagneticdata.Drive detects plugins to load a suitable drive. + # For testing we mock this to use the SampleDrive class. + if group == "micromagneticdata.plugins.CalculatorDrive": + return importlib.metadata.EntryPoints( + [ + importlib.metadata.EntryPoint( + name="micromagneticdata", + value="micromagneticdata.tests.test_drive:SampleDrive", + group="micromagneticdata.plugins.CalculatorDrive", + ), + ] + ) + elif group == "micromagneticdata.plugins.read_table": + return importlib.metadata.EntryPoints( + [ + importlib.metadata.EntryPoint( + name="micromagneticdata", + value="micromagneticdata.tests.test_drive:read_table", + group="micromagneticdata.plugins.read_table", + ), + ] + ) + else: + raise NotImplementedError(f"Group {group} not supported.") + + +def _create_drive(base: Path, system_name, index, n_steps): + """Create a drive with 25 steps and metadata compatible with SampleDrive.""" + drive_dir = base / system_name / f"drive-{index}" + drive_dir.mkdir(parents=True) + # minimalistic info json, incomplete but sufficient for tests + (drive_dir / "info.json").write_text( + json.dumps( + { + "drive_number": index, + "adapter": "micromagneticdata", + "driver": "SampleDriver", + } + ) + ) + # fake tabular data + pd.DataFrame( + { + "t": list(range(1, n_steps + 1)), + "mx": [0] * n_steps, + "my": [0] * n_steps, + "mz": [1] * n_steps, + } + ).to_csv(drive_dir / "table.csv") + m = df.Field( + mesh=df.Mesh(p1=(0, 0, 0), p2=(1, 1, 1), n=(5, 5, 5)), + nvdim=3, + value=(0, 1, 1), + norm=1e5, + ) + # initial magnetisation + m.to_file(drive_dir / "m0.omf") + # fake output magnetisation + for i in range(1, n_steps + 1): + m.to_file(drive_dir / f"m-{i:03}.hdf5") + # fake simulation script for the calculator + (drive_dir / "script.txt").write_text("run simulation") ##################### @@ -87,30 +153,21 @@ def calculator_script(): # TODO test plugin registration -def test_init(sample_drive): +def test_init(drive): # str for dirname - sample_drive = sample_drive - drive = mdata.Drive(name=sample_drive.name, number=0, dirname=sample_drive.dirname) - assert isinstance(drive, mdata.Drive) + created_drive = mdata.Drive(name=drive.name, number=0, dirname=drive.dirname) + assert isinstance(created_drive, mdata.Drive) # Path for dirname - drive2 = mdata.Drive( - name=sample_drive.name, number=0, dirname=Path(sample_drive.dirname) - ) + drive2 = mdata.Drive(name=drive.name, number=0, dirname=Path(drive.dirname)) assert isinstance(drive2, mdata.Drive) - assert drive.name == drive2.name - assert drive.number == drive2.number - assert drive.dirname == drive2.dirname + assert created_drive.name == drive2.name + assert created_drive.number == drive2.number + assert created_drive.dirname == drive2.dirname # Exception - with pytest.raises(IOError): - drive = mdata.Drive( - name=sample_drive.name, number=11, dirname=sample_drive.dirname - ) - - -def test_n(drive): - assert isinstance(drive.n, int) + with pytest.raises(OSError): + created_drive = mdata.Drive(name=drive.name, number=1, dirname=drive.dirname) def test_repr(drive): @@ -118,12 +175,12 @@ def test_repr(drive): assert "Drive" in repr(drive) -def test_info(self): - for i, drive in enumerate(self.data): - assert isinstance(drive.info, dict) - assert drive.info["drive_number"] == i +def test_info(drive): + assert isinstance(drive.info, dict) + assert drive.info["drive_number"] == 0 +@pytest.mark.skip def test_valid(drive): dirname = os.path.join(os.path.dirname(__file__), "test_sample") name = "hysteresis" @@ -146,42 +203,42 @@ def test_valid(drive): assert actual_valid == expected -def test_n_reference_data(sample_drive): - assert sample_drive.n == 25 +def test_n_reference_data(drive): + assert drive.n == 25 -def test_iter_reference_data(sample_drive): - assert len(list(sample_drive)) == 25 +def test_iter_reference_data(drive): + assert len(list(drive)) == 25 -def test_getitem_slice(sample_drive): - assert sample_drive.n == 25 +def test_getitem_slice(drive): + assert drive.n == 25 - sel = sample_drive[:] + sel = drive[:] assert isinstance(sel, mdata.Drive) assert sel.n == 25 assert len(list(sel)) == 25 assert sel.use_cache - sel = sample_drive[:1] + sel = drive[:1] assert isinstance(sel, mdata.Drive) assert sel.n == 1 assert len(list(sel)) == 1 assert sel.use_cache - sel = sample_drive[:-3] + sel = drive[:-3] assert isinstance(sel, mdata.Drive) assert sel.n == 22 assert len(list(sel)) == 22 assert sel.use_cache - sel = sample_drive[4:8] + sel = drive[4:8] assert isinstance(sel, mdata.Drive) assert sel.n == 4 assert len(list(sel)) == 4 assert sel.use_cache - sel = sample_drive[::2] + sel = drive[::2] assert isinstance(sel, mdata.Drive) assert sel.n == 13 assert len(list(sel)) == 13 @@ -199,133 +256,122 @@ def test_to_xarray(drive): ) -def test_hv_time_drive(self): - # time drive +def test_hv_drive_scalar(drive): check_hv( - self.data[0].hv(kdims=["y", "z"], vdims=["y", "z"]), - ["DynamicMap [x,t]", "Image [y,z]", "VectorField [y,z]"], - ) - check_hv( - self.data[0].hv.scalar(kdims=["y", "z"]), + drive.hv.scalar(kdims=["y", "z"]), ["DynamicMap [x,vdims,t]", "Image [y,z]"], ) - with pytest.raises(NotImplementedError): - check_hv(self.data[0].hv.scalar(kdims=["x", "t"]), ...) +def test_hv_drive_vector(drive): + check_hv( + drive.hv.vector(kdims=["x", "y"]), + ["DynamicMap [z,t]", "VectorField [x,y]"], + ) -def test_hv_min_drive(self): - # min drive + +def test_hv_drive_combined(drive): check_hv( - self.data[4] - .register_callback(lambda f: f.sel("z")) - .hv.vector(kdims=["x", "y"]), - ["VectorField [x,y]"], + drive.hv(kdims=["y", "z"], vdims=["y", "z"]), + ["DynamicMap [x,t]", "Image [y,z]", "VectorField [y,z]"], ) -def test_hv_min_drive_steps(self): - # min drive with steps +def test_hv_drive_t_not_as_kdim(drive): + with pytest.raises(NotImplementedError): + check_hv(drive.hv.scalar(kdims=["x", "t"]), ...) + + +def test_hv_drive_single_m(drive): + # create a drive with a single element -> slider for t is omitted + drive = drive[:1] check_hv( - self.data[6].hv.vector(kdims=["x", "y"]), - ["DynamicMap [z,iteration]", "VectorField [x,y]"], + drive.register_callback(lambda f: f.sel("z")).hv.vector(kdims=["x", "y"]), + ["VectorField [x,y]"], ) -def test_lshift(self): - # TimeDriver: 0, 1, 2, 5 - # MinDriver: 4, 6 - # RelaxDriver: 3 - # HysteresisDriver: 7 [CURRENTLY MISSING IN THE DATASET] - for d1, d2 in [(0, 1), (6, 6), (3, 3)]: - combined = self.data[d1] << self.data[d2] - assert isinstance(combined, mdata.CombinedDrive) - assert len(combined.drives) == 2 - assert combined.info["driver"] == self.data[d1].info["driver"] - assert combined.x == self.data[d1].x - assert len(combined.table.data) == combined.n - - for d1, d2 in [(0, 6), (3, 6), (4, 6)]: - # TODO - # (0, 3), (0, 4) should be added and fail - # (4, 6) mixes OOMMF and Mumax3 min drive which does not work because - # they have different independent variables - with pytest.raises(ValueError): - self.data[d1] << self.data[d2] +def test_lshift(drive): + combined = drive << drive + assert isinstance(combined, mdata.CombinedDrive) + assert len(combined.drives) == 2 + assert combined.info["driver"] == drive.info["driver"] + assert combined.x == drive.x + assert len(combined.table.data) == combined.n + with pytest.raises(TypeError): - self.data[0] << 1 + drive << 1 -def test_register_callback(self): - for drive in self.data: - drive_orientation = drive.register_callback(lambda field: field.orientation) - assert isinstance(drive_orientation, drive.__class__) - assert len(drive_orientation._callbacks) == 1 - for field in drive_orientation: - assert np.max(field.array) <= 1.0 - assert np.min(field.array) >= -1.0 +def test_register_callback(drive): + drive_orientation = drive.register_callback(lambda field: field.orientation) + assert isinstance(drive_orientation, drive.__class__) + assert len(drive_orientation._callbacks) == 1 + for field in drive_orientation: + assert np.max(field.array) <= 1.0 + assert np.min(field.array) >= -1.0 + assert field.nvdim == 3 - drive = self.data[0] - processed = drive.register_callback(lambda f: f.orientation) - processed = processed.register_callback(lambda f: f.x) + processed = drive_orientation.register_callback(lambda f: f.x) for field in processed: assert field.nvdim == 1 assert np.max(field.array) <= 1.0 assert np.min(field.array) >= -1.0 + assert field.nvdim == 1 assert len(processed.callbacks) == 2 -def test_cache(sample_drive, monkeypatch): - drive = mdata.Drive( - sample_drive.name, - sample_drive.number, - sample_drive.dirname, - sample_drive.x, +def test_cache(drive, monkeypatch): + generated_drive = mdata.Drive( + drive.name, + drive.number, + drive.dirname, + drive.x, use_cache=True, ) - assert len(list(drive)) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) + assert len(list(generated_drive)) == 25 + assert isinstance(generated_drive[0], df.Field) + assert isinstance(generated_drive.table, ut.Table) with monkeypatch.context() as m: - m.setattr(drive.__class__, "_step_file_glob", ["a.omf", "b.omf"]) - m.setattr(drive.__class__, "_table_path", "wrong_path") + m.setattr(generated_drive.__class__, "_step_file_glob", ["a.omf", "b.omf"]) + m.setattr(generated_drive.__class__, "_table_path", "wrong_path") - assert len(drive._step_files) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) + assert len(generated_drive._step_files) == 25 + assert isinstance(generated_drive[0], df.Field) + assert isinstance(generated_drive.table, ut.Table) - drive.use_cache = False + generated_drive.use_cache = False - assert drive._step_files == ["a.omf", "b.omf"] + assert generated_drive._step_files == ["a.omf", "b.omf"] with pytest.raises(FileNotFoundError): - drive[0] + generated_drive[0] with pytest.raises(FileNotFoundError): - drive.table # noqa: B018 + generated_drive.table # noqa: B018 - drive.use_cache = True # check new caching (no old cache) + generated_drive.use_cache = True # check new caching (no old cache) - assert drive._step_files == ["a.omf", "b.omf"] + assert generated_drive._step_files == ["a.omf", "b.omf"] with pytest.raises(FileNotFoundError): - drive[0] + generated_drive[0] with pytest.raises(FileNotFoundError): - drive.table # noqa: B018 + generated_drive.table # noqa: B018 # caching has effects outside monkeypatch context - assert drive._step_files == ["a.omf", "b.omf"] + assert generated_drive._step_files == ["a.omf", "b.omf"] with pytest.raises(FileNotFoundError): - drive[0] + generated_drive[0] # no table object is cached - assert isinstance(drive.table, ut.Table) + assert isinstance(generated_drive.table, ut.Table) - drive.use_cache = False # remove cached monkeypatch - drive.use_cache = True # check new caching (no old cache) + generated_drive.use_cache = False # remove cached monkeypatch + generated_drive.use_cache = True # check new caching (no old cache) - assert len(list(drive)) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) + assert len(list(generated_drive)) == 25 + assert isinstance(generated_drive[0], df.Field) + assert isinstance(generated_drive.table, ut.Table) def test_slider(drive):