diff --git a/CHANGELOG.md b/CHANGELOG.md index ee44bbfbf2..168588f154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ we hit release version 1.0.0. ## [0.14.0] - YYYY-MM-DD ### Added +- added SISL_UNIT_SIESTA to select between legacy or codata2018 units (since Siesta 5) + New default is codata2018, may create inconsistencies until Siesta 5 is widely adopted. - added --remove to sgeom for removing single atoms - added a EllipticalCylinder as a new shape - added basis-enthalpy to the stdoutSiestaSile.read_energy routine diff --git a/docs/environment.rst b/docs/environment.rst new file mode 100644 index 0000000000..d95f442e03 --- /dev/null +++ b/docs/environment.rst @@ -0,0 +1,65 @@ +.. _environment: + +Environment variables +===================== + +sisl understands some environment variables that may be used to tweak, or change +the default behaviour of sisl. + +Here we list the different options: + + +``SISL_NUM_PROCS = 1`` + Default the number of processors used in parallel calculations. + Currently only certain Brillouin zone operations has this enabled. + + Please test this for your machine before relying on it giving a lot + of performance. Especially in conjunction with the ``OMP_NUM_THREADS`` + flag for OpenMP in linear algebra libraries. + Benchmark and see if it actually improves (certain combinations will + severly hurt performance). + +``SISL_VIZ_AUTOLOAD == false`` + whether or not to autoload the visualization module. + The visualization module imports many dependent modules. + If you run small scripts that does not use the `sisl.viz` module, then + it is recommended to keep this to be false. + +``SISL_SHOW_PROGRESS = false`` + Certain sisl routines has a builtin progress bar. This variable can default + whether or not those will be shown. It can be nice for *slow* brillouinzone calculations + to see if progress is actually being made. + +``SISL_IO_DEFAULT = ''`` + The default IO methods `sisl.get_sile` will select files with this file-endings. + For instance there are many ``stdout`` file types (for each DFT code). + Setting this to ``Siesta`` would force all files to first search for Siesta file + endings (see `sisl.io` for class names). + +``SISL_TMP = '.sisl_tmp'`` + certain internal methods of sisl will use a temporary folder for storing data. + The default is a new folder in the currently executed directory. + +``SISL_FILES_TESTS`` + Full path to a folder containing tests files. Primarily used for developers. + +``SISL_CONFIGDIR = $HOME/.config/sisl`` + where certain configuration files should be stored. + + Currently not in use. + + +Code specific environment variables +----------------------------------- + +Siesta +^^^^^^ + +``SISL_UNIT_SIESTA = codata2018 | legacy`` + determine the default units for Siesta files. + + Since Siesta 5.0, the default units are updated to follow + the CODATA 2018 values. This means that quite a bit of + results changed. This will force the internal variables + to be consistent with this. + diff --git a/docs/index.rst b/docs/index.rst index f414ef5561..f67e0d62fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -103,6 +103,7 @@ of places to search/ask for answers: installation tutorials.rst + environment scripts/scripts nodes/nodes_intro diff --git a/src/sisl/_environ.py b/src/sisl/_environ.py index 69bcde041c..76d78a7ec2 100644 --- a/src/sisl/_environ.py +++ b/src/sisl/_environ.py @@ -2,9 +2,11 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import os +from collections.abc import Callable from contextlib import contextmanager from pathlib import Path from textwrap import dedent +from typing import Any __all__ = ["register_environ_variable", "get_environ_variable", "sisl_environ"] @@ -33,9 +35,9 @@ def sisl_environ(**environ): SISL_ENVIRON[key]["value"] = old[key] -def register_environ_variable(name, default, - description=None, - process=None): +def register_environ_variable(name: str , default: Any, + description: str=None, + process: Callable[[Any], Any]=None): """Register a new global sisl environment variable. Parameters @@ -75,7 +77,7 @@ def process(arg): } -def get_environ_variable(name): +def get_environ_variable(name: str): """ Gets the value of a registered environment variable. Parameters diff --git a/src/sisl/io/siesta/tests/test_eig.py b/src/sisl/io/siesta/tests/test_eig.py index b7cfc84430..ccd44e2e25 100644 --- a/src/sisl/io/siesta/tests/test_eig.py +++ b/src/sisl/io/siesta/tests/test_eig.py @@ -10,10 +10,17 @@ from sisl.io.siesta.eig import * from sisl.io.siesta.fdf import * + pytestmark = [pytest.mark.io, pytest.mark.siesta] _dir = osp.join("sisl", "io", "siesta") +def _convert45(unit): + """ Convert from legacy units to CODATA2018 """ + from sisl.unit.siesta import units, units_legacy + return units(unit) / units_legacy(unit) + + def test_si_pdos_kgrid_eig(sisl_files): f = sisl_files(_dir, "si_pdos_kgrid.EIG") eig = eigSileSiesta(f).read_data() @@ -21,7 +28,6 @@ def test_si_pdos_kgrid_eig(sisl_files): # nspin, nk, nb assert np.all(eig.shape == (1, 32, 26)) - def test_si_pdos_kgrid_eig_ArgumentParser(sisl_files, sisl_tmp): pytest.importorskip("matplotlib", reason="matplotlib not available") png = sisl_tmp("si_pdos_kgrid.EIG.png", _dir) @@ -83,4 +89,4 @@ def test_soc_pt2_xx_eig_fermi_level(sisl_files): # vs. siesta we have to make this. # once https://gitlab.com/siesta-project/siesta/-/merge_requests/30 # is merged - assert ef == pytest.approx(ef2, abs=1e-5) + assert ef * _convert45("eV") == pytest.approx(ef2, abs=1e-5) diff --git a/src/sisl/tests/test_geometry.py b/src/sisl/tests/test_geometry.py index b01c2cc240..fbf82161ae 100644 --- a/src/sisl/tests/test_geometry.py +++ b/src/sisl/tests/test_geometry.py @@ -1453,6 +1453,7 @@ def test_geometry_ase_new_to(self): from_ase = gr.new(to_ase) assert gr.equal(from_ase, R=False) + @pytest.mark.xfail(reason="pymatgen backconversion sets nsc=[3, 3, 3], we need to figure this out") def test_geometry_pymatgen_to(self): pytest.importorskip("pymatgen", reason="pymatgen not available") gr = sisl_geom.graphene() diff --git a/src/sisl/unit/base.py b/src/sisl/unit/base.py index 81e1d2a55c..1b0079b7eb 100644 --- a/src/sisl/unit/base.py +++ b/src/sisl/unit/base.py @@ -38,6 +38,7 @@ 'atu': 2.4188843265857e-17}, 'energy': {'DEFAULT': 'eV', 'J': 1.0, + 'kJ': 1.e3, 'erg': 1e-07, 'K': 1.380649e-23, 'eV': 1.602176634e-19, @@ -55,7 +56,7 @@ @set_module("sisl.unit") -def unit_group(unit, tbl=None): +def unit_group(unit, tbl=unit_table): """ The group of units that `unit` belong to Parameters @@ -72,10 +73,6 @@ def unit_group(unit, tbl=None): >>> unit_group("eV") "energy" """ - if tbl is None: - global unit_table - tbl = unit_table - for k in tbl: if unit in tbl[k]: return k @@ -83,7 +80,7 @@ def unit_group(unit, tbl=None): @set_module("sisl.unit") -def unit_default(group, tbl=None): +def unit_default(group, tbl=unit_table): """ The default unit of the unit group `group`. Parameters @@ -98,10 +95,6 @@ def unit_default(group, tbl=None): >>> unit_default("energy") "eV" """ - if tbl is None: - global unit_table - tbl = unit_table - for k in tbl: if group == k: return tbl[k]["DEFAULT"] @@ -110,7 +103,7 @@ def unit_default(group, tbl=None): @set_module("sisl.unit") -def unit_convert(fr, to, opts=None, tbl=None): +def unit_convert(fr, to, opts=None, tbl=unit_table): """ Factor that takes `fr` to the units of `to` Parameters @@ -131,9 +124,6 @@ def unit_convert(fr, to, opts=None, tbl=None): >>> unit_convert("eV","J") 1.60217733e-19 """ - if tbl is None: - global unit_table - tbl = unit_table if opts is None: opts = dict() @@ -186,19 +176,17 @@ class UnitParser: unit_table : dict a table with the units parsable by the class """ - __slots__ = ["_table", "_p_left", "_left", "_p_right", "_right"] + __slots__ = ("_table", "_p_left", "_left", "_p_right", "_right") def __init__(self, table): self._table = table - def convert(fr, to): + def value(unit): tbl = self._table for k in tbl: - if fr in tbl[k]: - if to in tbl[k]: - return tbl[k][fr] / tbl[k][to] - break - raise ValueError(f"The unit conversion is not from the same group: {fr} to {to}!") + if unit in tbl[k]: + return tbl[k][unit] + raise ValueError(f"The unit conversion did not contain unit {unit}!") def group(unit): tbl = self._table @@ -215,26 +203,26 @@ def default(group): return k["DEFAULT"] self._left = [] - self._p_left = self.create_parser(convert, default, group, self._left) + self._p_left = self.create_parser(value, default, group, self._left) self._right = [] - self._p_right = self.create_parser(convert, default, group, self._right) + self._p_right = self.create_parser(value, default, group, self._right) @staticmethod - def create_parser(convert, default, group, group_table=None): + def create_parser(value, default, group, group_table=None): """ Routine to internally create a parser with specified unit_convert, unit_default and unit_group routines """ # Any length of characters will be used as a word. if group_table is None: - def _convert(t): - return convert(t[0], default(group(t[0]))) + def _value(t): + return value(t[0]) def _float(t): return float(t[0]) else: - def _convert(t): + def _value(t): group_table.append(group(t[0])) - return convert(t[0], default(group_table[-1])) + return value(t[0]) def _float(t): f = float(t[0]) @@ -242,7 +230,7 @@ def _float(t): return f # The unit extractor - unit = pp.Word(pp.alphas).setParseAction(_convert) + unit = pp.Word(pp.alphas).setParseAction(_value) integer = pp.Word(pp.nums) plusorminus = pp.oneOf("+ -") @@ -250,6 +238,8 @@ def _float(t): e = pp.CaselessLiteral("E") sign_integer = pp.Combine(pp.Optional(plusorminus) + integer) exponent = pp.Combine(e + sign_integer) + sign_integer = pp.Combine(pp.Optional(plusorminus) + integer) + exponent = pp.Combine(e + sign_integer) number = pp.Or([pp.Combine(point + integer + pp.Optional(exponent)), # .[0-9][E+-[0-9]] pp.Combine(integer + pp.Optional(point + pp.Optional(integer)) + pp.Optional(exponent))] # [0-9].[0-9][E+-[0-9]] ).setParseAction(_float) @@ -343,14 +333,13 @@ def _convert(self, A, B): conv_A = self._p_left.parseString(A)[0] conv_B = self._p_right.parseString(B)[0] if not self.same_group(self._left, self._right): - # Ensure lists are cleaned (in case the user catches stuff left = list(self._left) right = list(self._right) - self._left = [] - self._right = [] + self._left.clear() + self._right.clear() raise ValueError(f"The unit conversion is not from the same group: {left} to {right}!") - self._left = [] - self._right = [] + self._left.clear() + self._right.clear() return conv_A / conv_B def convert(self, *units): @@ -387,11 +376,11 @@ def convert(self, *units): elif len(units) == 1: # to default - conv = self._p_left(units[0]) - self._left = [] + conv = self._p_left.parseString(units[0])[0] + self._left.clear() return conv - return tuple(self._convert(units[i], units[i + 1]) for i in range(len(units) - 1)) + return tuple(self._convert(A, B) for A, B in zip(units[:-1], units[1:])) def __call__(self, *units): return self.convert(*units) diff --git a/src/sisl/unit/siesta.py b/src/sisl/unit/siesta.py index c88149e06b..76dcc9ecc9 100644 --- a/src/sisl/unit/siesta.py +++ b/src/sisl/unit/siesta.py @@ -9,6 +9,7 @@ which means these unit conversions should be used for Siesta "stuff". """ +from sisl._environ import get_environ_variable, register_environ_variable from sisl._internal import set_module from .base import UnitParser @@ -20,19 +21,25 @@ __all__ = ["unit_group", "unit_convert", "unit_default", "units"] -unit_table_siesta = dict({key: dict(values) for key, values in unit_table.items()}) +register_environ_variable("SISL_UNIT_SIESTA", "codata2018", + "Choose default units used when parsing Siesta files. [codata2018, legacy]", + process=str.lower) -unit_table_siesta["length"].update({ + +unit_table_siesta_codata2018 = dict({key: dict(values) for key, values in unit_table.items()}) +unit_table_siesta_legacy = dict({key: dict(values) for key, values in unit_table.items()}) + +unit_table_siesta_legacy["length"].update({ "Bohr": 0.529177e-10, }) -unit_table_siesta["time"].update({ +unit_table_siesta_legacy["time"].update({ "mins": 60., "hours": 3600., "days": 86400., }) -unit_table_siesta["energy"].update({ +unit_table_siesta_legacy["energy"].update({ "meV": 1.60219e-22, "eV": 1.60219e-19, "mRy": 2.17991e-21, @@ -49,7 +56,7 @@ "cm^-1": 1.986e-23, }) -unit_table_siesta["force"].update({ +unit_table_siesta_legacy["force"].update({ "eV/Ang": 1.60219e-9, "eV/Bohr": 1.60219e-9*0.529177, "Ry/Bohr": 4.11943e-8, @@ -57,28 +64,31 @@ }) +# Check for the correct handlers +_def_unit = get_environ_variable("SISL_UNIT_SIESTA") +if _def_unit in ("codata2018", "codata"): + unit_table_siesta = unit_table_siesta_codata2018 +elif _def_unit in ("legacy", "original"): + unit_table_siesta = unit_table_siesta_legacy +else: + raise ValueError(f"Could not understand SISL_UNIT_SIESTA={_def_unit}, expected one of [codata2018, legacy]") + + @set_module("sisl.unit.siesta") -def unit_group(unit, tbl=None): - global unit_table_siesta - if tbl is None: - return u_group(unit, unit_table_siesta) +def unit_group(unit, tbl=unit_table_siesta): return u_group(unit, tbl) @set_module("sisl.unit.siesta") -def unit_default(group, tbl=None): - global unit_table_siesta - if tbl is None: - return u_default(group, unit_table_siesta) +def unit_default(group, tbl=unit_table_siesta): return u_default(group, tbl) @set_module("sisl.unit.siesta") -def unit_convert(fr, to, opts=None, tbl=None): - global unit_table_siesta - if tbl is None: - return u_convert(fr, to, opts, unit_table_siesta) +def unit_convert(fr, to, opts=None, tbl=unit_table_siesta): return u_convert(fr, to, opts, tbl) + # create unit-parser units = UnitParser(unit_table_siesta) +units_legacy = UnitParser(unit_table_siesta_legacy) diff --git a/src/sisl/unit/tests/test_unit.py b/src/sisl/unit/tests/test_unit.py index 48a080d5e2..757c3b2346 100644 --- a/src/sisl/unit/tests/test_unit.py +++ b/src/sisl/unit/tests/test_unit.py @@ -67,3 +67,7 @@ def test_unit_convert_f1(): def test_unit_convert_f2(): with pytest.raises(ValueError): unit_convert('eV', 'kg') + + +def test_unit_convert_single(): + assert units("Ang") == 1e-10 diff --git a/src/sisl/unit/tests/test_unit_siesta.py b/src/sisl/unit/tests/test_unit_siesta.py index 8567e6fa76..0ca3d26d54 100644 --- a/src/sisl/unit/tests/test_unit_siesta.py +++ b/src/sisl/unit/tests/test_unit_siesta.py @@ -5,33 +5,30 @@ approx = pytest.approx -from sisl.unit.siesta import unit_convert, unit_default, unit_group, unit_table_siesta +from sisl.unit.siesta import unit_convert, unit_default, unit_group -pytestmark = pytest.mark.unit +pytestmark = [pytest.mark.unit, pytest.mark.siesta] -@pytest.mark.parametrize('tbl', [None, unit_table_siesta]) -def test_group(tbl): - assert unit_group('kg', tbl) == 'mass' - assert unit_group('eV', tbl) == 'energy' - assert unit_group('N', tbl) == 'force' +def test_group(): + assert unit_group('kg') == 'mass' + assert unit_group('eV') == 'energy' + assert unit_group('N') == 'force' -@pytest.mark.parametrize('tbl', [None, unit_table_siesta]) -def test_unit_convert(tbl): - assert approx(unit_convert('kg', 'g', tbl=tbl)) == 1.e3 - assert approx(unit_convert('eV', 'J', tbl=tbl)) == 1.60219e-19 - assert approx(unit_convert('J', 'eV', tbl=tbl)) == 1/1.60219e-19 - assert approx(unit_convert('J', 'eV', {'^': 2}, tbl)) == (1/1.60219e-19) ** 2 - assert approx(unit_convert('J', 'eV', {'/': 2}, tbl)) == (1/1.60219e-19) / 2 - assert approx(unit_convert('J', 'eV', {'*': 2}, tbl)) == (1/1.60219e-19) * 2 +def test_unit_convert(): + assert approx(unit_convert('kg', 'g')) == 1.e3 + assert approx(unit_convert('eV', 'J')) == 1.602176634e-19 + assert approx(unit_convert('J', 'eV')) == 1/1.602176634e-19 + assert approx(unit_convert('J', 'eV', {'^': 2})) == (1/1.602176634e-19) ** 2 + assert approx(unit_convert('J', 'eV', {'/': 2})) == (1/1.602176634e-19) / 2 + assert approx(unit_convert('J', 'eV', {'*': 2})) == (1/1.602176634e-19) * 2 -@pytest.mark.parametrize('tbl', [None, unit_table_siesta]) -def test_default(tbl): - assert unit_default('mass', tbl) == 'amu' - assert unit_default('energy', tbl) == 'eV' - assert unit_default('force', tbl) == 'eV/Ang' +def test_default(): + assert unit_default('mass') == 'amu' + assert unit_default('energy') == 'eV' + assert unit_default('force') == 'eV/Ang' def test_group_f1():