diff --git a/ubermagtable/table.py b/ubermagtable/table.py index a1995e3..7b8af0c 100644 --- a/ubermagtable/table.py +++ b/ubermagtable/table.py @@ -2,15 +2,9 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd -import ubermagutil.typesystem as ts import ubermagutil.units -import ubermagtable.util as uu - -@ts.typesystem( - data=ts.Typed(expected_type=pd.DataFrame), units=ts.Typed(expected_type=dict) -) class Table: """Tabular data class. @@ -57,59 +51,50 @@ def __init__(self, data, units, x=None, attributes=None): self.x = x self.attributes = attributes if attributes is not None else {} self.attributes.setdefault("fourierspace", False) + # Detect duplicated lines as a last step to make use of the checks when + # assigning 'x' as independent variable. + self._duplicated_lines = any(self.data.duplicated(subset=self.x, keep="last")) + if self._duplicated_lines: + self.data.drop_duplicates( + subset=self.x, + keep="last", + inplace=True, + ignore_index=True, # reset the index to 0, 1, ..., n-1 + ) - @classmethod - def fromfile(cls, filename, /, x=None, rename=True): - """Reads an OOMMF ``.odt`` or mumax3 ``.txt`` scalar data file and - returns a ``ubermagtable.Table`` object. - - Parameters - ---------- - filename : str - - OOMMF ``.odt`` or mumax3 ``.txt`` 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``. + @property + def data(self): + """Scalar data of the drive. Returns ------- - ubermagtable.Table - - Table object. - - Examples - -------- - 1. Defining ``ubermagtable.Table`` by reading an OOMMF ``.odt`` file. + pd.DataFrame + """ + return self._data - >>> 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') + @data.setter + def data(self, data): + if not isinstance(data, pd.DataFrame): + raise TypeError(f"Invalid {type(data)=}; expected 'pandas.DataFrame'.") + self._data = data - 2. Defining ``ubermagtable.Table`` by reading a mumax3 ``.txt`` file. + @property + def units(self): + """Units of the scalar data. - >>> odtfile = os.path.join(os.path.dirname(__file__), - ... 'tests', 'test_sample', 'mumax3-file1.txt') - >>> table = ut.Table.fromfile(odtfile, x='t') + Returns + ------- + dict + Keys are the columns in the ``data`` property, values the respective units. """ - cols = uu.columns(filename, rename=rename) + return self._units - return cls( - data=pd.DataFrame(uu.data(filename), columns=cols), - units=uu.units(filename, rename=rename), - x=x, - ) + @units.setter + def units(self, units): + if not isinstance(units, dict): + raise TypeError(f"Invalid {type(units)=}; 'expected dict'.") + self._units = units @property def x(self): @@ -199,6 +184,11 @@ def xmax(self): """ return self.data[self.x].iloc[-1] + @property + def deduplicated(self): + """Indicate if the table on disk contains duplicated steps.""" + return self._duplicated_lines + def apply(self, func, columns=None, args=(), **kwargs): r"""Apply function. diff --git a/ubermagtable/tests/test_table.py b/ubermagtable/tests/test_table.py index d1d3e69..5bae337 100644 --- a/ubermagtable/tests/test_table.py +++ b/ubermagtable/tests/test_table.py @@ -68,10 +68,34 @@ def test_init(self): assert isinstance(table.data, pd.DataFrame) def test_fromfile(self): + for odtfile in self.odtfiles: + table = ut.Table.fromfile(odtfile, rename=False) + check_table(table) + + table_short_names = ut.Table.fromfile(odtfile, rename=True) + check_table(table_short_names) + + assert len(table.data) == len(table_short_names.data) + assert len(table.data.columns) == len(table_short_names.data.columns) + + def test_columns(self): for odtfile in self.odtfiles: for rename in [True, False]: - table = ut.Table.fromfile(odtfile) - check_table(table) + table = ut.Table.fromfile(odtfile, rename=rename) + columns = table.data.columns + assert all(isinstance(column, str) for column in columns) + assert len(columns) == len(set(columns)) # unique column names + + def test_units(self): + for odtfile in self.odtfiles: + for rename in [True, False]: + table = ut.Table.fromfile(odtfile, rename=rename) + units = table.units + assert isinstance(units, dict) + assert all(isinstance(unit, str) for unit in units.keys()) + assert all(isinstance(unit, str) for unit in units.values()) + assert "J" in units.values() # Energy is always in + assert "" in units.values() # Columns with no units are always in def test_xy(self): table = ut.Table.fromfile(self.odtfiles[0], x="t") @@ -178,6 +202,11 @@ def test_oommf_mel(self): assert len(columns) == 16 def test_oommf_issue1(self): + """The odt file contains columns ``Oxs_Exchange6Ngbr:...`` and + ``My_Exchange6Ngbr:...``. During processing we remove the ``Oxs_`` or ``My_`` + prefix and subsequently loose the "duplicated" columns in the pandas dataframe. + + """ table = ut.Table.fromfile(self.odtfiles[-1]) columns = table.data.columns.to_list() assert len(columns) == 30 diff --git a/ubermagtable/tests/test_util.py b/ubermagtable/tests/test_util.py deleted file mode 100644 index 0d45ebe..0000000 --- a/ubermagtable/tests/test_util.py +++ /dev/null @@ -1,55 +0,0 @@ -import itertools -import numbers -import os - -import ubermagtable.util as uu - -dirname = os.path.join(os.path.dirname(__file__), "test_sample/") -filenames = [ - "oommf-old-file1.odt", - "oommf-old-file2.odt", - "oommf-old-file3.odt", - "oommf-old-file4.odt", - "oommf-old-file5.odt", - "oommf-old-file6.odt", - "oommf-old-file7.odt", - "oommf-old-file8.odt", - "oommf-new-file1.odt", - "oommf-new-file2.odt", - "oommf-new-file3.odt", - "oommf-new-file4.odt", - "oommf-new-file5.odt", - "mumax3-file1.txt", - "oommf-mel-file.odt", -] -odtfiles = [os.path.join(dirname, f) for f in filenames] - - -def test_columns(): - for odtfile in odtfiles: - for rename in [True, False]: - columns = uu.columns(odtfile, rename=rename) - - assert isinstance(columns, list) - assert all(isinstance(column, str) for column in columns) - assert len(columns) == len(set(columns)) # unique column names - - -def test_units(): - for odtfile in odtfiles: - for rename in [True, False]: - units = uu.units(odtfile, rename=rename) - - assert isinstance(units, dict) - assert all(isinstance(unit, str) for unit in units.keys()) - assert all(isinstance(unit, str) for unit in units.values()) - assert "J" in units.values() # Energy is always in - assert "" in units.values() # Columns with no units are always in - - -def test_data(): - for odtfile in odtfiles: - data = uu.data(odtfile) - - assert isinstance(data, list) - assert all(isinstance(i, numbers.Real) for i in itertools.chain(*data)) diff --git a/ubermagtable/util/__init__.py b/ubermagtable/util/__init__.py index 79f451e..e69de29 100644 --- a/ubermagtable/util/__init__.py +++ b/ubermagtable/util/__init__.py @@ -1,2 +0,0 @@ -"""Utility tools""" -from .util import columns, data, units diff --git a/ubermagtable/util/util.py b/ubermagtable/util/util.py deleted file mode 100644 index 4a9cff4..0000000 --- a/ubermagtable/util/util.py +++ /dev/null @@ -1,317 +0,0 @@ -import re - -# The OOMMF columns are renamed according to this dictionary. -oommf_dict = { - "RungeKuttaEvolve:evolver:Total energy": "E", - "RungeKuttaEvolve:evolver:Energy calc count": "E_calc_count", - "RungeKuttaEvolve:evolver:Max dm/dt": "max_dm/dt", - "RungeKuttaEvolve:evolver:dE/dt": "dE/dt", - "RungeKuttaEvolve:evolver:Delta E": "delta_E", - "RungeKuttaEvolve::Total energy": "E", - "RungeKuttaEvolve::Energy calc count": "E_calc_count", - "RungeKuttaEvolve::Max dm/dt": "max_dm/dt", - "RungeKuttaEvolve::dE/dt": "dE/dt", - "RungeKuttaEvolve::Delta E": "delta_E", - "EulerEvolve:evolver:Total energy": "E", - "EulerEvolve:evolver:Energy calc count": "E_calc_count", - "EulerEvolve:evolver:Max dm/dt": "max_dmdt", - "EulerEvolve:evolver:dE/dt": "dE/dt", - "EulerEvolve:evolver:Delta E": "delta_E", - "TimeDriver::Iteration": "iteration", - "TimeDriver::Stage iteration": "stage_iteration", - "TimeDriver::Stage": "stage", - "TimeDriver::mx": "mx", - "TimeDriver::my": "my", - "TimeDriver::mz": "mz", - "TimeDriver::Last time step": "last_time_step", - "TimeDriver::Simulation time": "t", - "CGEvolve:evolver:Max mxHxm": "max_mxHxm", - "CGEvolve:evolver:Total energy": "E", - "CGEvolve:evolver:Delta E": "delta_E", - "CGEvolve:evolver:Bracket count": "bracket_count", - "CGEvolve:evolver:Line min count": "line_min_count", - "CGEvolve:evolver:Conjugate cycle count": "conjugate_cycle_count", - "CGEvolve:evolver:Cycle count": "cycle_count", - "CGEvolve:evolver:Cycle sub count": "cycle_sub_count", - "CGEvolve:evolver:Energy calc count": "energy_calc_count", - "CGEvolve::Max mxHxm": "max_mxHxm", - "CGEvolve::Total energy": "E", - "CGEvolve::Delta E": "delta_E", - "CGEvolve::Bracket count": "bracket_count", - "CGEvolve::Line min count": "line_min_count", - "CGEvolve::Conjugate cycle count": "conjugate_cycle_count", - "CGEvolve::Cycle count": "cycle_count", - "CGEvolve::Cycle sub count": "cycle_sub_count", - "CGEvolve::Energy calc count": "energy_calc_count", - "FixedMEL::Energy": "MEL_E", - "FixedMEL:magnetoelastic:Energy": "MEL_E", - "SpinTEvolve:evolver:Total energy": "E", - "SpinTEvolve:evolver:Energy calc count": "E_calc_count", - "SpinTEvolve:evolver:Max dm/dt": "max_dmdt", - "SpinTEvolve:evolver:dE/dt": "dE/dt", - "SpinTEvolve:evolver:Delta E": "delta_E", - "SpinTEvolve:evolver:average u": "average_u", - "SpinXferEvolve:evolver:Total energy": "E", - "SpinXferEvolve:evolver:Energy calc count": "E_calc_count", - "SpinXferEvolve:evolver:Max dm/dt": "max_dmdt", - "SpinXferEvolve:evolver:dE/dt": "dE/dt", - "SpinXferEvolve:evolver:Delta E": "delta_E", - "SpinXferEvolve:evolver:average u": "average_u", - "SpinXferEvolve:evolver:average J": "average_J", - "ThetaEvolve:evolver:Total energy": "E", - "ThetaEvolve:evolver:Energy calc count": "E_calc_count", - "ThetaEvolve:evolver:Max dm/dt": "max_dmdt", - "ThetaEvolve:evolver:dE/dt": "dE/dt", - "ThetaEvolve:evolver:Delta E": "delta_E", - "ThetaEvolve:evolver:Temperature": "T", - "ThermHeunEvolve:evolver:Total energy": "E", - "ThermHeunEvolve:evolver:Energy calc count": "E_calc_count", - "ThermHeunEvolve:evolver:Max dm/dt": "max_dmdt", - "ThermHeunEvolve:evolver:dE/dt": "dE/dt", - "ThermHeunEvolve:evolver:Delta E": "delta_E", - "ThermHeunEvolve:evolver:Temperature": "T", - "ThermSpinXferEvolve:evolver:Total energy": "E", - "ThermSpinXferEvolve:evolver:Energy calc count": "E_calc_count", - "ThermSpinXferEvolve:evolver:Max dm/dt": "max_dmdt", - "ThermSpinXferEvolve:evolver:dE/dt": "dE/dt", - "ThermSpinXferEvolve:evolver:Delta E": "delta_E", - "ThermSpinXferEvolve:evolver:Temperature": "T", - "MinDriver::Iteration": "iteration", - "MinDriver::Stage iteration": "stage_iteration", - "MinDriver::Stage": "stage", - "MinDriver::mx": "mx", - "MinDriver::my": "my", - "MinDriver::mz": "mz", - "UniformExchange::Max Spin Ang": "max_spin_ang", - "UniformExchange::Stage Max Spin Ang": "stage_max_spin_ang", - "UniformExchange::Run Max Spin Ang": "run_max_spin_ang", - "UniformExchange::Energy": "E_exchange", - "DMExchange6Ngbr::Energy": "E", - "DMI_Cnv::Energy": "E", - "DMI_T::Energy": "E", - "DMI_D2d::Energy": "E", - "Demag::Energy": "E", - "FixedZeeman::Energy": "E_zeeman", - "UZeeman::Energy": "E_zeeman", - "UZeeman::B": "B", - "UZeeman::Bx": "Bx", - "UZeeman::By": "By", - "UZeeman::Bz": "Bz", - "ScriptUZeeman::Energy": "E", - "ScriptUZeeman::B": "B", - "ScriptUZeeman::Bx": "Bx", - "ScriptUZeeman::By": "By", - "ScriptUZeeman::Bz": "Bz", - "TransformZeeman::Energy": "E", - "CubicAnisotropy::Energy": "E", - "UniaxialAnisotropy::Energy": "E", - "UniaxialAnisotropy4::Energy": "E", - "Southampton_UniaxialAnisotropy4::Energy": "E", - "Exchange6Ngbr::Energy": "E", - "Exchange6Ngbr::Max Spin Ang": "max_spin_ang", - "Exchange6Ngbr::Stage Max Spin Ang": "stage_max_spin_ang", - "Exchange6Ngbr::Run Max Spin Ang": "run_max_spin_ang", - "ExchangePtwise::Energy": "E", - "ExchangePtwise::Max Spin Ang": "max_spin_ang", - "ExchangePtwise::Stage Max Spin Ang": "stage_max_spin_ang", - "ExchangePtwise::Run Max Spin Ang": "run_max_spin_ang", - "TwoSurfaceExchange::Energy": "E", -} - -# The mumax3 columns are renamed according to this dictionary. -mumax3_dict = { - "t": "t", - "mx": "mx", - "my": "my", - "mz": "mz", - "E_total": "E", - "E_exch": "E_totalexchange", - "E_demag": "E_demag", - "E_Zeeman": "E_zeeman", - "E_anis": "E_totalanisotropy", - "dt": "dt", - "maxTorque": "maxtorque", -} - - -def rename_column(name, cols_dict): - if name in cols_dict.keys(): - return cols_dict[name] - - for key in cols_dict.keys(): - if len(key.split("::")) == 2: - start, end = key.split("::") - name_split = name.split(":") - if name_split[0] == start and name_split[-1] == end: - term_name = name.split(":")[1] - type_name = cols_dict[key] - # required for E_exchange in old and new OOMMF odt files - if not type_name.endswith(term_name): - type_name = f"{type_name}_{term_name}" - return type_name - else: - return name # name cannot be found in dictionary - - -def columns(filename, rename=True): - """Extracts column names from a table file. - - Parameters - ---------- - filename : str - - OOMMF ``.odt`` or mumax3 ``.txt`` file. - - rename : bool - - If ``rename=True``, the column names are renamed with their shorter - versions. Defaults to ``True``. - - Returns - ------- - list - - List of column names. - - Examples - -------- - 1. Extracting the column names from an OOMMF `.odt` file. - - >>> import os - >>> import ubermagtable.util as uu - ... - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'oommf-new-file1.odt') - >>> uu.columns(odtfile) - [...] - - 2. Extracting the names of columns from a mumax3 `.txt` file. - - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'mumax3-file1.txt') - >>> uu.columns(odtfile) - [...] - - """ - with open(filename) as f: - lines = f.readlines() - - if lines[0].startswith("# ODT"): # OOMMF odt file - cline = list(filter(lambda line: line.startswith("# Columns:"), lines))[0] - cline = re.split(r"Oxs_|Anv_|Southampton_|My_|YY_|UHH_|Xf_", cline)[1:] - cline = list(map(lambda col: re.sub(r"[{}]", "", col), cline)) - cols = list(map(lambda s: s.strip(), cline)) - cols_dict = oommf_dict - else: # mumax3 txt file - cline = lines[0][2:].rstrip().split("\t") - cols = list(map(lambda s: s.split(" ")[0], cline)) - cols_dict = mumax3_dict - - if rename: - return [rename_column(col, cols_dict) for col in cols] - else: - return cols - - -def units(filename, rename=True): - """Extracts units 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`` or mumax3 ``.txt`` 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. - - Examples - -------- - 1. Extracting units for individual columns from an OOMMF ``.odt`` file. - - >>> import os - >>> import ubermagtable.util as uu - ... - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'oommf-new-file2.odt') - >>> uu.units(odtfile) - {...} - - 2. Extracting units for individual columns from a mumax3 ``.txt`` file. - - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'mumax3-file1.txt') - >>> uu.units(odtfile) - {...} - - """ - with open(filename) as f: - lines = f.readlines() - - if lines[0].startswith("# ODT"): # OOMMF odt file - uline = list(filter(lambda line: line.startswith("# Units:"), lines))[0] - units = uline.split()[2:] - units = list(map(lambda s: re.sub(r"[{}]", "", s), units)) - else: # mumax3 txt file - uline = lines[0][2:].rstrip().split("\t") - units = list(map(lambda s: s.split()[1], uline)) - units = list(map(lambda s: re.sub(r"[()]", "", s), units)) - - return dict(zip(columns(filename, rename=rename), units)) - - -def data(filename): - """Extracts numerical data from a table file. - - Parameters - ---------- - filename : str - - OOMMF ``.odt`` or mumax3 ``.txt`` file. - - Returns - ------- - list - - List of numerical data. - - Examples - -------- - 1. Reading data from an OOMMF ``.odt`` file. - - >>> import os - >>> import ubermagtable.util as uu - ... - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'oommf-new-file3.odt') - >>> uu.data(odtfile) - [...] - - 2. Reading data from a mumax3 ``.txt`` file. - - >>> odtfile = os.path.join(os.path.dirname(__file__), '..', - ... 'tests', 'test_sample', 'mumax3-file1.txt') - >>> uu.data(odtfile) - [...] - - """ - with open(filename) as f: - lines = f.readlines() - - values = [] - for line in lines: - if not line.startswith("#"): - values.append(list(map(float, line.split()))) - - return values