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/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 4f0d536..8fbf03d 100644 --- a/micromagneticdata/drive.py +++ b/micromagneticdata/drive.py @@ -1,12 +1,12 @@ import abc import copy +import importlib.metadata 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,34 +76,81 @@ 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, 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. """ - if pathlib.Path(f"{dirname}/{name}/drive-{number}/{name}.out").exists(): - return super().__new__(md.Mumax3Drive) + 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.loads(f.read_text()) + 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" else: - return super().__new__(md.OOMMFDrive) + raise RuntimeError( + "No 'adapter' has been passed and the adapter could not be determined" + " automatically because no 'info.json' was found." + ) + + drive_entry_points = importlib.metadata.entry_points( + group="micromagneticdata.plugins.CalculatorDrive" + ) + + 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 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}") - 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 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.""" @@ -117,6 +164,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. @@ -153,12 +209,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 + + 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: + 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 @@ -362,7 +424,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/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}')" - ) 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..3c20800 --- /dev/null +++ b/micromagneticdata/testing/drive.py @@ -0,0 +1,48 @@ +"""Tests for plugins""" + +import discretisedfield as df +import pytest +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 + + with pytest.raises(ValueError): + drive.x = "not_a_valid_column_name" + + +def test_set_x(drive, drive_x, new_drive_x): + assert drive.x == drive_x + drive.x = new_drive_x + + assert drive.x == new_drive_x + + +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..0bb6c58 100644 --- a/micromagneticdata/tests/test_drive.py +++ b/micromagneticdata/tests/test_drive.py @@ -1,292 +1,378 @@ +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 md - - -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) - - def test_init(self): - # str for dirname - drive = md.Drive(name=self.name, number=0, dirname=self.dirname) - assert isinstance(drive, md.Drive) - - # 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) - - 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"] - - 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 +import micromagneticdata as mdata +from micromagneticdata.testing.drive import * # noqa: F403 + + +@pytest.fixture +def drive(tmp_path, monkeypatch): + """Fixture that returns a single drive. + + Parametrize the fixture to test different types of drives. + """ + # 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) + + 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 drive_x(): + """Independent variable of the drive.""" + return "t" + + +@pytest.fixture +def new_drive_x(): + """An other column in drive.table, that can be used as indendent variable.""" + return "mx" + + +@pytest.fixture +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 - 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]"], + @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(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", + ), + ] ) - check_hv( - self.data[0].hv.scalar(kdims=["y", "z"]), - ["DynamicMap [x,vdims,t]", "Image [y,z]"], + 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") + + +##################### + +# TODO test plugin registration + + +def test_init(drive): + # str for dirname + 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=drive.name, number=0, dirname=Path(drive.dirname)) + assert isinstance(drive2, mdata.Drive) + assert created_drive.name == drive2.name + assert created_drive.number == drive2.number + assert created_drive.dirname == drive2.dirname + + # Exception + with pytest.raises(OSError): + created_drive = mdata.Drive(name=drive.name, number=1, dirname=drive.dirname) + + +def test_repr(drive): + assert isinstance(repr(drive), str) + assert "Drive" in repr(drive) + + +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" + 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 - with pytest.raises(NotImplementedError): - check_hv(self.data[0].hv.scalar(kdims=["x", "t"]), ...) - # min drive - check_hv( - self.data[4] - .register_callback(lambda f: f.sel("z")) - .hv.vector(kdims=["x", "y"]), - ["VectorField [x,y]"], - ) +def test_n_reference_data(drive): + assert drive.n == 25 + + +def test_iter_reference_data(drive): + assert len(list(drive)) == 25 + + +def test_getitem_slice(drive): + assert drive.n == 25 + + sel = drive[:] + assert isinstance(sel, mdata.Drive) + assert sel.n == 25 + assert len(list(sel)) == 25 + assert sel.use_cache + + sel = drive[:1] + assert isinstance(sel, mdata.Drive) + assert sel.n == 1 + assert len(list(sel)) == 1 + assert sel.use_cache + + sel = drive[:-3] + assert isinstance(sel, mdata.Drive) + assert sel.n == 22 + assert len(list(sel)) == 22 + assert sel.use_cache - # min drive with steps - check_hv( - self.data[6].hv.vector(kdims=["x", "y"]), - ["DynamicMap [z,iteration]", "VectorField [x,y]"], + sel = drive[4:8] + assert isinstance(sel, mdata.Drive) + assert sel.n == 4 + assert len(list(sel)) == 4 + assert sel.use_cache + + sel = 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 - 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) - - assert len(list(drive)) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(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") - - assert len(drive._step_files) == 25 - assert isinstance(drive[0], df.Field) - assert isinstance(drive.table, ut.Table) - - drive.use_cache = False - - 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 = True # check new caching (no old cache) - - 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"] + +def test_hv_drive_scalar(drive): + check_hv( + drive.hv.scalar(kdims=["y", "z"]), + ["DynamicMap [x,vdims,t]", "Image [y,z]"], + ) + + +def test_hv_drive_vector(drive): + check_hv( + drive.hv.vector(kdims=["x", "y"]), + ["DynamicMap [z,t]", "VectorField [x,y]"], + ) + + +def test_hv_drive_combined(drive): + check_hv( + drive.hv(kdims=["y", "z"], vdims=["y", "z"]), + ["DynamicMap [x,t]", "Image [y,z]", "VectorField [y,z]"], + ) + + +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( + drive.register_callback(lambda f: f.sel("z")).hv.vector(kdims=["x", "y"]), + ["VectorField [x,y]"], + ) + + +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): + drive << 1 + + +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 + + 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(drive, monkeypatch): + generated_drive = mdata.Drive( + drive.name, + drive.number, + drive.dirname, + drive.x, + use_cache=True, + ) + + 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(generated_drive.__class__, "_step_file_glob", ["a.omf", "b.omf"]) + m.setattr(generated_drive.__class__, "_table_path", "wrong_path") + + assert len(generated_drive._step_files) == 25 + assert isinstance(generated_drive[0], df.Field) + assert isinstance(generated_drive.table, ut.Table) + + generated_drive.use_cache = False + + assert generated_drive._step_files == ["a.omf", "b.omf"] with pytest.raises(FileNotFoundError): - drive[0] - # no table object is cached - assert isinstance(drive.table, ut.Table) + generated_drive[0] + with pytest.raises(FileNotFoundError): + generated_drive.table # noqa: B018 + + generated_drive.use_cache = True # check new caching (no old cache) + + assert generated_drive._step_files == ["a.omf", "b.omf"] + with pytest.raises(FileNotFoundError): + generated_drive[0] + with pytest.raises(FileNotFoundError): + generated_drive.table # noqa: B018 + + # caching has effects outside monkeypatch context + assert generated_drive._step_files == ["a.omf", "b.omf"] + with pytest.raises(FileNotFoundError): + generated_drive[0] + # no table object is cached + assert isinstance(generated_drive.table, ut.Table) + + generated_drive.use_cache = False # remove cached monkeypatch + generated_drive.use_cache = True # check new caching (no old cache) + + assert len(list(generated_drive)) == 25 + assert isinstance(generated_drive[0], df.Field) + assert isinstance(generated_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) diff --git a/pyproject.toml b/pyproject.toml index 2531b3c..6a31a23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ classifiers = [ dependencies = [ "discretisedfield>=0.92.0", - "ubermagtable>=0.62.0" + "ubermagtable>=0.62.0", + "importlib_metadata; python_version<'3.10'", ] [project.optional-dependencies]