From 8babdaa7a953e7eddfd5631606fef0b14980f11a Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Wed, 29 Nov 2023 16:13:43 +0100 Subject: [PATCH 1/3] Add functionality for finding and reading data moved from micromagneticdata and ubermagtable, respectively --- oommfc/_output_collecting_util/__init__.py | 2 + oommfc/_output_collecting_util/oommfdrive.py | 119 +++++++++ oommfc/_output_collecting_util/read_table.py | 254 +++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 oommfc/_output_collecting_util/__init__.py create mode 100644 oommfc/_output_collecting_util/oommfdrive.py create mode 100644 oommfc/_output_collecting_util/read_table.py diff --git a/oommfc/_output_collecting_util/__init__.py b/oommfc/_output_collecting_util/__init__.py new file mode 100644 index 0000000..11b12cc --- /dev/null +++ b/oommfc/_output_collecting_util/__init__.py @@ -0,0 +1,2 @@ +from .oommfdrive import OOMMFDrive as Drive +from .read_table import table_from_file diff --git a/oommfc/_output_collecting_util/oommfdrive.py b/oommfc/_output_collecting_util/oommfdrive.py new file mode 100644 index 0000000..3b3f04a --- /dev/null +++ b/oommfc/_output_collecting_util/oommfdrive.py @@ -0,0 +1,119 @@ +import micromagneticdata as mdata +import ubermagutil as uu + + +@uu.inherit_docs +class OOMMFDrive(mdata.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 = dirname=os.path.join(os.path.dirname(__file__), + ... 'tests', 'test_sample') + >>> drive = md.Drive(name='system_name', 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) + + @mdata.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 = dirname=os.path.join(os.path.dirname(__file__), + ... 'tests', 'test_sample') + >>> drive = md.Drive(name='system_name', number=0, dirname=dirname) + >>> drive + OOMMFDrive(name='system_name', 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/oommfc/_output_collecting_util/read_table.py b/oommfc/_output_collecting_util/read_table.py new file mode 100644 index 0000000..2489b94 --- /dev/null +++ b/oommfc/_output_collecting_util/read_table.py @@ -0,0 +1,254 @@ +import re + +import pandas as pd +import ubermagtable + + +def table_from_file(filename, /, x=None, rename=True): + """Convert an OOMMF ``.odt`` scalar data file into a ``ubermagtable.Table``. + + Parameters + ---------- + filename : str + + OOMMF ``.odt`` file. + + x : str, optional + + Independent variable name. Defaults to ``None``. + + rename : bool, optional + + If ``rename=True``, the column names are renamed with their shorter + versions. Defaults to ``True``. + + Returns + ------- + ubermagtable.Table + + Table object. + + TODO: update example + Examples + -------- + 1. Defining ``ubermagtable.Table`` by reading an OOMMF ``.odt`` file. + + >>> import os + >>> import ubermagtable as ut + ... + >>> odtfile = os.path.join(os.path.dirname(__file__), + ... 'tests', 'test_sample', + ... 'oommf-hysteresis1.odt') + >>> table = ut.Table.fromfile(odtfile, x='B_hysteresis') + + 2. Defining ``ubermagtable.Table`` by reading a mumax3 ``.txt`` file. + + >>> odtfile = os.path.join(os.path.dirname(__file__), + ... 'tests', 'test_sample', 'mumax3-file1.txt') + >>> table = ut.Table.fromfile(odtfile, x='t') + + """ + quantities = _read_header(filename, rename=rename) + data = pd.read_csv( + filename, + sep=r"\s+", + comment="#", + header=None, + names=list(quantities.keys()), + ) + return ubermagtable.Table(data=data, units=quantities, x=x) + + +def _read_header(filename, rename=True): + """Extract quantities for individual columns from a table file. + + This method extracts both column names and units and returns a dictionary, + where keys are column names and values are the units. + + Parameters + ---------- + filename : str + + OOMMF ``.odt`` file. + + rename : bool + + If ``rename=True``, the column names are renamed with their shorter + versions. Defaults to ``True``. + + Returns + ------- + dict + + Dictionary of column names and units. + """ + with open(filename) as f: + # COLUMN NAMES + while not (cline := f.readline()).startswith("# Columns"): + pass + columns = cline.lstrip("# Columns:").rstrip() + cols = re.findall(r"(?<={)[^}]+|[^ {}]+", columns) + # UNITS + uline = f.readline() + assert uline.startswith("# Units:") + units = uline.split()[2:] # [2:] to remove ["#", "Units:"] + units = [re.sub(r"[{}]", "", unit) for unit in units] + + if rename: + cols = [_rename_column(col, _OOMMF_DICT) for col in cols] + + return dict(zip(cols, units)) + + +def _rename_column(name, cols_dict): + """Rename columns to get shorter names without spaces. + + Renaming is based on _OOMMF_DICT. + """ + name_split = name.split(":") + try: + group = cols_dict[name_split[0]] + attribute = group[name_split[-1]] + term_name = name_split[1] + if not attribute.endswith(term_name): + # - unique names if the same quantity is present multiple times + # e.g. multiple Zeeman fields + # - also required for changes in the exchange field in "old" and "new" + # OOMMF odt files + attribute = f"{attribute}_{term_name}" + return attribute + except KeyError: + return name + + +# The OOMMF columns are renamed according to this dictionary. +_OOMMF_DICT = { + "Oxs_RungeKuttaEvolve": { + "Total energy": "E", + "Energy calc count": "E_calc_count", + "Max dm/dt": "max_dm/dt", + "dE/dt": "dE/dt", + "Delta E": "delta_E", + }, + "Oxs_EulerEvolve": { + "Total energy": "E", + "Energy calc count": "E_calc_count", + "Max dm/dt": "max_dmdt", + "dE/dt": "dE/dt", + "Delta E": "delta_E", + }, + "Oxs_CGEvolve": { + "Max mxHxm": "max_mxHxm", + "Total energy": "E", + "Delta E": "delta_E", + "Bracket count": "bracket_count", + "Line min count": "line_min_count", + "Conjugate cycle count": "conjugate_cycle_count", + "Cycle count": "cycle_count", + "Cycle sub count": "cycle_sub_count", + "Energy calc count": "energy_calc_count", + }, + "Anv_SpinTEvolve": { + "Total energy": "E", + "Energy calc count": "E_calc_count", + "Max dm/dt": "max_dmdt", + "dE/dt": "dE/dt", + "Delta E": "delta_E", + "average u": "average_u", + }, + "Oxs_SpinXferEvolve": { + "Total energy": "E", # NO SAMPLE + "Energy calc count": "E_calc_count", # NO SAMPLE + "Max dm/dt": "max_dmdt", # NO SAMPLE + "dE/dt": "dE/dt", # NO SAMPLE + "Delta E": "delta_E", # NO SAMPLE + "average u": "average_u", # NO SAMPLE + "average J": "average_J", # NO SAMPLE + }, + "UHH_ThetaEvolve": { + "Total energy": "E", # NO SAMPLE + "Energy calc count": "E_calc_count", # NO SAMPLE + "Max dm/dt": "max_dmdt", # NO SAMPLE + "dE/dt": "dE/dt", # NO SAMPLE + "Delta E": "delta_E", # NO SAMPLE + "Temperature": "T", # NO SAMPLE + }, + "Xf_ThermHeunEvolve": { + "Total energy": "E", # NO SAMPLE + "Energy calc count": "E_calc_count", # NO SAMPLE + "Max dm/dt": "max_dmdt", # NO SAMPLE + "dE/dt": "dE/dt", # NO SAMPLE + "Delta E": "delta_E", # NO SAMPLE + "Temperature": "T", # NO SAMPLE + }, + "Xf_ThermSpinXferEvolve": { + "Total energy": "E", # NO SAMPLE + "Energy calc count": "E_calc_count", # NO SAMPLE + "Max dm/dt": "max_dmdt", # NO SAMPLE + "dE/dt": "dE/dt", # NO SAMPLE + "Delta E": "delta_E", # NO SAMPLE + "Temperature": "T", # NO SAMPLE + }, + "Oxs_MinDriver": { + "Iteration": "iteration", + "Stage iteration": "stage_iteration", + "Stage": "stage", + "mx": "mx", + "my": "my", + "mz": "mz", + }, + "Oxs_TimeDriver": { + "Iteration": "iteration", + "Stage iteration": "stage_iteration", + "Stage": "stage", + "mx": "mx", + "my": "my", + "mz": "mz", + "Last time step": "last_time_step", + "Simulation time": "t", + }, + "Oxs_UniformExchange": { + "Max Spin Ang": "max_spin_ang", + "Stage Max Spin Ang": "stage_max_spin_ang", + "Run Max Spin Ang": "run_max_spin_ang", + "Energy": "E_exchange", + }, + "Oxs_Exchange6Ngbr": { + "Energy": "E_exchange6ngbr", + "Max Spin Ang": "max_spin_ang", + "Stage Max Spin Ang": "stage_max_spin_ang", + "Run Max Spin Ang": "run_max_spin_ang", + }, + "Oxs_ExchangePtwise": { + "Energy": "E_exchange_ptwise", # NO SAMPLE + "Max Spin Ang": "max_spin_ang", # NO SAMPLE + "Stage Max Spin Ang": "stage_max_spin_ang", # NO SAMPLE + "Run Max Spin Ang": "run_max_spin_ang", # NO SAMPLE + }, + "Oxs_TwoSurfaceExchange": {"Energy": "E_two_surface_exchange"}, # NO SAMPLE + "Oxs_Demag": {"Energy": "E_demag"}, + "Oxs_DMExchange6Ngbr": {"Energy": "E_DM_exchange6ngbr"}, # NO SAMPLE + "Oxs_DMI_Cnv": {"Energy": "E_DMI_Cnv"}, # TODO: PREFIX + "Oxs_DMI_T": {"Energy": "E_DMI_T"}, # NO SAMPLE, TODO: PREFIX + "Oxs_DMI_D2d": {"Energy": "E_DMI_Dd"}, # NO SAMPLE, TODO: PREFIX + "Oxs_FixedZeeman": {"Energy": "E_zeeman"}, + "Oxs_UZeeman": { + "Energy": "E_zeeman", + "B": "B", + "Bx": "Bx", + "By": "By", + "Bz": "Bz", + }, + "Oxs_ScriptUZeeman": { + "Energy": "E_zeeman", # NO SAMPLE + "B": "B", # NO SAMPLE + "Bx": "Bx", # NO SAMPLE + "By": "By", # NO SAMPLE + "Bz": "Bz", # NO SAMPLE + }, + "Oxs_TransformZeeman": {"Energy": "E_zeeman"}, # NO SAMPLE + "Oxs_CubicAnisotropy": {"Energy": "E_zeeman"}, + "Oxs_UniaxialAnisotropy": {"Energy": "E_zeeman"}, + "Southampton_UniaxialAnisotropy4": {"Energy": "E_zeeman"}, # NO SAMPLE + "YY_FixedMEL": {"Energy": "MEL_E"}, +} From 348883f19a5ef23f3a1f099f328af8eec5e514c9 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Sat, 23 Mar 2024 14:53:32 +0100 Subject: [PATCH 2/3] Implement micromagneticdata.output_collecting entry_points --- oommfc/_output_collecting_util/__init__.py | 2 -- oommfc/_output_collecting_util/oommfdrive.py | 2 +- pyproject.toml | 5 ++++- 3 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 oommfc/_output_collecting_util/__init__.py diff --git a/oommfc/_output_collecting_util/__init__.py b/oommfc/_output_collecting_util/__init__.py deleted file mode 100644 index 11b12cc..0000000 --- a/oommfc/_output_collecting_util/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .oommfdrive import OOMMFDrive as Drive -from .read_table import table_from_file diff --git a/oommfc/_output_collecting_util/oommfdrive.py b/oommfc/_output_collecting_util/oommfdrive.py index 3b3f04a..ee988b6 100644 --- a/oommfc/_output_collecting_util/oommfdrive.py +++ b/oommfc/_output_collecting_util/oommfdrive.py @@ -4,7 +4,7 @@ @uu.inherit_docs class OOMMFDrive(mdata.Drive): - """Drive class for OOMMFDrives (created automatically). + """Drive class for OOMMFDrives. This class provides utility for the analysis of individual OOMMF drives. It should not be created explicitly. Instead, use ``micromagneticdata.Drive`` which diff --git a/pyproject.toml b/pyproject.toml index ab525f0..6a411f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,10 @@ homepage = "https://ubermag.github.io" documentation = "https://ubermag.github.io/documentation/oommfc" repository = "https://github.com/ubermag/oommfc" - +[project.entry-points."micromagneticdata.output_collecting.Drive"] +oommfc = "oommfc._output_collecting_util.oommfdrive:OOMMFDrive" +[project.entry-points."micromagneticdata.output_collecting.read_table"] +oommfc = "oommfc._output_collecting_util.read_table:table_from_file" [tool.black] experimental-string-processing = true From 713ed3f4ed87800294fd0d6b56a1c43532d5bef5 Mon Sep 17 00:00:00 2001 From: Martin Lang Date: Thu, 9 May 2024 11:34:08 +0200 Subject: [PATCH 3/3] Explain regex for reading columns --- oommfc/_output_collecting_util/read_table.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/oommfc/_output_collecting_util/read_table.py b/oommfc/_output_collecting_util/read_table.py index 2489b94..5515ff3 100644 --- a/oommfc/_output_collecting_util/read_table.py +++ b/oommfc/_output_collecting_util/read_table.py @@ -87,6 +87,10 @@ def _read_header(filename, rename=True): while not (cline := f.readline()).startswith("# Columns"): pass columns = cline.lstrip("# Columns:").rstrip() + # Columns can e.g. look like: + # {Oxs_CGEvolve:evolver:Max mxHxm} {...} Oxs_MinDriver::Stage Oxs_MinDriver::mx + # - the first part of the regex finds column names with spaces inside {} + # - the second part finds column names without spaces and without {} cols = re.findall(r"(?<={)[^}]+|[^ {}]+", columns) # UNITS uline = f.readline()