From 1c09dcfa3b2102b218be57d874aa3ea50f3a14ff Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 14:43:35 +0200 Subject: [PATCH 01/46] update default parameters --- larixite/fdmnes.py | 15 ++++++++------- larixite/templates/fdmnes_xas.tmpl | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 2432ab3..eeb35f5 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -29,18 +29,19 @@ "Density": False, "Density_all": False, "Green": True, + "Polarize": False, #: -> TODO "Memory_save": True, "Relativism": False, "Spinorbit": None, "SCF": False, - "SCFexc": False, - "SCFexcv": False, - "Screening": False, - "Full_atom": False, + "SCF_exc": False, + "Screening": False, #: -> TODO + "Dilatorb": False, #: -> TODO "TDDFT": False, - "PBE96": False, + "SpGr_atom": False, #: `Full_atom` is by default since March 2026 + "Hedin": False, #: `PBE69` is by default since March 2026 "Atom_conf": False, #: preferred over `Atom` (permits to keep atomic number in the list of atoms) -> TODO - "COOP": False, + "COOP": False, #: -> TODO "Convolution": True, } @@ -60,7 +61,7 @@ class FdmnesXasInput: radius: float = 7 #: radius of the calculation struct_type: Union[str, None] = None #: type of the structure vmax: Union[float, None] = None #: maximum potential value for molecules - erange: str = "-20.0 0.1 70.0 1.0 100.0" #: energy range + erange: str = "-10.0 0.25 60.0 1.0 100.0" #: energy range fileout_prefix: str = ( "job" #: prefix of the output filename for the FDMNES job (extension: .inp) ) diff --git a/larixite/templates/fdmnes_xas.tmpl b/larixite/templates/fdmnes_xas.tmpl index f508d11..8a9fb10 100755 --- a/larixite/templates/fdmnes_xas.tmpl +++ b/larixite/templates/fdmnes_xas.tmpl @@ -16,16 +16,26 @@ Range {Energpho} Radius {radius} +! +{Green} ! {Quadrupole} +! +{Polarize} +! {TDDFT} +{Memory_save} {Relativism} {Spinorbit} -{Full_atom} {Density} {Density_all} +! +{PBE96} !PBE96 by default since March 2026 +! +{SpGr_atom} !`Full_atom` is default since March 2026 +! {SCF} -{Green} +{SCF_exc} {vmax} {Atom_conf} ! From 262cde1fc764b73bc52f6ce5fa086c0d73c3d82b Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 14:48:09 +0200 Subject: [PATCH 02/46] type hinting: remove Union --- larixite/fdmnes.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index eeb35f5..48ab0d8 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -11,7 +11,6 @@ """ from dataclasses import dataclass -from typing import Union from pathlib import Path from pymatgen.core import __version__ as pymatgen_version, Element, Molecule from larixite.struct import get_structure, get_structure_from_text @@ -29,19 +28,19 @@ "Density": False, "Density_all": False, "Green": True, - "Polarize": False, #: -> TODO + "Polarize": False, #: -> TODO "Memory_save": True, "Relativism": False, "Spinorbit": None, "SCF": False, "SCF_exc": False, "Screening": False, #: -> TODO - "Dilatorb": False, #: -> TODO + "Dilatorb": False, #: -> TODO "TDDFT": False, "SpGr_atom": False, #: `Full_atom` is by default since March 2026 - "Hedin": False, #: `PBE69` is by default since March 2026 + "Hedin": False, #: `PBE69` is by default since March 2026 "Atom_conf": False, #: preferred over `Atom` (permits to keep atomic number in the list of atoms) -> TODO - "COOP": False, #: -> TODO + "COOP": False, #: -> TODO "Convolution": True, } @@ -50,23 +49,21 @@ class FdmnesXasInput: """Input generator for a XAS calculation with FDMNES""" - structpath: Union[ - str, Path, XasStructure - ] #: path to the structural file or XasStructure - absorber: Union[ - str, int, Element - ] #: atomic symbol or number of the absorbing element + structpath: ( + str | Path | XasStructure + ) #: path to the structural file or XasStructure + absorber: str | int | Element #: atomic symbol or number of the absorbing element frame: int = 0 #: index of the frame inside the structure - edge: Union[str, None] = None #: edge for calculation + edge: str | None = None #: edge for calculation radius: float = 7 #: radius of the calculation - struct_type: Union[str, None] = None #: type of the structure - vmax: Union[float, None] = None #: maximum potential value for molecules + struct_type: str | None = None #: type of the structure + vmax: float | None = None #: maximum potential value for molecules erange: str = "-10.0 0.25 60.0 1.0 100.0" #: energy range fileout_prefix: str = ( "job" #: prefix of the output filename for the FDMNES job (extension: .inp) ) - tmplpath: Union[str, Path, None] = None #: path to the FDMNES input template - params: Union[dict[str, bool], None] = None #: enable/disable parameters for FDMNES + tmplpath: str | Path | None = None #: path to the FDMNES input template + params: dict[str, bool] | None = None #: enable/disable parameters for FDMNES spacer: str = " " #: spacer for the FDMNES input text def __post_init__(self): @@ -181,7 +178,7 @@ def optimize_params(self) -> dict: return params - def get_structure(self, struct_type: Union[str, None] = None) -> str: + def get_structure(self, struct_type: str | None = None) -> str: """Get the structure section of the input Parameters @@ -323,7 +320,7 @@ def get_input(self, comment: str = "", struct_type: str = None) -> str: return strict_ascii(template.format(**conf)) def write_input( - self, inputtext: Union[str, None] = None, outdir: Union[str, Path, None] = None + self, inputtext: str | None = None, outdir: str | Path | None = None ) -> Path: """Write the FDMNES input text to disk and return the output directory""" if inputtext is None: @@ -350,11 +347,11 @@ def write_input( def struct2fdmnes( - inp: Union[str, Path], - absorber: Union[str, int, Element], + inp: str | Path, + absorber: str | int | Element, frame: int = 0, format: str = "cif", - filename: Union[None, str] = None, + filename: None | str = None, radius: float = 7, edge: str | None = None, ) -> dict: From fe05c8b490ef1b847ceee8dd1f6843ccac6f4e89 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 15:54:25 +0200 Subject: [PATCH 03/46] make xs private --- larixite/fdmnes.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 48ab0d8..b99b0b7 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -22,17 +22,20 @@ TEMPLATE_FOLDER = Path(Path(__file__).parent, "templates") -FDMNES_DEFAULT_PARAMS = { +FDMNES_DEFAULT_PARAMS = { #: "FDMNES key name": True/False "Energpho": False, "Quadrupole": False, + "Polarize": False, #: -> TODO "Density": False, "Density_all": False, "Green": True, - "Polarize": False, #: -> TODO "Memory_save": True, "Relativism": False, - "Spinorbit": None, + "Spinorbit": False, "SCF": False, + "R_self": False, #: -> TODO + "N_self": False, #: -> TODO + "P_self": False, #: -> TODO "SCF_exc": False, "Screening": False, #: -> TODO "Dilatorb": False, #: -> TODO @@ -51,9 +54,9 @@ class FdmnesXasInput: structpath: ( str | Path | XasStructure - ) #: path to the structural file or XasStructure + ) #: str or path to the structural file, or directly the XasStructure object absorber: str | int | Element #: atomic symbol or number of the absorbing element - frame: int = 0 #: index of the frame inside the structure + frame: int = 0 #: index of the frame inside the structure (e.g. for multi-frame XYZ files) edge: str | None = None #: edge for calculation radius: float = 7 #: radius of the calculation struct_type: str | None = None #: type of the structure @@ -77,17 +80,17 @@ def __post_init__(self): if isinstance(self.structpath, str): self.structpath = Path(self.structpath) if isinstance(self.structpath, XasStructure): - self.xs = self.structpath - self.structpath = self.xs.filepath + self._xs = self.structpath + self.structpath = self._xs.filepath else: - self.xs = get_structure(self.structpath, absorber=self.absorber) + self._xs = get_structure(self.structpath, absorber=self.absorber) #: radius self.set_radius(self.radius) #: structure type if self.struct_type is None: - self.struct_type = self.xs.struct_type + self.struct_type = self._xs.struct_type else: - self.xs.struct_type = self.struct_type + self._xs.struct_type = self.struct_type #: template if self.tmplpath is None: self.tmplpath = Path(TEMPLATE_FOLDER, "fdmnes_xas.tmpl") @@ -102,7 +105,7 @@ def __post_init__(self): def set_radius(self, value: float): self.radius = value - self.xs.radius = value + self._xs.radius = value def validate_edge(self): """Validates and adjusts the edge attribute""" @@ -144,7 +147,7 @@ def validate_edge(self): def optimize_params(self) -> dict: """Optimize the given input parameters""" params = self.params.copy() - atoms_z = [species.Z for species in self.xs.struct.types_of_species] + atoms_z = [species.Z for species in self._xs.struct.types_of_species] abs_z = self.absorber.Z transition_metals = [range(21, 31), range(39, 49), range(57, 81)] @@ -207,7 +210,7 @@ def get_structure(self, struct_type: str | None = None) -> str: self.optimize_params() else: struct_type = self.struct_type - if "crys" in struct_type.lower() and isinstance(self.xs.struct, Molecule): + if "crys" in struct_type.lower() and isinstance(self._xs.struct, Molecule): errmsg = "cannot generate a crystal input from a molecule -> use `struct_type='molecule'`" logger.error(errmsg) raise AttributeError(errmsg) @@ -218,7 +221,7 @@ def get_structure(self, struct_type: str | None = None) -> str: structout.append("Z_absorber") structout.append(f"{self.spacer}{self.absorber.Z}") #: space group - spgrp = self.xs.space_group + spgrp = self._xs.space_group if spgrp == 1: #: FDMNES doesn't recognize 1 as a space group -> `P1` spgrp = "P1" if spgrp == 2: #: FDMNES doesn't recognize 2 as a space group -> `P-1` @@ -229,7 +232,7 @@ def get_structure(self, struct_type: str | None = None) -> str: structout.append("Occupancy") #: crystal structout.append("Crystal") - lattice = self.xs.sym_struct.lattice + lattice = self._xs.sym_struct.lattice structout.append( f"{self.spacer}{lattice.a} {lattice.b} {lattice.c} {lattice.alpha} {lattice.beta} {lattice.gamma}" ) @@ -240,11 +243,11 @@ def get_structure(self, struct_type: str | None = None) -> str: occupancy, len_sites, wyckoff, - ) in self.xs.unique_sites: + ) in self._xs.unique_sites: zelems = [elem.Z for elem in site.species.elements] if not len(set(zelems)) == 1: logger.warning( - f"[{self.xs.label}] site {idx} has species with different Z -> {site.species_string}" + f"[{self._xs.label}] site {idx} has species with different Z -> {site.species_string}" ) for elem, elstr in zip( site.species.elements, site.species_string.split(", ") @@ -253,7 +256,7 @@ def get_structure(self, struct_type: str | None = None) -> str: structout.append(sitestr) elif "mol" in struct_type.lower(): #: build the cluster and map by distance from absorber at (0,0,0) - mol = self.xs.cluster + mol = self._xs.cluster map_mol_by_dist = [(0, 0)] for i, site in enumerate(mol[1:]): isite = i + 1 @@ -294,7 +297,7 @@ def get_input(self, comment: str = "", struct_type: str = None) -> str: template = open(self.tmplpath, "r").read() comment = ( - f"{self.spacer}{self.xs.name}: {self.absorber.symbol} ({self.absorber.Z}) {self.edge} edge" + f"{self.spacer}{self._xs.name}: {self.absorber.symbol} ({self.absorber.Z}) {self.edge} edge" + comment ) #: fill the template @@ -329,7 +332,7 @@ def write_input( import tempfile outdir = ( - Path(tempfile.gettempdir()) / "larixite" / "fdmnes" / str(self.xs.name) + Path(tempfile.gettempdir()) / "larixite" / "fdmnes" / str(self._xs.name) ) outdir.mkdir(parents=True, exist_ok=True) outdir = tempfile.mkdtemp(dir=outdir, prefix="job_") From 0bb5f84db224fe1c194ba2e03a8f3c711a7a0386 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 15:54:41 +0200 Subject: [PATCH 04/46] wip: add more keys --- larixite/templates/fdmnes_xas.tmpl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/larixite/templates/fdmnes_xas.tmpl b/larixite/templates/fdmnes_xas.tmpl index 8a9fb10..b0756c6 100755 --- a/larixite/templates/fdmnes_xas.tmpl +++ b/larixite/templates/fdmnes_xas.tmpl @@ -21,8 +21,8 @@ Radius ! {Quadrupole} ! -{Polarize} -! +!{Polarize} +!{_polarize} {TDDFT} {Memory_save} {Relativism} @@ -35,6 +35,12 @@ Radius {SpGr_atom} !`Full_atom` is default since March 2026 ! {SCF} +!{R_self} +!{_rself} +!{N_self} +!{_nself} +!{P_self} +!{_pself} {SCF_exc} {vmax} {Atom_conf} From 447df203a860379af8b751f410a2f535722f340c Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 16:42:32 +0200 Subject: [PATCH 05/46] add SCF params + make optimize optional --- larixite/fdmnes.py | 38 +++++++++++++++++++----------- larixite/templates/fdmnes_xas.tmpl | 12 +++++----- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index b99b0b7..1d0534d 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -33,9 +33,6 @@ "Relativism": False, "Spinorbit": False, "SCF": False, - "R_self": False, #: -> TODO - "N_self": False, #: -> TODO - "P_self": False, #: -> TODO "SCF_exc": False, "Screening": False, #: -> TODO "Dilatorb": False, #: -> TODO @@ -61,12 +58,16 @@ class FdmnesXasInput: radius: float = 7 #: radius of the calculation struct_type: str | None = None #: type of the structure vmax: float | None = None #: maximum potential value for molecules + rself: float | None = None #: radius for the SCF calculation + nself: int = 100 #: number of maximum iterations for the SCF calculation + pself: float = 0.025 #: initial weight for the SCF calculation erange: str = "-10.0 0.25 60.0 1.0 100.0" #: energy range fileout_prefix: str = ( "job" #: prefix of the output filename for the FDMNES job (extension: .inp) ) tmplpath: str | Path | None = None #: path to the FDMNES input template params: dict[str, bool] | None = None #: enable/disable parameters for FDMNES + optimize: bool = False #: optimize the input parameters spacer: str = " " #: spacer for the FDMNES input text def __post_init__(self): @@ -101,6 +102,7 @@ def __post_init__(self): #: optimize params if self.params is None: self.params = FDMNES_DEFAULT_PARAMS + if self.optimize: self.params = self.optimize_params() def set_radius(self, value: float): @@ -166,18 +168,16 @@ def optimize_params(self) -> dict: "Spinorbit enabled. **NOTE**: the simulations are typically 4 to 8 times longer and need 2 times more memory space" ) - if (8 in atoms_z) and (params["SCF"] is True): - """Sometimes, and specifically for oxides, it can be noted that SCF leads to solutions not - converging versus the cluster radius. One observes for instance a beating phenomenon - on the atomic charges, versus the number of atomic shells incoming in the calculation - area. This can be overcome by the use of the keyword "Full_atom" which suppress an - approximation, taking the cluster atoms equivalent by the space group symmetry - instead of the punctual group symmetry.""" - - params["Full_atom"] = True - if "mol" in self.struct_type.lower(): self.vmax = -6 + logger.info(f"Vmax set to {self.vmax} for molecule") + + if self.radius < 3.5: + self.rself = self.radius + else: + self.rself = 3.5 + if params["SCF"]: + logger.info(f"R_self set to {self.rself} for radius {self.radius}") return params @@ -283,7 +283,7 @@ def get_structure(self, struct_type: str | None = None) -> str: return "\n".join(structout) def get_vmax(self) -> str: - """Get the vmax section of the input""" + """Get the Vmax section of the input""" if self.vmax is not None: vmax = ["Vmax"] vmax.append(f"{self.spacer}-6") @@ -291,6 +291,13 @@ def get_vmax(self) -> str: else: return "! Vmax" + def get_rself(self): + """Get the R_self section of the input""" + if self.rself is not None: + return f"{self.spacer}{self.rself}" + else: + return "!" + def get_input(self, comment: str = "", struct_type: str = None) -> str: """Get the FDMNES input text""" params = self.params.copy() @@ -314,6 +321,9 @@ def get_input(self, comment: str = "", struct_type: str = None) -> str: "radius": f"{self.spacer}{self.radius:.2f}", "erange": self.spacer + self.erange, "vmax": self.get_vmax(), + "rself": self.get_rself(), + "nself": f"{self.spacer}{self.nself}", + "pself": f"{self.spacer}{self.pself:.3f}", "struct_type": self.struct_type, "structure": self.get_structure(struct_type=struct_type), } diff --git a/larixite/templates/fdmnes_xas.tmpl b/larixite/templates/fdmnes_xas.tmpl index b0756c6..ee14a66 100755 --- a/larixite/templates/fdmnes_xas.tmpl +++ b/larixite/templates/fdmnes_xas.tmpl @@ -35,12 +35,12 @@ Radius {SpGr_atom} !`Full_atom` is default since March 2026 ! {SCF} -!{R_self} -!{_rself} -!{N_self} -!{_nself} -!{P_self} -!{_pself} +R_self +{rself} +N_self +{nself} +P_self +{pself} {SCF_exc} {vmax} {Atom_conf} From 6344fe63553094d61bee24a3b9d6567b0b097c78 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Mon, 11 May 2026 16:58:54 +0200 Subject: [PATCH 06/46] bug fixes --- larixite/fdmnes.py | 17 ++++++++++------- larixite/templates/fdmnes_xas.tmpl | 8 ++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 1d0534d..66df064 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -87,6 +87,9 @@ def __post_init__(self): self._xs = get_structure(self.structpath, absorber=self.absorber) #: radius self.set_radius(self.radius) + #: R_self + if self.rself is None: + self.set_rself() #: structure type if self.struct_type is None: self.struct_type = self._xs.struct_type @@ -109,6 +112,13 @@ def set_radius(self, value: float): self.radius = value self._xs.radius = value + def set_rself(self): + if self.radius < 3.5: + self.rself = self.radius + else: + self.rself = 3.5 + logger.info(f"R_self set to {self.rself} for radius {self.radius}") + def validate_edge(self): """Validates and adjusts the edge attribute""" @@ -172,13 +182,6 @@ def optimize_params(self) -> dict: self.vmax = -6 logger.info(f"Vmax set to {self.vmax} for molecule") - if self.radius < 3.5: - self.rself = self.radius - else: - self.rself = 3.5 - if params["SCF"]: - logger.info(f"R_self set to {self.rself} for radius {self.radius}") - return params def get_structure(self, struct_type: str | None = None) -> str: diff --git a/larixite/templates/fdmnes_xas.tmpl b/larixite/templates/fdmnes_xas.tmpl index ee14a66..656e211 100755 --- a/larixite/templates/fdmnes_xas.tmpl +++ b/larixite/templates/fdmnes_xas.tmpl @@ -13,7 +13,7 @@ Edge ! Range {erange} -{Energpho} +{Energpho} !output energy as photon energy (default relative to Fermi level) Radius {radius} ! @@ -21,8 +21,8 @@ Radius ! {Quadrupole} ! -!{Polarize} -!{_polarize} +{Polarize} +! =>TODO {TDDFT} {Memory_save} {Relativism} @@ -30,7 +30,7 @@ Radius {Density} {Density_all} ! -{PBE96} !PBE96 by default since March 2026 +{Hedin} !PBE96 is default since March 2026 ! {SpGr_atom} !`Full_atom` is default since March 2026 ! From bf8269e52dcc93a5b59388d9dd018bc30b9762c7 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 09:58:34 +0200 Subject: [PATCH 07/46] synchronize struct2xas from Larch --- larixite/struct2xas.py | 86 ++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index 965164d..3e7f695 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -28,14 +28,6 @@ from larch.io import read_ascii from larch.math.convolution1D import lin_gamma, conv -try: - import pandas as pd - from pandas.io.formats.style import Styler - - HAS_PANDAS = True -except ImportError: - HAS_PANDAS = False - try: import py3Dmol @@ -46,8 +38,8 @@ __author__ = ["Beatriz G. Foschiani", "Mauro Rovezzi"] __email__ = ["beatrizgfoschiani@gmail.com", "mauro.rovezzi@esrf.fr"] -__credits__ = ["Jade Chongsathapornpong", "Marius Retegan"] -__version__ = "2024.1.0" +__credits__ = ["Jade Chongsathapornpong", "Marius Retegan", "Helen Engelhardt"] +__version__ = "2026.1.0" # initialize the logger @@ -72,30 +64,20 @@ def _pprint(matrix): def xyz2struct(molecule): - """Convert pymatgen molecule to dummy pymatgen structure""" - - alat, blat, clat = 1, 1, 1 - - # Set the lattice dimensions in each direction - for i in range(len(molecule) - 1): - if molecule.cart_coords[i][0] > molecule.cart_coords[i + 1][0]: - alat = molecule.cart_coords[i][0] - if molecule.cart_coords[i][1] > molecule.cart_coords[i + 1][1]: - blat = molecule.cart_coords[i][1] - if molecule.cart_coords[i][2] > molecule.cart_coords[i + 1][2]: - clat = molecule.cart_coords[i][2] - - # Set the lattice dimensions in each direction + """Convert pymatgen molecule to dummy pymatgen structure using vectorized calculation.""" + # Ensure the coordinates are in a NumPy array + coords = np.array(molecule.cart_coords) + + # Compute maximum coordinate along each axis + alat, blat, clat = np.max(coords, axis=0) + lattice = Lattice.from_parameters( a=alat, b=blat, c=clat, alpha=90, beta=90, gamma=90 ) - + # Create a list of species species = [Element(sym) for sym in molecule.species] - - # Create a list of coordinates - coords = molecule.cart_coords - + # Create the Structure object struct = Structure(lattice, species, coords, coords_are_cartesian=True) return struct @@ -474,17 +456,9 @@ def get_abs_sites_info(self): "idx_in_struct", ] abs_sites = self.get_abs_sites() - if HAS_PANDAS: - df = pd.DataFrame( - abs_sites, - columns=header, - ) - df = Styler(df).hide(axis="index") - return df - else: - matrix = [header] - matrix.extend(abs_sites) - _pprint(matrix) + matrix = [header] + matrix.extend(abs_sites) + _pprint(matrix) def get_atoms_from_abs(self, radius): """Get atoms in sphere from absorbing atom with certain radius""" @@ -744,13 +718,9 @@ def get_coord_envs_info(self): ) print(coord_sym) header = ["Element", "Distance"] - if HAS_PANDAS: - df = pd.DataFrame(data=elems_dist, columns=header) - return df - else: - matrix = [header] - matrix.extend(elems_dist) - _pprint(matrix) + matrix = [header] + matrix.extend(elems_dist) + _pprint(matrix) def make_cluster(self, radius): """Create a cluster with absorber atom site at the center. @@ -942,7 +912,7 @@ def make_input_fdmnes( np.allclose(unique_sites[i][0].coords, selected_site[4], atol=0.01) is True ): - replacements["absorber"] = f"absorber\n{i+1}" + replacements["absorber"] = f"absorber\n{i + 1}" # absorber = f"{absorber}" # replacements["absorber"] = f"Z_absorber\n{round(Element(elem).Z)}" @@ -963,7 +933,7 @@ def make_input_fdmnes( absorber = f"{absorber}" for i in range(len(atoms)): if np.allclose(atoms[i][1], [0, 0, 0], atol=0.01) is True: - replacements["absorber"] = f"absorber\n{i+1}" + replacements["absorber"] = f"absorber\n{i + 1}" replacements["group"] = "" @@ -1029,6 +999,7 @@ def make_input_feff( edge: str = "K", sig2: Union[float, None] = None, debye: Union[List[float], None] = None, + feff_print: Union[List[int], None] = None, **kwargs, ): """ @@ -1056,6 +1027,11 @@ def make_input_feff( temperature at which the Debye-Waller factors are calculated [Kelvin]. debye_temperature : float Debye Temperature of the material [Kelvin]. + feff_print : list of six ints or None, [None] + PRINT card values controlling output verbosity for each FEFF module: + [pot, xsph, fms, paths, genfmt, ff2chi] + Values: 0=minimal, 1=standard, 2+=verbose + If None, defaults to [1, 0, 0, 0, 0, 3] (matches structure2feff.py) ..note:: refer to [FEFF documentation](https://feff.phys.washington.edu/feffproject-feff-documentation.html) @@ -1124,6 +1100,10 @@ def make_input_feff( use_debye = "" temperature, debye_temperature = debye[0], debye[1] + # Default PRINT values match structure2feff.py behavior + if feff_print is None: + feff_print = [1, 0, 0, 0, 0, 3] + feff_comment = f"{feff_comment}" edge = f"{edge}" radius = f"{radius}" @@ -1210,7 +1190,7 @@ def make_input_feff( for i in range(len(at)): if self.full_occupancy: atoms += "\n" + ( - f"{at[i][0][0]:10.6f} {at[i][0][1]:10.6f} {at[i][0][2]:10.6f} { int(at[i][1])} {at[i][2]:>5} {at[i][3]:10.5f} *1 " + f"{at[i][0][0]:10.6f} {at[i][0][1]:10.6f} {at[i][0][2]:10.6f} {int(at[i][1])} {at[i][2]:>5} {at[i][3]:10.5f} *1 " ) else: choice = np.random.choice( @@ -1233,6 +1213,12 @@ def make_input_feff( replacements["potentials"] = potentials replacements["atoms"] = atoms replacements["title"] = title + replacements["print_pot"] = feff_print[0] + replacements["print_xsph"] = feff_print[1] + replacements["print_fms"] = feff_print[2] + replacements["print_paths"] = feff_print[3] + replacements["print_genfmt"] = feff_print[4] + replacements["print_ff2chi"] = feff_print[5] # replacements[""] = try: From a5def3c0f608e299b7d33d8c903a274fe0c180e3 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 10:20:58 +0200 Subject: [PATCH 08/46] add deprecation warnings --- larixite/struct2xas.py | 9 ++++++++- .../{fdmnes.tmpl => fdmnes_struct2xas_to_remove.tmpl} | 0 2 files changed, 8 insertions(+), 1 deletion(-) rename larixite/templates/{fdmnes.tmpl => fdmnes_struct2xas_to_remove.tmpl} (100%) diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index 3e7f695..df342d3 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -3,6 +3,12 @@ """ Struct2XAS: convert CIFs and XYZs files to FDMNES and FEFF inputs + + +/!\ DEPRECATED /!\ +THIS MODULE WILL BE REMOVED IN FUTURE VERSIONS +-> USE larixite.struct or larixite.fdmnes +/!\ DEPRECATED /!\ """ # main imports @@ -802,6 +808,7 @@ def make_input_fdmnes( ||||||+ job{njob} """ + logger.warning("`make_input_fdmnes` is deprecated, use `larixite.fdmnes` instead") if parent_path is None: parent_path = self.parent_path @@ -831,7 +838,7 @@ def make_input_fdmnes( if template is None: template = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "templates", "fdmnes.tmpl" + os.path.dirname(os.path.realpath(__file__)), "templates", "fdmnes_struct2xas_to_remove.tmpl" ) assert os.path.isfile(template), "wrong template path" diff --git a/larixite/templates/fdmnes.tmpl b/larixite/templates/fdmnes_struct2xas_to_remove.tmpl similarity index 100% rename from larixite/templates/fdmnes.tmpl rename to larixite/templates/fdmnes_struct2xas_to_remove.tmpl From 911b1a46aab9ac2cf8db8a90b23bad118c57efb9 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 10:59:54 +0200 Subject: [PATCH 09/46] renamed to examples --- examples/fdmnes_xas_workflow.ipynb | 484 +++++++++++++++++++++++++++++ notebooks/workflow_fdmnes.ipynb | 202 ------------ 2 files changed, 484 insertions(+), 202 deletions(-) create mode 100644 examples/fdmnes_xas_workflow.ipynb delete mode 100644 notebooks/workflow_fdmnes.ipynb diff --git a/examples/fdmnes_xas_workflow.ipynb b/examples/fdmnes_xas_workflow.ipynb new file mode 100644 index 0000000..b24f694 --- /dev/null +++ b/examples/fdmnes_xas_workflow.ipynb @@ -0,0 +1,484 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# FDMNES XAS simulation workflow with Larixite\n", + "This notebook shows an example workflow for FDMNES XAS simulation using Larixite.\n", + "\n", + "- Author: Mauro Rovezzi\n", + "- Contact: mauro.rovezzi@esrf.fr\n", + "- Status: *in progress...*\n", + "- Last update: 2026-05-12" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First import the necessary modules and set some variables" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext autoreload\n", + "%autoreload 2\n", + "\n", + "from pathlib import Path\n", + "import numpy as np\n", + "import crystal_toolkit\n", + "import warnings\n", + "from larixite.struct import get_structs_from_dir\n", + "from larixite.fdmnes import FdmnesXasInput\n", + "from larixite.fdmnes import logger\n", + "\n", + "logger.setLevel(\"INFO\") #: adjust this level if you want to have more or less information: \"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"\n", + "warnings.simplefilter('always', UserWarning) #: to filter out some technic warnings from underlying pymatgen\n", + "\n", + "#: set a directory containing the structural files (TODO: set your own path)\n", + "curdir = Path().cwd()\n", + "basedir = curdir.parent\n", + "testdir = basedir / \"tests\"\n", + "structsdir = testdir / \"structs\"\n", + "\n", + "abs = \"Zn\" #: select the absorber\n", + "\n", + "fdmnes_executable = \"fdmnes\" #: path to fdmnes executable (TODO: set your own path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get a list of XasStructure objects from a directory containing structural files, based on the absorbing element specified before." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 4 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", + " struct = structs.parse_structures(primitive=False)[frame]\n", + "\u001b[32;21m[larixite.struct | INFO ] 0: ZnO_wurtzite_Weber1923_COD-1011259.cif\u001b[0m\n" + ] + } + ], + "source": [ + "structs = get_structs_from_dir(structsdir, abs, globstr=f\"*{abs}*\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "select one structure and print some information about it" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unique sites for ZnO_wurtzite_Weber1923_COD-1011259.cif\n", + "iunique\tistruct\tlabel\tfrac_coords \toccu\tcart_coords \tnsites\twyckoff\tspecies_string\n", + "0 (abs)\t0 \tZn1 \t[0.33333333 0.66666667 0. ]\t1 \t[3.58065429e-16 1.93470075e+00 2.05189571e-16]\t2 \t2b \tZn2+ \n", + "1 \t2 \tO1 \t[0.33333333 0.66666667 0.375 ]\t1 \t[3.58065429e-16 1.93470075e+00 1.95975000e+00]\t2 \t2b \tO2- \n" + ] + } + ], + "source": [ + "sg = structs[0]\n", + "sg.show_unique_sites()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the structure can be visualized with the [Crystal Toolkit](https://docs.crystaltoolkit.org/)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sg.struct" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The absorber site is set by default to the first site with the absorbing element, but can be changed by indicating the index in the structure (`istruct` given by the `show_unique_sites()` method) to the `absorber_idx` attribute. It is convenient to check with the `show_unique_sites()` method that the index of the absorbing site is correctly set, as it gets the `(abs)` label next to the index." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unique sites for ZnO_wurtzite_Weber1923_COD-1011259.cif\n", + "iunique\tistruct\tlabel\tfrac_coords \toccu\tcart_coords \tnsites\twyckoff\tspecies_string\n", + "0 (abs)\t0 \tZn1 \t[0.33333333 0.66666667 0. ]\t1 \t[3.58065429e-16 1.93470075e+00 2.05189571e-16]\t2 \t2b \tZn2+ \n", + "1 \t2 \tO1 \t[0.33333333 0.66666667 0.375 ]\t1 \t[3.58065429e-16 1.93470075e+00 1.95975000e+00]\t2 \t2b \tO2- \n" + ] + } + ], + "source": [ + "sg.absorber_idx = 0\n", + "sg.show_unique_sites()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "then generate an FDMNES input file FdmnesXasInput object and show it" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32;21m[larixite.fdmnes | INFO ] R_self set to 3.5 for radius 5\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "! FDMNES input file generated by larixite\n", + "! 2026-05-12 10:56:09\n", + "! larixite : 2025.5.1\n", + "! pymatgen : 2025.6.14\n", + "Header\n", + "Python\n", + "Comment\n", + " ZnO_wurtzite_Weber1923_COD-1011259.cif: Zn (30) K edge\n", + "Filout\n", + " job\n", + "Edge\n", + " K\n", + "!\n", + "Range\n", + " -5 0.5 10\n", + "! Energpho !output energy as photon energy (default relative to Fermi level)\n", + "Radius\n", + " 5.00\n", + "!\n", + "Green\n", + "!\n", + "Quadrupole\n", + "!\n", + "! Polarize \n", + "! =>TODO\n", + "! TDDFT\n", + "Memory_save\n", + "! Relativism\n", + "! Spinorbit\n", + "! Density\n", + "! Density_all\n", + "!\n", + "! Hedin !PBE96 is default since March 2026\n", + "!\n", + "! SpGr_atom !`Full_atom` is default since March 2026\n", + "!\n", + "! SCF\n", + "R_self\n", + " 3.5\n", + "N_self\n", + " 100\n", + "P_self\n", + " 0.025\n", + "! SCF_exc\n", + "! Vmax\n", + "! Atom_conf\n", + "!\n", + "Z_absorber\n", + " 30\n", + "Spgroup\n", + " 186\n", + "Occupancy\n", + "Crystal\n", + " 3.351 3.351 5.226 90.0 90.0 119.99999999999999\n", + " 30 0.3333333333 0.6666666667 0.0000000000 1.000 ! Zn1 2b Zn2+\n", + " 8 0.3333333333 0.6666666667 0.3750000000 1.000 ! O1 2b O2-\n", + "!\n", + "Convolution\n", + "End\n", + "\n" + ] + } + ], + "source": [ + "f = FdmnesXasInput(sg, absorber=abs, struct_type=\"crystal\", radius=5, erange=\"-5 0.5 10\", optimize=True)\n", + "text = f.get_input()\n", + "print(text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "write the FDMNES input file and run FDMNES" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32;21m[larixite.fdmnes | INFO ] written `/tmp/larixite/fdmnes/ZnO_wurtzite_Weber1923_COD-1011259.cif/job_88xkt1ta/job.inp`\u001b[0m\n" + ] + } + ], + "source": [ + "outdir = f.write_input()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " FDMNES program, Revision 3rd of October 2025 \n", + " Date = 12 05 2026 \n", + " Time = 08 h 56 mn 34 s \n", + "\n", + " python \n", + " edge \n", + " range \n", + " radius \n", + " green \n", + " quadrupol\n", + " memory_sa\n", + " r_self \n", + " n_self \n", + " p_self \n", + " z_absorbe\n", + " spgroup \n", + " occupancy\n", + " crystal \n", + "\n", + " Filout: job\n", + " Threshold: Zinc K1 edge\n", + "\n", + " Sequential calculation\n", + "\n", + " Number of calculated non equivalent absorbing atom = 1\n", + "\n", + " E_edge = 9659.00 eV\n", + "\n", + " Cluster radius = 5.00 A, nb. of atom = 42\n", + " Fermi energy calculation : cluster radius = 3.50 A, nb. of atom = 18\n", + " Potential sup calculation: cluster radius = 7.50 A, nb. of atom = 144\n", + "\n", + " Point group : 3m (C3v )\n", + "\n", + " Point group used : 3 (C3 )\n", + "\n", + " ia Z mult ch_val ch_core ch_total ch_out Atom charge\n", + " 1 30 1 11.520 16.998 28.518 0.001 1.482\n", + " 2 8 1 5.374 2.000 7.374 0.004 0.626\n", + " 3 8 3 5.372 2.000 7.372 0.003 0.628\n", + " 4 30 3 10.478 17.992 28.470 0.140 1.530\n", + " 5 30 3 10.478 17.992 28.470 0.140 1.530\n", + " 6 8 1 5.359 2.000 7.359 0.094 0.641\n", + " 7 30 3 10.477 17.992 28.469 0.172 1.531\n", + " 8 30 3 10.477 17.992 28.469 0.172 1.531\n", + "\n", + " Cycle 1, Fermi energy = -7.401 eV, Occupancy val abs = 10.187\n", + "\n", + " Cycle 1: Total energy = -641497.9 eV,\n", + "\n", + " ia Z Energy_Tot Charge Ch-Ch_i pop_orb_val(L) L Radius\n", + " 1 30 -48748.1 1.988 0.506 10.187 2 1.19147\n", + " 2 8 -2102.7 -0.206 -0.832 4.370 1 0.96556\n", + " 3 8 -2101.3 -0.196 -0.824 4.361 1 0.96556\n", + " 4 30 -48530.7 1.826 0.296 9.657 2 1.09321\n", + " 5 30 -48511.3 1.870 0.339 9.627 2 1.09321\n", + " 6 8 -2109.0 -0.309 -0.950 4.386 1 0.96556\n", + " 7 30 -48518.0 1.838 0.307 9.624 2 1.09321\n", + " 8 30 -48518.0 1.838 0.307 9.624 2 1.09321\n", + "\n", + " ia Z it igr ipr iap posx posy posz igrpt PtGrName Comp Axe Mag\n", + " 1 30 0 1 0 1 0.00000 0.00000 0.00000 16 3 T T F\n", + " 2 8 2 3 2 2 0.00000 0.00000 1.95975 16 3 T T F\n", + " 3 8 2 4 2 5 0.00000 1.93470 -0.65325 1 1 T F F\n", + " 4 30 1 2 1 9 0.00000 1.93470 -2.61300 1 1 T F F\n", + " 5 30 1 2 1 11 0.00000 1.93470 2.61300 1 1 T F F\n", + " 6 8 2 3 2 12 0.00000 0.00000 -3.26625 16 3 T T F\n", + " 7 30 1 1 1 17 3.35100 0.00000 0.00000 1 1 T F F\n", + " 8 30 1 1 1 18 1.67550 2.90205 0.00000 1 1 T F F\n", + " 9 8 2 3 2 22 3.35100 0.00000 1.95975 1 1 T F F\n", + " 10 8 2 3 2 24 1.67550 2.90205 1.95975 1 1 T F F\n", + " 11 8 2 4 2 25 3.35100 1.93470 -0.65325 1 1 T F F\n", + " 12 30 1 2 1 29 3.35100 1.93470 -2.61300 1 1 T F F\n", + " 13 30 1 2 1 32 3.35100 1.93470 2.61300 1 1 T F F\n", + " 14 8 2 3 2 35 1.67550 2.90205 -3.26625 1 1 T F F\n", + " 15 8 2 3 2 37 3.35100 0.00000 -3.26625 1 1 T F F\n", + " 16 8 2 4 2 42 0.00000 1.93470 4.57275 1 1 T F F\n", + "\n", + " V0bdcF = -12.49955 eV, Vhbdc = -3.671 eV, rsbdc = 2.194 a.u.\n", + "\n", + " ipr Z Atom-Charge Atom-Radius Ionic-Charge Ionic-Radius Vmft\n", + " 0 30* 1.482 1.191 3.825 0.740 -18.328\n", + " 1 30 1.530 1.093 3.518 0.740 -18.328\n", + " 2 8 0.626 0.966 -1.318 1.400 -18.328\n", + "Epsii used = 9497.266 eV\n", + " Number of Energies = 31\n", + " Energy \n", + " -5.00000 2.6106000E-02\n", + " -4.50000 1.4966086E-04\n", + " -4.00000 4.1089473E-03\n", + " -3.50000 2.6891110E-02\n", + " -3.00000 8.4292987E-03\n", + " -2.50000 1.0047078E-03\n", + " -2.00000 6.6510232E-04\n", + " -1.50000 7.4967038E-04\n", + " -1.00000 8.9585450E-04\n", + " -0.50000 1.0778311E-03\n", + " 0.00000 1.3036424E-03\n", + " 0.50000 1.6056663E-03\n", + " 1.00000 2.0341465E-03\n", + " 1.50000 2.6863665E-03\n", + " 2.00000 4.0265447E-03\n", + " 2.50000 7.2399656E-03\n", + " 3.00000 1.4748046E-02\n", + " 3.50000 2.8747562E-02\n", + " 4.00000 3.3309915E-02\n", + " 4.50000 2.6993936E-02\n", + " 5.00000 3.3241095E-02\n", + " 5.50000 3.5329115E-02\n", + " 6.00000 3.2082621E-02\n", + " 6.50000 3.2082223E-02\n", + " 7.00000 3.6461065E-02\n", + " 7.50000 4.4630562E-02\n", + " 8.00000 5.8971353E-02\n", + " 8.50000 7.4217152E-02\n", + " 9.00000 8.8816372E-02\n", + " 9.50000 1.0986046E-01\n", + " 10.0000 1.2665657E-01\n", + "\n", + " Arctangent model\n", + " Gamma_max = 15.00, Ecent = 30.00, Elarg = 30.00\n", + " Gamma_hole = 1.67, E_cut = -2.000, Shift = 0.000 eV\n", + " E_(eV) Width_(eV) lambda_(A)\n", + " -5.000 1.670 0.000\n", + " -2.000 1.670 0.000\n", + " -1.500 1.673 2923.701\n", + " -1.000 1.680 780.025\n", + " -0.500 1.693 367.185\n", + " 0.000 1.711 217.447\n", + " 1.000 1.761 105.634\n", + " 2.000 1.832 64.037\n", + " 3.000 1.924 43.703\n", + " 4.000 2.037 32.095\n", + " 5.000 2.171 24.769\n", + " 6.000 2.327 19.810\n", + " 7.000 2.505 16.274\n", + " 8.000 2.706 13.653\n", + " 9.000 2.930 11.650\n", + " 10.000 3.177 10.080\n" + ] + } + ], + "source": [ + "! module purge && module load fdmnes && cd {outdir.as_posix()} && {fdmnes_executable}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following steps will be showed in the next version of Larixite:\n", + "\n", + "- load the FDMNES output files\n", + "- plot the XAS spectrum\n", + "- compare the simulated spectrum with experimental data and adjust the convolution parameters\n", + "- run multiple jobs for convergence studies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "sloth2507", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/workflow_fdmnes.ipynb b/notebooks/workflow_fdmnes.ipynb deleted file mode 100644 index 7a08865..0000000 --- a/notebooks/workflow_fdmnes.ipynb +++ /dev/null @@ -1,202 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# FDMNES XAS simulation workflow with Larixite\n", - "This notebook shows an example workflow for FDMNES XAS simulation using Larixite.\n", - "\n", - "- Author: Mauro Rovezzi\n", - "- Contact: mauro.rovezzi@esrf.fr\n", - "- Status: in progress\n", - "- Last update: 2025-05-14" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First import the necessary modules and set some variables" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%reload_ext autoreload\n", - "%autoreload 2\n", - "\n", - "from pathlib import Path\n", - "import numpy as np\n", - "import crystal_toolkit\n", - "import warnings\n", - "from larixite.struct import get_structs_from_dir\n", - "from larixite.fdmnes import FdmnesXasInput\n", - "from larixite.fdmnes import logger\n", - "\n", - "logger.setLevel(\"INFO\") #: adjust this level if you want to have more or less information: \"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"\n", - "warnings.simplefilter('always', UserWarning) #: to filter out some technic warnings from underlying pymatgen\n", - "\n", - "#: set a directory containing the structural files (TODO: set your own path)\n", - "curdir = Path().cwd()\n", - "basedir = curdir.parent\n", - "testdir = basedir / \"tests\"\n", - "structsdir = testdir / \"structs\"\n", - "\n", - "abs = \"Zn\" #: select the absorber\n", - "\n", - "fdmnes_executable = \"/home/mauro/local/fdmnes_Linux_2024_11_14/fdmnes_Linux/fdmnes_linux64\" #: path to fdmnes executable (TODO: set your own path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Get a list of XasStructure objects from a directory containing structural files, based on the absorbing element specified before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "structs = get_structs_from_dir(structsdir, abs, globstr=f\"*{abs}*\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "select one structure and print some information about it" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sg = structs[0]\n", - "sg.show_unique_sites()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "the structure can be visualized with the [Crystal Toolkit](https://docs.crystaltoolkit.org/)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sg.struct" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The absorber site is set by default to the first site with the absorbing element, but can be changed by indicating the index in the structure (`istruct` given by the `show_unique_sites()` method) to the `absorber_idx` attribute. It is convenient to check with the `show_unique_sites()` method that the index of the absorbing site is correctly set, as it gets the `(abs)` label next to the index." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sg.absorber_idx = 0\n", - "sg.show_unique_sites()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "then generate an FDMNES input file FdmnesXasInput object and show it" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f = FdmnesXasInput(sg, absorber=abs, struct_type=\"crystal\", radius=5)\n", - "text = f.get_input()\n", - "print(text)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "write the FDMNES input file and run FDMNES" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "outdir = f.write_input()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "! cd {outdir.as_posix()} && {fdmnes_executable}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following steps will be showed in the next version of Larixite:\n", - "\n", - "- load the FDMNES output files\n", - "- plot the XAS spectrum\n", - "- compare the simulated spectrum with experimental data and adjust the convolution parameters\n", - "- run multiple jobs for convergence studies" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "sloth2411", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 8d778e4cf3c52082564508b978e534fe35a8043c Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 12:05:47 +0200 Subject: [PATCH 10/46] migrate struct2xas examples --- examples/struct2xas_to_remove.ipynb | 828 ++++++++++++++++++++++++++++ larixite/struct2xas.py | 2 +- tests/structs/GaBr_single_frame.xyz | 21 + tests/structs/ZnO_mp-2133.cif | 30 + tests/structs/ZnO_mp-997630.cif | 50 ++ 5 files changed, 930 insertions(+), 1 deletion(-) create mode 100755 examples/struct2xas_to_remove.ipynb create mode 100755 tests/structs/GaBr_single_frame.xyz create mode 100644 tests/structs/ZnO_mp-2133.cif create mode 100644 tests/structs/ZnO_mp-997630.cif diff --git a/examples/struct2xas_to_remove.ipynb b/examples/struct2xas_to_remove.ipynb new file mode 100755 index 0000000..4c8e4c9 --- /dev/null +++ b/examples/struct2xas_to_remove.ipynb @@ -0,0 +1,828 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Struct2XAS examples: from CIF/XYZ to XANES/EXAFS simulations with FDMNES and FEFF\n", + "\n", + "- Authors: Beatriz G. Foschiani and Mauro Rovezzi\n", + "- Contact: mauro.rovezzi@esrf.fr\n", + "- Status: **deprecated** -> migration in progress (not all the examples below may work)\n", + "- Last modified: 2026-05-12\n", + "\n", + "Here we provide various examples on the usage of `larixite.struct2xas` module.\n", + "\n", + "**WARNING**: this module will be removed in future versions. Use `larixite.struct` and `larixite.fdmnes` instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "import larch" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: Zn K-edge XANES of wurtzite ZnO with FDMNES from a CIF file\n", + "\n", + "This example shows how to use the `Struct2XAS` class to convert a CIF file to a FDMNES XANES input. We will simulate Zn K-edge XAS of ZnO. \n", + "\n", + "- The input structure is taken from the Materials Project database: [mp-2133](https://legacy.materialsproject.org/materials/mp-2133/).\n", + "- The experimental data are taken from the SSHADE/FAME database: [DOI:10.26302/SSHADE/EXPERIMENT_ST_20180418_001](https://www.sshade.eu/data/EXPERIMENT_ST_20180418_001)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the main class and instantiate it with the CIF file and the name of the absorbing element" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from larixite.struct2xas import Struct2XAS\n", + "mat_obj = Struct2XAS(file = \"../tests/structs/ZnO_mp-2133.cif\", abs_atom=\"Zn\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to get the information about absorbing site" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# information about absorbing site as pandas.DataFrame\n", + "mat_obj.get_abs_sites_info()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the same, more programmatically" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.get_abs_sites()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Getters and setters are available for changing the absorber site being considered. We can use the methods `set_abs_site` and `get_abs_site`. This structure has only one absorber site (an example with multiple sites is given later)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.set_abs_site(0)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to get information on the coordination environment around absorber atom" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.get_coord_envs_info()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or programmatically" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.get_coord_envs()\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is possible to visualize the local environment at a ginen radius from the absorber" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.visualize(radius=2.5)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To create the FDMNES input file using the default template in a temporary directory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mytemplate = None #: uses default FDMNES XANES template (-> `larch/xrd/templates/fdmnes.tmpl`)\n", + "mypath = None #: creates a default structure -> \"mydir/fdmnes/input_structure/abs_atom/siteN/\"\n", + "mat_obj.make_input_fdmnes(radius=7, green=False, template=mytemplate, parent_path=mypath, newjob=\"test\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To show the created input file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! cat {mat_obj.outdir}/job_inp.txt" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run FDMNES (refer to FDMNES documentation if needed)\n", + "\n", + "```python\n", + "#Example how to run FDMNES at the ESRF via SLURM \n", + "input = mat_ojb.parent_path\n", + "! cd {input}; subfdmnes -c 30\n", + "```\n", + "\n", + "The output files created by FDMNES will be:\n", + "- `job.txt`: non-convoluted spectra\n", + "- `job_conv.txt`: convoluted spectra (by fdmnes with default parameters)\n", + "- `job_out.txt`: FDMNES output during the execution of the program (NOTE: this is created by the SLURM submission script)\n", + "- `job_sd0.txt`: file with the projected density of states information\n", + "- `job_bav.txt`: file with large simulation infos (better to compress it or remove)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Please, refer to [XANES_Convolution.ipynb](./XANES_Convolution.ipynb) for a comparison of the simulated data with experimental one." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1a: Zn K-edge EXAFS with FEFF\n", + "\n", + "We can also simulate the EXAFS spectrum with FEFF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mytemplate = None #: uses default FEFF EXAFS template\n", + "mypath = mat_obj.parent_path #: if None creates a default structure -> \"mydir/feff/input_structure/abs_atom/frameN/siteN/\"\n", + "mat_obj.make_input_feff(radius=7, template=mytemplate, parent_path=mypath)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to show the input file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! cat {mat_obj.outdir}/feff.inp" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "run FEFF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#via larch\n", + "from larch.xafs import FeffRunner\n", + "feff_inp = f\"{mat_obj.outdir}/feff.inp\"\n", + "sim = FeffRunner(feff_inp)\n", + "sim.run()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: can also be run via direct call to `feff8l`\n", + "\n", + "```\n", + "! cd {mat_obj.parent_path}; feff8l\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "load the simulated EXAFS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from glob import glob\n", + "xmu = glob(f\"{mat_obj.outdir}/xmu.dat\")[0]\n", + "from larch.io import read_ascii\n", + "gsim = read_ascii(xmu, labels=[\"energy\", \"erel\", \"k\", \"mu\", \"mu0\", \"chi\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Load experimental data and quickly extract the EXAFS signal" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "example_fn = os.path.join('..', \"fdmnes\", \"ZnO_SSHADE.data.txt\")\n", + "gexp = read_ascii(example_fn, labels=[\"energy\", \"mu\"])\n", + "from larch.xafs import pre_edge, autobk\n", + "pre_edge(gexp, e0=9661)\n", + "autobk(gexp)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "then plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from larch.plot.plotly_xafsplots import PlotlyFigure\n", + "fig = PlotlyFigure()\n", + "fig.add_plot(gsim.k+0.3, gsim.chi*gsim.k**2, label=\"FEFF simulation\")\n", + "fig.add_plot(gexp.k, gexp.chi*gexp.k**2, label=\"EXP data\")\n", + "\n", + "fig.set_style(title=\"Wurtzite ZnO EXAFS\", width=800, height=500, xaxis_title=\"k (Å^-1)\", yaxis_title=\"chi(k) * k^2\" )\n", + "fig.set_xrange(0, 10)\n", + "fig.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Note: the agreement is not very good because the experimental EXAFS data stops at 8 $\\AA^{-1}$ and the structure parameters are not optimized.*" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: structures with multiple sites\n", + "\n", + "Here an example when the input structure has multiple sites for a given absorbing element. The number of non-equivalent sites is found via the [SpaceGroupAnalyzer](https://pymatgen.org/pymatgen.symmetry.analyzer.html#pymatgen.symmetry.analyzer.SpacegroupAnalyzer) in Pymatgen, which is based on [Spglib](https://spglib.readthedocs.io/en/latest/).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from larch.xrd.struct2xas import Struct2XAS\n", + "cif_input_fn = Path(\"../tests/structs/ZnO_mp-997630.cif\")\n", + "mat_obj = Struct2XAS(file=cif_input_fn, abs_atom=\"Zn\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mat_obj.get_abs_sites_info()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "it is possible to select one site and get information on the local environment and visualize, as shown before" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# analysis for second site\n", + "mat_obj.set_abs_site(2)\n", + "print(mat_obj.get_coord_envs_info())\n", + "mat_obj.visualize()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "to create the FDMNES inputs simply range over the sites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mypath = None\n", + "for site in range(mat_obj.nabs_sites): \n", + " mat_obj.set_abs_site(site)\n", + " mat_obj.make_input_fdmnes(radius=7, green=False, parent_path=mypath)\n", + " mypath = mat_obj.parent_path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and the FEFF inputs too" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for site in range(mat_obj.nabs_sites): \n", + " mat_obj.set_abs_site(site)\n", + " mat_obj.make_input_feff(radius=7, parent_path=mypath)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 3: grab CIFs from Materials Project\n", + "\n", + "In this example some CIFs files are grabbed from the Materials Project database (MP) via a query and the inputs files are generated.\n", + "\n", + "A personal API key is required to query the database (-> [here](https://legacy.materialsproject.org/open). *NOTE* the legacy version is used in this example, that is, the \"old\" version of MP. Export `your_api_key` to the environmental variable `MP_API_KEY`.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from mp_api.client import MPRester\n", + "from larch.xrd.struct2xas import Struct2XAS, save_mp_structure, structure_folders\n", + "\n", + "api_key = os.getenv(\"MP_API_KEY\")\n", + "mpr = MPRester(api_key)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "we search for materials containing 2 elements, Zn and O, plus further filter those IDs not present in the ICSD database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# criterias to query the materials\n", + "\n", + "entries = mpr.summary.search(elements=[\"Zn\", \"O\"], num_elements=[2, None])\n", + "\n", + "#: skip materials not present in the ICSD database\n", + "mp_ids = [e.material_id for e in entries if 'icsd' in e.database_IDs]\n", + " \n", + "print(f\"Found {len(mp_ids)} structures\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "save the corresponding Structure files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for id in mp_ids[:10]:\n", + " struct_fname = save_mp_structure(api_key, id)\n", + " print(f\"saved {id} to {struct_fname}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "load the CIF files and generate the FDMNES/FEFF inputs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from glob import glob\n", + "from larch.xrd.struct2xas import structure_folders\n", + "sfolder = structure_folders()['mp_structs']\n", + "print(sfolder)\n", + "for sdat in glob(f\"{sfolder}/*.mpjson\"):\n", + " s2x = Struct2XAS(sdat, abs_atom=\"Zn\")\n", + " for abs_site in s2x.get_abs_sites():\n", + " s2x.set_abs_site(abs_site[0])\n", + " s2x.make_input_fdmnes(radius=7)\n", + " s2x.make_input_feff(radius=7)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "the next steps consist in running the simulations and comparing with experimental data. This is beyond the scope of current example." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 4: from XYZ (single or multi-frame) to XAS\n", + "\n", + "This example shows the case of XYZ files as input. In particular, in the case the XYZ file contains multiple frames. This is a typical output format of molecular dynamics simulations.\n", + "\n", + "Here the case of Ga K-edge of a GaBr solution is taken as example:\n", + "- Cécile Da Silva, Olivier Proux, Jean-Louis Hazemann, Julianne James-Smith, Denis Testemale, Toshio Yamaguchi,\n", + "X-ray absorption spectroscopy study of solvation and ion-pairing in aqueous gallium bromide solutions at supercritical conditions,\n", + "Journal of Molecular Liquids, Volume 147, Issues 1–2, 2009, Pages 83-95, ISSN 0167-7322, [https://doi.org/10.1016/j.molliq.2008.06.022]\n", + "- The experimental data are taken from the SSHADE database [DOI:10.26302/SSHADE/EXPERIMENT_ST_20180418_001](https://www.sshade.eu/data/EXPERIMENT_ST_20180418_001)\n", + "- The input structure is manually generated by simply changing the Ga-O distance\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from larch.xrd.struct2xas import Struct2XAS\n", + "gabr = Struct2XAS(file=\"../tests/structs/GaBr_multi-frame.xyz\", abs_atom=\"Ga\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "it is possible to select a given frame, visualize and get the coordination environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gabr.set_frame(2)\n", + "gabr.visualize(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gabr.get_coord_envs_info()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "generate FEFF EXAFS inputs (for FDMNES is equivalent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for frame in range(gabr.nframes):\n", + " gabr.set_frame(frame)\n", + " gabr.make_input_feff(radius=5, sig2=0.005)\n", + "outdir = os.path.abspath(os.path.join(gabr.outdir, '..', '..'))\n", + "print(outdir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "run the simulations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from glob import glob\n", + "from larch.xafs import FeffRunner\n", + "\n", + "feffinps = glob(f\"{outdir}/**/feff.inp\", recursive=True)\n", + "print(feffinps)\n", + "for feffinp in feffinps:\n", + " sim = FeffRunner(feffinp)\n", + " sim.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "sometimes is useful to export all the `xmu.dat` files in a single HDF5 file container" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import h5py\n", + "from larch.io import read_ascii\n", + "\n", + "h5dir = os.path.join(outdir, \"hdf5\")\n", + "try:\n", + " os.makedirs(h5dir)\n", + "except FileExistsError:\n", + " pass\n", + "h5out = os.path.join(h5dir, \"gabr_exafs.h5\")\n", + "\n", + "xmus = glob(f\"{outdir}/**/xmu.dat\", recursive=True)\n", + "labels = [\"energy\", \"erel\", \"k\", \"mu\", \"mu0\", \"chi\"]\n", + "\n", + "with h5py.File(h5out, \"w\") as f:\n", + " for xmu in xmus:\n", + " gxmu = read_ascii(xmu, labels=labels)\n", + " sample, abs_at, framestr, sitestr = xmu.split(os.sep)[-5:-1] \n", + " grouppath = f\"/{sample}/{abs_at}/{framestr}/{sitestr}\"\n", + " print(f\"save to group {grouppath}\")\n", + " try:\n", + " f.create_group(grouppath)\n", + " except Exception:\n", + " pass\n", + " for lab in labels:\n", + " f.create_dataset(f\"{grouppath}/{lab}\", data=getattr(gxmu, lab))\n", + "print(f\"xmu data exported to HDF5: {h5out}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from larch.xafs import pre_edge, autobk\n", + "from larch.plot.plotly_xafsplots import PlotlyFigure, plotlabels\n", + "\n", + "fig = PlotlyFigure()\n", + "\n", + "xmus = glob(f\"{outdir}/**/xmu.dat\", recursive=True)\n", + "for xmu in xmus:\n", + " sample, abs_at, framestr, sitestr = xmu.split(os.sep)[-5:-1] \n", + " gxmu = read_ascii(xmu, labels=[\"energy\", \"erel\", \"k\", \"mu\", \"mu0\", \"chi\"])\n", + " fig.add_plot(x=gxmu.k, y=gxmu.chi*gxmu.k**2, label=f\"{sample}/{framestr}\")\n", + "\n", + "#EXP DATA\n", + "fn_in = os.path.join('..', \"structuredata\", \"struct2xas\", \"GaBr_GaK_SSHADE.data.txt\")\n", + "gexp = read_ascii(fn_in, labels=[\"energy\", \"mu\"])\n", + "pre_edge(gexp)\n", + "autobk(gexp)\n", + "fig.add_plot(x=gexp.k, y=gexp.chi*gexp.k**2, color=\"black\", label=\"exp data\")\n", + "\n", + "\n", + "\n", + "fig.set_style(title=\"GaBr (Ga K-edge): FEFF sim. vs exp.\", width=900, height=500, xaxis_title=plotlabels.k, yaxis_title=plotlabels.chi2)\n", + "fig.set_xrange(2, 14)\n", + "fig.set_yrange(-3, 3)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with h5py.File(h5out, \"r\") as f:\n", + " print(list(f.keys()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "sloth2507", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index df342d3..73b8c26 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -1086,7 +1086,7 @@ def make_input_feff( template = os.path.join( os.path.dirname(os.path.realpath(__file__)), "templates", - "feff_exafs.tmpl", + "feff_struct2xas_to_remove.tmpl", ) assert os.path.isfile(template), "wrong template path" diff --git a/tests/structs/GaBr_single_frame.xyz b/tests/structs/GaBr_single_frame.xyz new file mode 100755 index 0000000..ab6dbfb --- /dev/null +++ b/tests/structs/GaBr_single_frame.xyz @@ -0,0 +1,21 @@ +19 +Ga1 H12 O6 +Ga 0.000000 0.000000 0.000000 +O 1.960000 0.000000 0.000000 +O -1.960000 0.000000 0.000000 +O 0.000000 1.960000 0.000000 +O 0.000000 -1.960000 0.000000 +O 0.000000 0.000000 1.960000 +O 0.000000 0.000000 -1.960000 +H -0.791840 2.528400 0.000000 +H 0.791840 -2.528400 0.000000 +H 0.791840 2.528400 0.000000 +H -0.791840 -2.528400 0.000000 +H 0.000000 -0.791840 -2.528400 +H 0.000000 0.791840 -2.528400 +H 0.000000 -0.791840 2.528400 +H 0.000000 0.791840 2.528400 +H 2.528400 0.000000 0.791840 +H -2.528400 0.000000 -0.791840 +H -2.528400 0.000000 0.791840 +H 2.528400 0.000000 -0.791840 \ No newline at end of file diff --git a/tests/structs/ZnO_mp-2133.cif b/tests/structs/ZnO_mp-2133.cif new file mode 100644 index 0000000..32882b7 --- /dev/null +++ b/tests/structs/ZnO_mp-2133.cif @@ -0,0 +1,30 @@ +# generated using pymatgen +data_ZnO +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.28910296 +_cell_length_b 3.28910296 +_cell_length_c 5.30682100 +_cell_angle_alpha 90.00000000 +_cell_angle_beta 90.00000000 +_cell_angle_gamma 120.00001938 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural ZnO +_chemical_formula_sum 'Zn2 O2' +_cell_volume 49.71871825 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Zn Zn0 1 0.66666700 0.33333300 0.50054800 1 + Zn Zn1 1 0.33333300 0.66666700 0.00054800 1 + O O2 1 0.66666700 0.33333300 0.87976200 1 + O O3 1 0.33333300 0.66666700 0.37976200 1 diff --git a/tests/structs/ZnO_mp-997630.cif b/tests/structs/ZnO_mp-997630.cif new file mode 100644 index 0000000..0b206b3 --- /dev/null +++ b/tests/structs/ZnO_mp-997630.cif @@ -0,0 +1,50 @@ +# generated using pymatgen +data_ZnO +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.05547751 +_cell_length_b 3.05547751 +_cell_length_c 30.40991300 +_cell_angle_alpha 90.00000000 +_cell_angle_beta 90.00000000 +_cell_angle_gamma 119.99998934 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural ZnO +_chemical_formula_sum 'Zn12 O12' +_cell_volume 245.86914906 +_cell_formula_units_Z 12 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Zn Zn0 1 0.33333300 0.66666700 0.83493800 1 + Zn Zn1 1 0.66666700 0.33333300 0.16506200 1 + Zn Zn2 1 0.66666700 0.33333300 0.33493800 1 + Zn Zn3 1 0.33333300 0.66666700 0.66506200 1 + Zn Zn4 1 0.33333300 0.66666700 0.41747200 1 + Zn Zn5 1 0.66666700 0.33333300 0.58252800 1 + Zn Zn6 1 0.66666700 0.33333300 0.91747200 1 + Zn Zn7 1 0.33333300 0.66666700 0.08252800 1 + Zn Zn8 1 0.00000000 0.00000000 0.25000000 1 + Zn Zn9 1 0.00000000 0.00000000 0.75000000 1 + Zn Zn10 1 0.00000000 0.00000000 0.00000000 1 + Zn Zn11 1 0.00000000 0.00000000 0.50000000 1 + O O12 1 0.33333300 0.66666700 0.95873100 1 + O O13 1 0.66666700 0.33333300 0.04126900 1 + O O14 1 0.66666700 0.33333300 0.45873100 1 + O O15 1 0.33333300 0.66666700 0.54126900 1 + O O16 1 0.33333300 0.66666700 0.20615800 1 + O O17 1 0.66666700 0.33333300 0.79384200 1 + O O18 1 0.66666700 0.33333300 0.70615800 1 + O O19 1 0.33333300 0.66666700 0.29384200 1 + O O20 1 0.00000000 0.00000000 0.12375900 1 + O O21 1 0.00000000 0.00000000 0.87624100 1 + O O22 1 0.00000000 0.00000000 0.62375900 1 + O O23 1 0.00000000 0.00000000 0.37624100 1 From f21695c2385960ac89ed4d657da050c09b57d9c7 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 12:06:32 +0200 Subject: [PATCH 11/46] expose some parameters as attrs --- larixite/fdmnes.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 66df064..d4f9438 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -53,22 +53,24 @@ class FdmnesXasInput: str | Path | XasStructure ) #: str or path to the structural file, or directly the XasStructure object absorber: str | int | Element #: atomic symbol or number of the absorbing element - frame: int = 0 #: index of the frame inside the structure (e.g. for multi-frame XYZ files) edge: str | None = None #: edge for calculation - radius: float = 7 #: radius of the calculation struct_type: str | None = None #: type of the structure - vmax: float | None = None #: maximum potential value for molecules + frame: int = ( + 0 #: index of the frame inside the structure (e.g. for multi-frame XYZ files) + ) + radius: float = 7 #: radius of the calculation + erange: str = "-10.0 0.25 60.0 1.0 100.0" #: energy range rself: float | None = None #: radius for the SCF calculation nself: int = 100 #: number of maximum iterations for the SCF calculation pself: float = 0.025 #: initial weight for the SCF calculation - erange: str = "-10.0 0.25 60.0 1.0 100.0" #: energy range + vmax: float | None = None #: maximum potential value for molecules fileout_prefix: str = ( "job" #: prefix of the output filename for the FDMNES job (extension: .inp) ) tmplpath: str | Path | None = None #: path to the FDMNES input template params: dict[str, bool] | None = None #: enable/disable parameters for FDMNES optimize: bool = False #: optimize the input parameters - spacer: str = " " #: spacer for the FDMNES input text + spacer: str = " " #: spacer string for the FDMNES input text def __post_init__(self): """Validate and optimize attributes""" @@ -102,12 +104,29 @@ def __post_init__(self): self.tmplpath = Path(self.tmplpath) #: absorption edge self.validate_edge() - #: optimize params + #: parameters if self.params is None: self.params = FDMNES_DEFAULT_PARAMS + #: optimize params if self.optimize: self.params = self.optimize_params() + @property + def green(self): + return self.params["Green"] + + @green.setter + def green(self, val): + self.params["Green"] = val + + @property + def scf(self): + return self.params["SCF"] + + @scf.setter + def scf(self, val): + self.params["SCF"] = val + def set_radius(self, value: float): self.radius = value self._xs.radius = value From 61ca7add6e0055f83be7947b58f230fc6c1bb856 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 12:06:46 +0200 Subject: [PATCH 12/46] temporary template --- .../templates/feff_struct2xas_to_remove.tmpl | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 larixite/templates/feff_struct2xas_to_remove.tmpl diff --git a/larixite/templates/feff_struct2xas_to_remove.tmpl b/larixite/templates/feff_struct2xas_to_remove.tmpl new file mode 100755 index 0000000..6613221 --- /dev/null +++ b/larixite/templates/feff_struct2xas_to_remove.tmpl @@ -0,0 +1,42 @@ +* FEFF8* input file generated by Struct2XAS version {version} + +{title} + +* edge energy = eV +EDGE {edge} +S02 0.0 + +* pot xsph fms paths genfmt ff2chi +CONTROL 1 1 1 1 1 1 +PRINT {print_pot} {print_xsph} {print_fms} {print_paths} {print_genfmt} {print_ff2chi} + +*** ixc=0 means to use Hedin-Lundqvist +* ixc [ Vr Vi ] +EXCHANGE 0 + +* r_scf [ l_scf n_scf ca ] +*SCF 5.0 + +RPATH {radius} +EXAFS 20 +NLEG 6 + +* EXAFS damping, to mimic the structural disorder +{use_sig2} SIG2 {sig2} + +* Debye-Waller factors +{use_debye} DEBYE {temperature} {debye_temperature} + +* emin emax eimag +*LDOS -30 20 0.1 + + +POTENTIALS + +{potentials} + +ATOMS + +{atoms} + +END From a70f9feccf0e90e7103cf5795ec4ca2d0a499da4 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 12:09:08 +0200 Subject: [PATCH 13/46] reintroduce self.xs for backward compatibility --- larixite/fdmnes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index d4f9438..b81a185 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -111,6 +111,10 @@ def __post_init__(self): if self.optimize: self.params = self.optimize_params() + @property + def xs(self): + return self._xs + @property def green(self): return self.params["Green"] From 4ac1f5b1cd5651a82e84f3ee1a2bb56ec5681ce9 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 12:59:12 +0200 Subject: [PATCH 14/46] specify input tolerances for specegroup analyzer --- larixite/struct/xas_cif.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/larixite/struct/xas_cif.py b/larixite/struct/xas_cif.py index 7079284..4e1041b 100644 --- a/larixite/struct/xas_cif.py +++ b/larixite/struct/xas_cif.py @@ -22,12 +22,12 @@ def struct(self): @property def sga(self): - return SpacegroupAnalyzer(self.struct) + return SpacegroupAnalyzer(self.struct, symprec=0.01, angle_tolerance=5) @property def space_group(self): sd = self.sga.get_symmetry_dataset() - #spg = sd.international + # spg = sd.international spg = sd.number if sd.choice != "": return f"{spg}:{sd.choice}" From 649eb22a98081b0aa5036f52a6704c568c6ba675 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 13:38:43 +0200 Subject: [PATCH 15/46] add make_sbatch --- larixite/fdmnes.py | 50 +++++++++++++++++++--- larixite/templates/fdmnes_sbatch_esrf.tmpl | 31 ++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 larixite/templates/fdmnes_sbatch_esrf.tmpl diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index b81a185..fac3a65 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -67,7 +67,9 @@ class FdmnesXasInput: fileout_prefix: str = ( "job" #: prefix of the output filename for the FDMNES job (extension: .inp) ) - tmplpath: str | Path | None = None #: path to the FDMNES input template + tmplpath: str | Path | None = ( + None #: path to the templates directory (FDMNES inputs and sbatch) + ) params: dict[str, bool] | None = None #: enable/disable parameters for FDMNES optimize: bool = False #: optimize the input parameters spacer: str = " " #: spacer string for the FDMNES input text @@ -99,7 +101,7 @@ def __post_init__(self): self._xs.struct_type = self.struct_type #: template if self.tmplpath is None: - self.tmplpath = Path(TEMPLATE_FOLDER, "fdmnes_xas.tmpl") + self.tmplpath = Path(TEMPLATE_FOLDER) if isinstance(self.tmplpath, str): self.tmplpath = Path(self.tmplpath) #: absorption edge @@ -110,6 +112,8 @@ def __post_init__(self): #: optimize params if self.optimize: self.params = self.optimize_params() + #: set outdir to None before writing the input + self.outdir = None @property def xs(self): @@ -324,10 +328,19 @@ def get_rself(self): else: return "!" - def get_input(self, comment: str = "", struct_type: str = None) -> str: + def get_input( + self, + comment: str = "", + struct_type: str = None, + template: str | Path | None = None, + ) -> str: """Get the FDMNES input text""" params = self.params.copy() - template = open(self.tmplpath, "r").read() + if template is None: + template = self.tmplpath / "fdmnes_xas.tmpl" + if isinstance(template, str): + template = Path(template) + template_text = open(template, "r").read() comment = ( f"{self.spacer}{self._xs.name}: {self.absorber.symbol} ({self.absorber.Z}) {self.edge} edge" @@ -356,7 +369,7 @@ def get_input(self, comment: str = "", struct_type: str = None) -> str: for parkey, parval in params.items(): conf[parkey] = str(parkey) if parval is True else f"! {parkey}" - return strict_ascii(template.format(**conf)) + return strict_ascii(template_text.format(**conf)) def write_input( self, inputtext: str | None = None, outdir: str | Path | None = None @@ -382,8 +395,35 @@ def write_input( with open(outdir / "fdmfile.txt", "w") as fp: fp.write(f"1\n{fileout_name}\n") logger.info(f"written `{fnout}`") + self.outdir = outdir return outdir + def write_sbatch(self, template: str | Path | None = None, **kwargs): + """Generates a SBATCH file (SLURM workload manager) using a template + + Arguments + --------- + template: str, Path or None + path to the SBATCH template file + if None, the default template will be used (`fdmnes_sbatch_esrf.tmpl`) + **kwargs + keyword arguments to be replaced in the template file + + Returns + ------- + None: writes `{self.fileout_prefix}.sbatch` + + """ + if template is None: + template = self.tmplpath / "fdmnes_sbatch_esrf.tmpl" + if self.outdir is None: + logger.error("execute `write_input` first") + return + sbatchout = self.outdir / f"{self.fileout_prefix}.sbatch" + with open(sbatchout, "w") as fp, open(template) as tp: + fp.write(tp.read().format(**kwargs)) + logger.info(f"written {fp.name}") + def struct2fdmnes( inp: str | Path, diff --git a/larixite/templates/fdmnes_sbatch_esrf.tmpl b/larixite/templates/fdmnes_sbatch_esrf.tmpl new file mode 100644 index 0000000..ca1b655 --- /dev/null +++ b/larixite/templates/fdmnes_sbatch_esrf.tmpl @@ -0,0 +1,31 @@ +#!/bin/bash -l +#SBATCH --nodes=1 #: Number of nodes +#SBATCH --ntasks-per-node={ncpus} #: Total number of tasks (=CPUs cores) per node +#SBATCH --cpus-per-task=1 #: CPUs for each task +#SBATCH --mem-per-cpu=16G #: Total memory for each CPU +#SBATCH --time=48:00:00 #: Wall-time limit +#SBATCH --partition=nice,nice-long #: ESRF cluster +#SBATCH --constraint="hpc3|hpc5|hpc6|hpc7" #: ESRF cluster +#SBATCH --job-name="fdmnes" #: Name used to set the output files +#SBATCH --output=%x_output.txt +#SBATCH --error=%x_output.txt + +function compress {{ + gzip -f job_bav.txt +}} + +module purge + +#VERSION="default" #"module avail fdmnes" to see available versions +VERSION="2026.03.10" +if [ $VERSION = "default" ]; then + module load fdmnes +else + module load fdmnes/$VERSION +fi + +# Run the calculation. +srun ${{FDMNES_COMMAND}} fdmnes + +# Compress the large job_bav.txt file. +compress From 30050f9ddef0f33ac42232ffc22b4106f17ce49f Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 13:48:28 +0200 Subject: [PATCH 16/46] started CHANGELOG.md --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..04735fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +Notable changes to this project will be documented in this file. The GitHub +Release Notes will also be useful. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +The versioning scheme followed is `YYYY.MAJOR.MINOR`: + +- `YYYY`: year of release +- `MAJOR`: major feature (= change in API, no backward compatibility) +- `MINOR`: minor feature (= bug fixes, minor changes not affecting the API, backward compatible) + +## [2026.1.0 - not released yet] + +### `fdmnes` + +- Update to FDMNES version March 2026 +- Add `make_sbatch` with a template for the ESRF SLURM scheduler + +### `struc2xas` + +- Fully migrated from `larch.xrd.struc2xas` (and removed there) +- Make `struc2xas` *deprecated* and start refactoring \ No newline at end of file From 69bed3c3609f8a36d71bb94c967767058b9fbc20 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 14:43:29 +0200 Subject: [PATCH 17/46] add more tests --- tests/test_struct.py | 234 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 221 insertions(+), 13 deletions(-) diff --git a/tests/test_struct.py b/tests/test_struct.py index 6031cce..e534cd1 100644 --- a/tests/test_struct.py +++ b/tests/test_struct.py @@ -1,12 +1,39 @@ +"""Tests for larixite.struct — XasStructure, CIF/XYZ parsing, clusters, error handling.""" + +import tempfile from pathlib import Path -from larixite.struct import get_structure -from larixite.utils import get_logger -logger = get_logger("larixite.test") +import pytest +import numpy as np + +from larixite.struct import get_structure, get_structure_from_text, get_structs_from_dir +from larixite.struct.xas import site_label +from larixite.struct.xas_cif import XasStructureCif +from larixite.struct.xas_xyz import XasStructureXyz + +STRUCTS = Path(__file__).parent / "structs" + +# ── fixtures ────────────────────────────────────────────────────── + + +@pytest.fixture(scope="module") +def cif_zno(): + return get_structure(STRUCTS / "ZnO_mp-2133.cif", "Zn") + + +@pytest.fixture(scope="module") +def cif_fe3o4(): + return get_structure(STRUCTS / "Fe3O4_cub_fracOcc_Levy2012_ICSD-183969.cif", "Fe") -structsdir = Path(__file__).parent / "structs" -test_structures = ( # filename_in_structsdir, absorber_str, absorber_index, occupancy +@pytest.fixture(scope="module") +def xyz_cuo6(): + return get_structure(STRUCTS / "CuO6_D4h.xyz", "Cu") + + +# ── parametrized occupancy (legacy) ──────────────────────────── + +test_structures = ( ("CuO6_D4h.xyz", "Cu", 0, 1), ("VO6_Oh.xyz", "V", 0, 1), ("GaBr_multi-frame.xyz", "Ga", 0, 1), @@ -17,13 +44,194 @@ ) -def test_struct(): - for filename, abs, abs_index, occupancy in test_structures: - sg = get_structure(structsdir / filename, abs) - sg.absorber_idx = abs_index - s = sg.get_site(sg.absorber_idx) - assert sg.get_occupancy(s) == occupancy, "Wrong occupancy" +@pytest.mark.parametrize("filename,abs,abs_index,occupancy", test_structures) +def test_struct(filename, abs, abs_index, occupancy): + sg = get_structure(STRUCTS / filename, abs) + sg.absorber_idx = abs_index + s = sg.get_site(sg.absorber_idx) + assert sg.get_occupancy(s) == pytest.approx(occupancy, rel=0.02) + + +# ── CIF ──────────────────────────────────────────────────────── + + +class TestCif: + def test_properties(self, cif_zno): + assert isinstance(cif_zno, XasStructureCif) + assert cif_zno.struct_type == "crystal" + assert cif_zno.formula == "ZnO" + assert cif_zno.space_group == 186 + assert cif_zno.absorber_idx == 0 + + def test_symmetry(self, cif_zno): + assert cif_zno.sga is not None and cif_zno.sym_struct is not None + assert len(cif_zno.unique_sites) == 2 and len(cif_zno.equivalent_sites) == 2 + assert len(cif_zno.wyckoff_symbols) == 2 + assert all(isinstance(w, str) and w for w in cif_zno.wyckoff_symbols) + + def test_site_labels(self, cif_zno): + labels = cif_zno.site_labels + assert len(labels) == 4 and all("[" in l and "]" in l for l in labels) + + def test_absorber(self, cif_zno): + assert len(cif_zno.absorber_sites) >= 1 + idxes = cif_zno.get_absorber_indexes() + assert len(idxes) >= 1 and all("Zn" in cif_zno.get_site(i).species_string for i in idxes) + + def test_alt_absorber(self, cif_zno): + sg = get_structure(STRUCTS / "ZnO_mp-2133.cif", "O") + assert sg.absorber.symbol == "O" and len(sg.absorber_sites) >= 1 + + def test_full_occupancy(self, cif_zno): + assert cif_zno.get_occupancy(cif_zno.get_site(0)) == 1.0 + + def test_cluster(self, cif_zno): + from pymatgen.core import Molecule + cl = cif_zno.cluster + assert isinstance(cl, Molecule) and cl[0].species_string == "Zn" + assert np.allclose(cl[0].coords, [0, 0, 0], atol=0.01) + assert len(cl) > len(cif_zno.struct) + + def test_cluster_radius(self, cif_zno): + cif_zno.radius = 3 + small = cif_zno.cluster + cif_zno.radius = 7 + assert len(small) < len(cif_zno.cluster) + + +# ── XYZ ──────────────────────────────────────────────────────── + + +class TestXyz: + def test_properties(self, xyz_cuo6): + assert isinstance(xyz_cuo6, XasStructureXyz) + assert xyz_cuo6.struct_type == "molecule" and xyz_cuo6.space_group == "P1" + assert xyz_cuo6.absorber_idx == 0 + assert "Cu" in xyz_cuo6.formula and "O" in xyz_cuo6.formula + + def test_symmetry(self, xyz_cuo6): + assert all(w == "1a" for w in xyz_cuo6.wyckoff_symbols) + sites = xyz_cuo6.equivalent_sites + assert len(sites) == len(xyz_cuo6.struct) and all(len(g) == 1 for g in sites) + + def test_misc(self, xyz_cuo6): + assert len(xyz_cuo6.get_absorber_indexes()) >= 1 + assert xyz_cuo6.get_occupancy(xyz_cuo6.get_site(0)) == 1.0 + + def test_multi_frame(self): + for f in (0, 3): + sg = get_structure(STRUCTS / "GaBr_multi-frame.xyz", "Ga", frame=f) + assert isinstance(sg, XasStructureXyz) and sg.absorber.symbol == "Ga" + + def test_cluster(self, xyz_cuo6): + from pymatgen.core import Molecule + cl = xyz_cuo6.cluster + assert isinstance(cl, Molecule) and cl[0].species_string == "Cu" + assert all(hasattr(s, "label") for s in cl) + + +# ── absorber_idx setter ────────────────────────────────────── + + +class TestAbsorberIdx: + def test_valid(self, cif_zno): + cif_zno.absorber_idx = 0 + assert cif_zno.absorber_idx == 0 + + def test_out_of_range(self, cif_zno): + with pytest.raises(IndexError, match="not valid"): + cif_zno.absorber_idx = 999 + + def test_wrong_element(self, xyz_cuo6): + with pytest.raises(IndexError, match="not in site"): + xyz_cuo6.absorber_idx = 4 + + def test_absorber_site(self, xyz_cuo6): + assert "Cu" in xyz_cuo6.absorber_site.species_string + + def test_second_absorber_site(self, cif_fe3o4): + cif_fe3o4.absorber_idx = 1 + assert cif_fe3o4.absorber_idx == 16 + + +# ── errors ─────────────────────────────────────────────────── + +class TestErrors: + def test_nonexistent_file(self): + with pytest.raises(FileNotFoundError): + get_structure("/tmp/_no_such_file_12345.cif", "Zn") + + def test_bad_extension(self): + with tempfile.NamedTemporaryFile(suffix=".bad", delete=False) as f: + f.write(b"x"); fname = f.name + try: + with pytest.raises(ValueError, match="unknown structure format"): + get_structure(fname, "Zn") + finally: + Path(fname).unlink() + + +# ── get_structure_from_text ────────────────────────────────── + + +class TestFromText: + @pytest.fixture(scope="class") + def cif_text(cls): + return (STRUCTS / "ZnO_mp-2133.cif").read_text() + + @pytest.fixture(scope="class") + def xyz_text(cls): + return (STRUCTS / "CuO6_D4h.xyz").read_text() + + def test_cif(self, cif_text): + sg = get_structure_from_text(cif_text, "Zn", format="cif", filename="t.cif") + assert isinstance(sg, XasStructureCif) and sg.formula == "ZnO" + + def test_xyz(self, xyz_text): + sg = get_structure_from_text(xyz_text, "Cu", format="xyz", filename="t.xyz") + assert isinstance(sg, XasStructureXyz) and sg.struct_type == "molecule" + + def test_int_absorber(self, cif_text, xyz_text): + assert get_structure_from_text(cif_text, 30, format="cif", filename="t.cif").absorber.symbol == "Zn" + assert get_structure_from_text(xyz_text, 29, format="xyz", filename="t.xyz").absorber.symbol == "Cu" + + def test_bad_format(self, cif_text): + with pytest.raises(ValueError, match="unknown structure format"): + get_structure_from_text(cif_text, "Zn", format="pdb") + + +# ── get_structs_from_dir ──────────────────────────────────── + + +class TestFromDir: + def test_single(self): + s = get_structs_from_dir(STRUCTS, "Fe", globstr="Fe3O4*") + assert len(s) == 1 and s[0].name == "Fe3O4_cub_fracOcc_Levy2012_ICSD-183969.cif" + + def test_cif_glob(self): + assert all(isinstance(x, XasStructureCif) + for x in get_structs_from_dir(STRUCTS, "Zn", globstr="ZnO*")) + + def test_xyz_glob(self): + assert all(isinstance(x, XasStructureXyz) + for x in get_structs_from_dir(STRUCTS, "Cu", globstr="CuO6*")) + + def test_exclude(self): + s = get_structs_from_dir(STRUCTS, "Ga", globstr="GaBr*", + exclude_names=["GaBr_single_frame.xyz"]) + assert any(x.name == "GaBr_multi-frame.xyz" for x in s) + assert not any(x.name == "GaBr_single_frame.xyz" for x in s) + + +# ── site_label ────────────────────────────────────────────── + +class TestSiteLabel: + def test_format(self, cif_zno, xyz_cuo6): + lbl = site_label(cif_zno.struct[0]) + assert "[" in lbl and "]" in lbl and "," in lbl + assert site_label(xyz_cuo6.struct[0]).startswith("Cu") -if __name__ == "__main__": - test_struct() + def test_species_included(self, cif_zno): + for site in cif_zno.struct: + assert site.species_string.split(":")[0] in site_label(site) \ No newline at end of file From e5097ef5a7d607a4b5d76aa8680bb596a971b85c Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 15:01:22 +0200 Subject: [PATCH 18/46] lower set_rself logging to debug --- larixite/fdmnes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index fac3a65..ef50c77 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -144,7 +144,7 @@ def set_rself(self): self.rself = self.radius else: self.rself = 3.5 - logger.info(f"R_self set to {self.rself} for radius {self.radius}") + logger.debug(f"R_self set to {self.rself} for radius {self.radius}") def validate_edge(self): """Validates and adjusts the edge attribute""" From b15d775151461d6fa636f34080850e907f077045 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 15:34:03 +0200 Subject: [PATCH 19/46] catch pymatgen warnings --- tests/test_struct.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_struct.py b/tests/test_struct.py index e534cd1..a6c139a 100644 --- a/tests/test_struct.py +++ b/tests/test_struct.py @@ -18,7 +18,8 @@ @pytest.fixture(scope="module") def cif_zno(): - return get_structure(STRUCTS / "ZnO_mp-2133.cif", "Zn") + with pytest.warns(UserWarning, match="fractional coordinates rounded"): + return get_structure(STRUCTS / "ZnO_mp-2133.cif", "Zn") @pytest.fixture(scope="module") @@ -79,7 +80,8 @@ def test_absorber(self, cif_zno): assert len(idxes) >= 1 and all("Zn" in cif_zno.get_site(i).species_string for i in idxes) def test_alt_absorber(self, cif_zno): - sg = get_structure(STRUCTS / "ZnO_mp-2133.cif", "O") + with pytest.warns(UserWarning, match="fractional coordinates rounded"): + sg = get_structure(STRUCTS / "ZnO_mp-2133.cif", "O") assert sg.absorber.symbol == "O" and len(sg.absorber_sites) >= 1 def test_full_occupancy(self, cif_zno): @@ -184,7 +186,8 @@ def xyz_text(cls): return (STRUCTS / "CuO6_D4h.xyz").read_text() def test_cif(self, cif_text): - sg = get_structure_from_text(cif_text, "Zn", format="cif", filename="t.cif") + with pytest.warns(UserWarning, match="fractional coordinates rounded"): + sg = get_structure_from_text(cif_text, "Zn", format="cif", filename="t.cif") assert isinstance(sg, XasStructureCif) and sg.formula == "ZnO" def test_xyz(self, xyz_text): @@ -192,7 +195,10 @@ def test_xyz(self, xyz_text): assert isinstance(sg, XasStructureXyz) and sg.struct_type == "molecule" def test_int_absorber(self, cif_text, xyz_text): - assert get_structure_from_text(cif_text, 30, format="cif", filename="t.cif").absorber.symbol == "Zn" + # CIF triggers pymatgen rounding warning + with pytest.warns(UserWarning, match="fractional coordinates rounded"): + result_cif = get_structure_from_text(cif_text, 30, format="cif", filename="t.cif") + assert result_cif.absorber.symbol == "Zn" assert get_structure_from_text(xyz_text, 29, format="xyz", filename="t.xyz").absorber.symbol == "Cu" def test_bad_format(self, cif_text): @@ -209,8 +215,9 @@ def test_single(self): assert len(s) == 1 and s[0].name == "Fe3O4_cub_fracOcc_Levy2012_ICSD-183969.cif" def test_cif_glob(self): - assert all(isinstance(x, XasStructureCif) - for x in get_structs_from_dir(STRUCTS, "Zn", globstr="ZnO*")) + with pytest.warns(UserWarning, match="fractional coordinates rounded"): + structs = get_structs_from_dir(STRUCTS, "Zn", globstr="ZnO*") + assert all(isinstance(x, XasStructureCif) for x in structs) def test_xyz_glob(self): assert all(isinstance(x, XasStructureXyz) From c57f79d1ec58915831536497d697d7e64d3a9ee9 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 15:54:07 +0200 Subject: [PATCH 20/46] suppress pymatgen warnings during tests --- tests/conftest.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1822b3a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +"""Shared pytest configuration for the larixite test suite.""" + +import warnings + + +def pytest_configure(config): + """Suppress pymatgen rounding warnings that occur inside Flask handlers + (where pytest.warns can't capture them). The warnings are explicitly + tested in test_webapp.py::TestPymatgenRoundingWarning.""" + warnings.filterwarnings("ignore", + message=".*fractional coordinates rounded.*", + category=UserWarning, + module="larixite.cif_cluster") + warnings.filterwarnings("ignore", + message=".*fractional coordinates rounded.*", + category=UserWarning, + module="larixite.struct") From 0117eb53465a5e506b79454a77e059b33e57d352 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 15:57:42 +0200 Subject: [PATCH 21/46] add webapp tests from AI agent --- tests/test_webapp.py | 247 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests/test_webapp.py diff --git a/tests/test_webapp.py b/tests/test_webapp.py new file mode 100644 index 0000000..9461b81 --- /dev/null +++ b/tests/test_webapp.py @@ -0,0 +1,247 @@ +"""Tests for larixite Flask webapp.""" + +import tempfile +from pathlib import Path + +import pytest + +# Skip if Flask not installed +FLASK = pytest.importorskip("flask") + + +STRUCTS = Path(__file__).parent / "structs" + + +@pytest.fixture(scope="module") +def app(): + from larixite.webapp.webapp import app + + app.config["TESTING"] = True + app.config["UPLOAD_FOLDER"] = str(tempfile.mkdtemp()) + app.config["MAX_CONTENT_LENGTH"] = 2**24 + app.config["PROPAGATE_EXCEPTIONS"] = False + return app + + +@pytest.fixture(scope="module") +def client(app): + return app.test_client() + + +ZNO_TEXT = (STRUCTS / "ZnO_mp-2133.cif").read_text() + + +def _upload(client, content): + """Upload a CIF and return the redirect location.""" + with tempfile.NamedTemporaryFile(suffix=".cif", delete=False, mode="w") as f: + f.write(content) + fname = f.name + try: + rv = client.post( + "/upload_cif", + data={"file": (open(fname, "rb"), "test.cif")}, + content_type="multipart/form-data", + follow_redirects=True, + ) + assert rv.status_code == 200 + return rv + finally: + Path(fname).unlink() + + +class TestIndex: + def test_index(self, client): + rv = client.get("/") + assert rv.status_code == 200 + + +class TestUpload: + def test_upload_valid_cif(self, client): + rv = _upload(client, ZNO_TEXT) + assert rv.status_code == 200 + + def test_upload_empty_file(self, client): + rv = client.post( + "/upload_cif", + data={"file": (b"", "")}, + content_type="multipart/form-data", + follow_redirects=True, + ) + assert rv.status_code == 200 + + +class TestCifs: + def test_cifs_page_after_upload(self, client): + _upload(client, ZNO_TEXT) + rv = client.get("/cifs/test.cif") + assert rv.status_code == 200 + + def test_feff_post(self, client): + _upload(client, ZNO_TEXT) + rv = client.post( + "/cifs/test.cif", + data={ + "feff": "1", + "absorbing_atom": "Zn", + "edge": "K", + "cluster_size": "7.0", + "with_h": "0", + }, + follow_redirects=True, + ) + assert rv.status_code == 200 + data = rv.get_data(as_text=True) + assert "Zn" in data + + def test_fdmnes_post(self, client): + _upload(client, ZNO_TEXT) + rv = client.post( + "/cifs/test.cif", + data={ + "fdmnes": "1", + "absorbing_atom": "Zn", + "edge": "K", + "cluster_size": "7.0", + "with_h": "0", + }, + follow_redirects=True, + ) + assert rv.status_code == 200 + + +class TestOutput: + """Test /output route — returns raw FEFF/FDMNES input text.""" + + def _setup(self, client): + _upload(client, ZNO_TEXT) + return client + + def test_feff_output_zn_k(self, client): + self._setup(client) + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/feff/feff.inp") + assert rv.status_code == 200 + data = rv.get_data(as_text=True) + assert data.count("TITLE") >= 1 or data.count("*") > 0 + assert "Zn" in data + + def test_feff_output_with_h(self, client): + self._setup(client) + rv = client.get("/output/test.cif/Zn/1/K/7.0/1/feff/feff.inp") + assert rv.status_code == 200 + + def test_fdmnes_output(self, client): + self._setup(client) + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/fdmnes/fdmfile.txt") + assert rv.status_code == 200 + data = rv.get_data(as_text=True) + assert "1" in data or ".inp" in data + + def test_fdmnes_output_o_edge(self, client): + self._setup(client) + rv = client.get("/output/test.cif/O/1/K/7.0/0/fdmnes/fdmfile.txt") + assert rv.status_code == 200 + + +class TestCiffile: + def test_ciffile_download(self, client): + _upload(client, ZNO_TEXT) + rv = client.get("/ciffile/test.cif/test.cif") + assert rv.status_code == 200 + data = rv.get_data(as_text=True) + assert "Zn" in data + assert "O" in data + + +class TestZipfile: + def test_feff_zip(self, client): + _upload(client, ZNO_TEXT) + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/feff/test_zoom") + assert rv.status_code in (200, 500) + + def test_fdmnes_zip(self, client): + _upload(client, ZNO_TEXT) + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/fdmnes/test_zoom") + assert rv.status_code in (200, 500) + + +class TestUploadCifFile: + """Test actual file upload with multipart form data.""" + + def test_real_file_upload(self, client): + with open(STRUCTS / "ZnO_mp-2133.cif", "rb") as f: + rv = client.post( + "/upload_cif", + data={"file": (f, "uploaded.cif")}, + content_type="multipart/form-data", + follow_redirects=True, + ) + assert rv.status_code == 200 + + def test_no_file_part(self, client): + rv = client.post( + "/upload_cif", + data={}, + content_type="multipart/form-data", + follow_redirects=True, + ) + assert rv.status_code == 200 + + def test_wrong_extension(self, client): + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f: + f.write(ZNO_TEXT) + fname = f.name + try: + rv = client.post( + "/upload_cif", + data={"file": (open(fname, "rb"), "test.txt")}, + content_type="multipart/form-data", + follow_redirects=True, + ) + assert rv.status_code == 200 + finally: + Path(fname).unlink() + + +class TestAbout: + def test_about(self, client): + rv = client.get("/about/") + assert rv.status_code == 200 + + +class TestUtility: + """Test helper functions exposed by the webapp.""" + + def test_random_string(self): + from larixite.webapp.webapp import random_string + + for _ in range(5): + s = random_string(12) + assert len(s) == 12 and s.islower() + + def test_allowed_file(self): + from larixite.webapp.webapp import allowed_file + + assert allowed_file("test.cif") is True + assert allowed_file("test.txt") is False + assert allowed_file("test.CIF") is True + assert allowed_file("test") is False + + +class TestGetElements: + def test_simple_element(self): + from larixite.webapp.webapp import get_elements + + stoich, _ = get_elements("Fe") + assert stoich == {"Fe": 1} + + def test_compound(self): + from larixite.webapp.webapp import get_elements + + stoich, _ = get_elements("Fe2O3") + assert stoich == {"Fe": 2, "O": 3} + + def test_wat_prefix_raises(self): + from larixite.webapp.webapp import get_elements + + with pytest.raises(UnboundLocalError): + get_elements("Wat") From cca6a9abb22b378e1ff49b2b68f55330925af748 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 15:58:01 +0200 Subject: [PATCH 22/46] jobname in the sbatch --- larixite/fdmnes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index ef50c77..ed78f88 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -420,6 +420,7 @@ def write_sbatch(self, template: str | Path | None = None, **kwargs): logger.error("execute `write_input` first") return sbatchout = self.outdir / f"{self.fileout_prefix}.sbatch" + kwargs.update({"jobname": self.fileout_prefix}) with open(sbatchout, "w") as fp, open(template) as tp: fp.write(tp.read().format(**kwargs)) logger.info(f"written {fp.name}") From f5d321e5b79b32f1f118f636982c6ebaf4ed0ca6 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 12 May 2026 16:28:48 +0200 Subject: [PATCH 23/46] fix sbatch bugs --- examples/fdmnes_xas_workflow.ipynb | 48 ++++++++++++++-------- larixite/fdmnes.py | 5 ++- larixite/templates/fdmnes_sbatch_esrf.tmpl | 22 +++++----- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/examples/fdmnes_xas_workflow.ipynb b/examples/fdmnes_xas_workflow.ipynb index b24f694..d2eb26f 100644 --- a/examples/fdmnes_xas_workflow.ipynb +++ b/examples/fdmnes_xas_workflow.ipynb @@ -69,7 +69,13 @@ "text": [ "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 4 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", " struct = structs.parse_structures(primitive=False)[frame]\n", - "\u001b[32;21m[larixite.struct | INFO ] 0: ZnO_wurtzite_Weber1923_COD-1011259.cif\u001b[0m\n" + "\u001b[32;21m[larixite.struct | INFO ] 0: ZnO_wurtzite_Weber1923_COD-1011259.cif\u001b[0m\n", + "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 8 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", + " struct = structs.parse_structures(primitive=False)[frame]\n", + "\u001b[32;21m[larixite.struct | INFO ] 1: ZnO_mp-2133.cif\u001b[0m\n", + "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 32 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", + " struct = structs.parse_structures(primitive=False)[frame]\n", + "\u001b[32;21m[larixite.struct | INFO ] 2: ZnO_mp-997630.cif\u001b[0m\n" ] } ], @@ -132,7 +138,7 @@ " " ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -180,22 +186,15 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32;21m[larixite.fdmnes | INFO ] R_self set to 3.5 for radius 5\u001b[0m\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ "! FDMNES input file generated by larixite\n", - "! 2026-05-12 10:56:09\n", + "! 2026-05-12 16:00:04\n", "! larixite : 2025.5.1\n", "! pymatgen : 2025.6.14\n", "Header\n", @@ -272,24 +271,41 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32;21m[larixite.fdmnes | INFO ] written `/tmp/larixite/fdmnes/ZnO_wurtzite_Weber1923_COD-1011259.cif/job_88xkt1ta/job.inp`\u001b[0m\n" + "\u001b[32;21m[larixite.fdmnes | INFO ] written `/tmp_14_days/larixite/job.inp`\u001b[0m\n" ] } ], "source": [ - "outdir = f.write_input()" + "outdir = f.write_input(outdir=\"/tmp_14_days/larixite\")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32;21m[larixite.fdmnes | INFO ] written /tmp_14_days/larixite/job.sbatch\u001b[0m\n" + ] + } + ], + "source": [ + "f.write_sbatch()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -298,7 +314,7 @@ "text": [ " FDMNES program, Revision 3rd of October 2025 \n", " Date = 12 05 2026 \n", - " Time = 08 h 56 mn 34 s \n", + " Time = 14 h 00 mn 05 s \n", "\n", " python \n", " edge \n", diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index ed78f88..afb783d 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -398,7 +398,7 @@ def write_input( self.outdir = outdir return outdir - def write_sbatch(self, template: str | Path | None = None, **kwargs): + def write_sbatch(self, template: str | Path | None = None, ncpus: int = 8): """Generates a SBATCH file (SLURM workload manager) using a template Arguments @@ -420,7 +420,8 @@ def write_sbatch(self, template: str | Path | None = None, **kwargs): logger.error("execute `write_input` first") return sbatchout = self.outdir / f"{self.fileout_prefix}.sbatch" - kwargs.update({"jobname": self.fileout_prefix}) + kwargs = {"jobname": self.fileout_prefix, + "ncpus": ncpus,} with open(sbatchout, "w") as fp, open(template) as tp: fp.write(tp.read().format(**kwargs)) logger.info(f"written {fp.name}") diff --git a/larixite/templates/fdmnes_sbatch_esrf.tmpl b/larixite/templates/fdmnes_sbatch_esrf.tmpl index ca1b655..8fbbdc9 100644 --- a/larixite/templates/fdmnes_sbatch_esrf.tmpl +++ b/larixite/templates/fdmnes_sbatch_esrf.tmpl @@ -1,31 +1,29 @@ -#!/bin/bash -l +#!/bin/zsh -l #SBATCH --nodes=1 #: Number of nodes #SBATCH --ntasks-per-node={ncpus} #: Total number of tasks (=CPUs cores) per node #SBATCH --cpus-per-task=1 #: CPUs for each task #SBATCH --mem-per-cpu=16G #: Total memory for each CPU -#SBATCH --time=48:00:00 #: Wall-time limit +#SBATCH --time=120:00:00 #: Wall-time limit #SBATCH --partition=nice,nice-long #: ESRF cluster -#SBATCH --constraint="hpc3|hpc5|hpc6|hpc7" #: ESRF cluster -#SBATCH --job-name="fdmnes" #: Name used to set the output files +#SBATCH --constraint="hpc5" #: ESRF cluster +#SBATCH --job-name="{jobname}" #: Name used to set the output files #SBATCH --output=%x_output.txt #SBATCH --error=%x_output.txt function compress {{ - gzip -f job_bav.txt + gzip -f {jobname}_bav.txt }} +# Unload all loaded modules. module purge -#VERSION="default" #"module avail fdmnes" to see available versions +#"module avail fdmnes" to see available versions VERSION="2026.03.10" -if [ $VERSION = "default" ]; then - module load fdmnes -else - module load fdmnes/$VERSION -fi + +module load fdmnes/$VERSION # Run the calculation. -srun ${{FDMNES_COMMAND}} fdmnes +srun ${{FDMNES_COMMAND}} # Compress the large job_bav.txt file. compress From 5423cc4c202ef9fd8a05b540b5d36993f74cff79 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 11:48:37 +0200 Subject: [PATCH 24/46] migrate etst from larch --- tests/test_struct2xas.py | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_struct2xas.py diff --git a/tests/test_struct2xas.py b/tests/test_struct2xas.py new file mode 100644 index 0000000..e041a8a --- /dev/null +++ b/tests/test_struct2xas.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +"""Test feff_print parameter in Struct2XAS.make_input_feff() + +Tests the configurable PRINT card values feature added to struct2xas.py. +""" + +import re +import pytest +import tempfile +from pathlib import Path + +from larixite.struct2xas import Struct2XAS + +STRUCTS = Path(__file__).parent / "structs" +CIF_FILE = STRUCTS / "ZnO_mp-2133.cif" + + +@pytest.fixture +def cif_file(): + """Return path to test CIF file.""" + if not CIF_FILE.exists(): + pytest.skip(f"Test CIF file not found: {CIF_FILE}") + return CIF_FILE + + +def extract_print_values(feff_inp_path): + """Extract PRINT card values from feff.inp file.""" + content = Path(feff_inp_path).read_text() + match = re.search( + r"^PRINT\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", + content, + re.MULTILINE, + ) + if not match: + return None + return [int(match.group(i)) for i in range(1, 7)] + + +def test_default_feff_print_has_ff2chi_3(cif_file): + """Default feff_print should be [1, 0, 0, 0, 0, 3] matching structure2feff.py.""" + with tempfile.TemporaryDirectory() as tmpdir: + mat = Struct2XAS(file=str(cif_file), abs_atom="Zn") + mat.set_abs_site(0) + mat.make_input_feff(radius=6.0, parent_path=tmpdir) + + feff_files = list(Path(tmpdir).glob("**/feff.inp")) + assert len(feff_files) == 1, "feff.inp should be generated" + + values = extract_print_values(feff_files[0]) + assert values is not None, "PRINT card should be present" + assert values == [1, 0, 0, 0, 0, 3], f"Expected [1,0,0,0,0,3], got {values}" + + +def test_custom_feff_print_values(cif_file): + """Custom feff_print values should be written to feff.inp.""" + custom_print = [1, 1, 1, 1, 1, 2] + + with tempfile.TemporaryDirectory() as tmpdir: + mat = Struct2XAS(file=str(cif_file), abs_atom="Zn") + mat.set_abs_site(0) + mat.make_input_feff(radius=6.0, parent_path=tmpdir, feff_print=custom_print) + + feff_files = list(Path(tmpdir).glob("**/feff.inp")) + assert len(feff_files) == 1 + + values = extract_print_values(feff_files[0]) + assert values == custom_print, f"Expected {custom_print}, got {values}" + + +def test_feff_print_old_behavior_recoverable(cif_file): + """Old behavior (ff2chi=0) should be recoverable via feff_print parameter.""" + old_print = [1, 0, 0, 0, 0, 0] + + with tempfile.TemporaryDirectory() as tmpdir: + mat = Struct2XAS(file=str(cif_file), abs_atom="Zn") + mat.set_abs_site(0) + mat.make_input_feff(radius=6.0, parent_path=tmpdir, feff_print=old_print) + + feff_files = list(Path(tmpdir).glob("**/feff.inp")) + assert len(feff_files) == 1 + + values = extract_print_values(feff_files[0]) + assert values == old_print, f"Expected {old_print}, got {values}" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 81351df1b81553a34ab25c4d6b37dcd2f8efbdd3 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:25:11 +0200 Subject: [PATCH 25/46] fix test workflow --- .github/workflows/test-ubuntu.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index cc40abd..c8e7ff4 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -36,5 +36,4 @@ jobs: pip install ".[dev,test,doc]" - name: Run test suite run: | - cd tests - python -m pytest --cov=./ --cov-report=xml + python -m pytest --cov=larixite --cov-report=xml From a41239819d64580eff8b28b7a91157aab504c1f5 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:27:53 +0200 Subject: [PATCH 26/46] update numpy --- .github/workflows/test-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index c8e7ff4..6425d99 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -30,7 +30,7 @@ jobs: - name: Install conda dependencies run: | printenv - conda install -y -c conda-forge python=${{ matrix.python-version }} "numpy>=1.26" scipy pytest pytest-cov coverage + conda install -y -c conda-forge python=${{ matrix.python-version }} "numpy>=2.0" scipy pytest pytest-cov coverage - name: Install larixite and dependencies with pip run: | pip install ".[dev,test,doc]" From 97e6e4c700bd9f9dd4c9c2dc5e7c64daccc661c1 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:28:35 +0200 Subject: [PATCH 27/46] update deps --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 989f9ac..33ef473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "pymatgen>=2025.6.14", "mp-api>=0.45.8", "emmet-core>=0.84.9", + "requests", ] license = "MIT" From c431190bef0890e2faa86231fb21569c85c45e75 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:35:16 +0200 Subject: [PATCH 28/46] fix workflow? --- .github/workflows/test-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 6425d99..8f9f477 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -30,7 +30,7 @@ jobs: - name: Install conda dependencies run: | printenv - conda install -y -c conda-forge python=${{ matrix.python-version }} "numpy>=2.0" scipy pytest pytest-cov coverage + conda install -y -c conda-forge python=${{ matrix.python-version }} "numpy>=2.0" scipy pytest pytest-cov coverage requests - name: Install larixite and dependencies with pip run: | pip install ".[dev,test,doc]" From 29b2a57eefc31434e78f5d46795c398d90a336d4 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:39:53 +0200 Subject: [PATCH 29/46] remove conda installs, pip should be enough --- .github/workflows/test-ubuntu.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 8f9f477..ea72354 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -27,10 +27,6 @@ jobs: channel-priority: true activate-environment: anaconda-client-env python-version: ${{ matrix.python-version }} - - name: Install conda dependencies - run: | - printenv - conda install -y -c conda-forge python=${{ matrix.python-version }} "numpy>=2.0" scipy pytest pytest-cov coverage requests - name: Install larixite and dependencies with pip run: | pip install ".[dev,test,doc]" From 5c623c3d04623e1eb46e7d579cb6dfc969978abb Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:52:14 +0200 Subject: [PATCH 30/46] fix remove larch deps in struct2xas --- larixite/struct2xas.py | 107 +++++++++++++++++++++-------------------- larixite/utils.py | 15 ++++-- 2 files changed, 67 insertions(+), 55 deletions(-) diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index 73b8c26..5696822 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -15,6 +15,7 @@ import os import json import time +from pathlib import Path from dataclasses import dataclass from typing import Union, List # , Any, Dict @@ -27,12 +28,14 @@ from pymatgen.io.cif import CifParser from pymatgen.symmetry.analyzer import SpacegroupAnalyzer -import larch.utils.logging as logging -from larch.utils import mkdir, unixpath +import larixite.utils.logging as logging +from larixite.utils import mkdir, unixpath -from larch.site_config import user_larchdir -from larch.io import read_ascii -from larch.math.convolution1D import lin_gamma, conv +#from larch.site_config import user_larchdir +#from larch.io import read_ascii +#from larch.math.convolution1D import lin_gamma, conv + +user_larchdir = Path.home() / ".larch" try: import py3Dmol @@ -1379,67 +1382,67 @@ def visualize(self, radius=2.5, unitcell=False): # print("Label:\n", color_elems) -def get_fdmnes_info(file, labels=("energy", "mu")): - """Get info from the fdmnes output file such as edge energy, atomic number Z, - and fermi level energy, and returns a group with the storage information +# def get_fdmnes_info(file, labels=("energy", "mu")): +# """Get info from the fdmnes output file such as edge energy, atomic number Z, +# and fermi level energy, and returns a group with the storage information - Parameters: +# Parameters: - file (str): path to the fdmnes output file. - Obs: The INPUT file must have the "Header" keyword to use this function in the OUTPUT file +# file (str): path to the fdmnes output file. +# Obs: The INPUT file must have the "Header" keyword to use this function in the OUTPUT file - """ - group = read_ascii(file, labels=labels) - - with open(group.path) as f: - line = f.readlines()[3] - header = line.split() - ( - e_edge, - Z, - e_fermi, - ) = (float(header[0]), float(header[1]), float(header[6])) - print( - f"Calculated Fermi level: {e_fermi}\nAtomic_number: {Z}\nEnergy_edge: {e_edge}" - ) +# """ +# group = read_ascii(file, labels=labels) - group.e_edge = e_edge - group.Z = Z - group.e_fermi = e_fermi +# with open(group.path) as f: +# line = f.readlines()[3] +# header = line.split() +# ( +# e_edge, +# Z, +# e_fermi, +# ) = (float(header[0]), float(header[1]), float(header[6])) +# print( +# f"Calculated Fermi level: {e_fermi}\nAtomic_number: {Z}\nEnergy_edge: {e_edge}" +# ) - return group +# group.e_edge = e_edge +# group.Z = Z +# group.e_fermi = e_fermi +# return group -def convolve_data( - energy, mu, group, fwhm=1, linbroad=[1.5, 0, 50], kernel="gaussian", efermi=None -): - """ - Function for manual convolution using Convolution1D from larch and returning a group - Generic discrete convolution +# def convolve_data( +# energy, mu, group, fwhm=1, linbroad=[1.5, 0, 50], kernel="gaussian", efermi=None +# ): +# """ +# Function for manual convolution using Convolution1D from larch and returning a group - Description - ----------- +# Generic discrete convolution - This is a manual (not optimized!) implementation of discrete 1D - convolution intended for spectroscopy analysis. The difference with - commonly used methods is the possibility to adapt the convolution - kernel for each convolution point, e.g. change the FWHM of the - Gaussian kernel as a function of the energy scale. +# Description +# ----------- - Resources - --------- +# This is a manual (not optimized!) implementation of discrete 1D +# convolution intended for spectroscopy analysis. The difference with +# commonly used methods is the possibility to adapt the convolution +# kernel for each convolution point, e.g. change the FWHM of the +# Gaussian kernel as a function of the energy scale. - .. [WPconv] - .. [Fisher] - .. [GP1202] +# Resources +# --------- - """ +# .. [WPconv] +# .. [Fisher] +# .. [GP1202] + +# """ - gamma_e = lin_gamma(energy, fwhm=fwhm, linbroad=linbroad) - mu_conv = conv(energy, mu, kernel=kernel, fwhm_e=gamma_e, efermi=efermi) - group.conv = mu_conv - return group +# gamma_e = lin_gamma(energy, fwhm=fwhm, linbroad=linbroad) +# mu_conv = conv(energy, mu, kernel=kernel, fwhm_e=gamma_e, efermi=efermi) +# group.conv = mu_conv +# return group def save_cif_from_mp( diff --git a/larixite/utils.py b/larixite/utils.py index 18e3457..1789339 100644 --- a/larixite/utils.py +++ b/larixite/utils.py @@ -7,6 +7,7 @@ import logging import io from datetime import datetime +from pathlib import Path from typing import Union from gzip import GzipFile from pathlib import Path @@ -43,9 +44,7 @@ def get_homedir() -> Path: # use Windows API to get home directory from win32com.shell import shellcon, shell - homedir = Path( - shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) - ) + homedir = Path(shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0)) except ImportError: pass @@ -257,3 +256,13 @@ def read_textfile(filename: Union[Path, io.IOBase, str, bytes], size=None) -> st with fopen(get_path(filename), "rb") as fh: text = bytes2str(fh.read(size)) return text.replace("\r\n", "\n").replace("\r", "\n") + + +def unixpath(d): + if isinstance(d, bytes): + d = str(from_bytes(d).best()) + if isinstance(d, str): + d = Path(d).absolute() + if isinstance(d, Path): + return d.as_posix() + raise ValueError(f"cannot get Path name from {d}") From 64ec4b8dfcece7ed07ceacb47f9c9ab0061845dc Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:56:30 +0200 Subject: [PATCH 31/46] deps for tests --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 33ef473..a90dc92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ Tracker = "https://github.com/xraypy/larixite/issues" [project.optional-dependencies] web = ["flask"] jupyter = ["jupyterlab==3.*", "crystal-toolkit>=2025.7.31"] -test = ["pytest"] +test = ["pytest", "pytest-cov", "pygments"] doc = ["sphinx"] dev = ["build", "twine", "ruff"] all = ["larixite[dev,web,test,doc,jupyter]"] From 2c08f6adc400240be9549f22796a518f6b3ad117 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 13:59:40 +0200 Subject: [PATCH 32/46] install all --- .github/workflows/test-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index ea72354..43c265c 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -29,7 +29,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install larixite and dependencies with pip run: | - pip install ".[dev,test,doc]" + pip install ".[all]" - name: Run test suite run: | python -m pytest --cov=larixite --cov-report=xml From 2d70aa26dbc9a97922d264f87de2372fd7a47c6e Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 14:03:20 +0200 Subject: [PATCH 33/46] pip install in the good conda environment --- .github/workflows/test-ubuntu.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 43c265c..0535e21 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -27,9 +27,11 @@ jobs: channel-priority: true activate-environment: anaconda-client-env python-version: ${{ matrix.python-version }} + auto-activate-base: false - name: Install larixite and dependencies with pip run: | - pip install ".[all]" + python -m pip install --upgrade pip + python -m pip install ".[all]" - name: Run test suite run: | python -m pytest --cov=larixite --cov-report=xml From 794b19f3c7db5cebc9db3367c9566cf19b1c7411 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 14:07:17 +0200 Subject: [PATCH 34/46] mamba should have pip by default --- .github/workflows/test-ubuntu.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 0535e21..4bc045f 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -28,6 +28,8 @@ jobs: activate-environment: anaconda-client-env python-version: ${{ matrix.python-version }} auto-activate-base: false + mamba-version: "*" + use-mamba: true - name: Install larixite and dependencies with pip run: | python -m pip install --upgrade pip From db9346a255d3a8a3b827eea2de8f0517eebfce68 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Wed, 13 May 2026 14:12:03 +0200 Subject: [PATCH 35/46] fix bug in logger import --- larixite/struct2xas.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index 5696822..e2287be 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -28,8 +28,7 @@ from pymatgen.io.cif import CifParser from pymatgen.symmetry.analyzer import SpacegroupAnalyzer -import larixite.utils.logging as logging -from larixite.utils import mkdir, unixpath +from larixite.utils import mkdir, unixpath, get_logger #from larch.site_config import user_larchdir #from larch.io import read_ascii @@ -52,7 +51,7 @@ # initialize the logger -logger = logging.getLogger("struct2xas", level="INFO") +logger = get_logger("struct2xas", level="INFO") def _get_timestamp() -> str: From 439e0a114e60775abc0e240b92a8b012e9236c61 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 10:14:31 +0200 Subject: [PATCH 36/46] include FDMNES optimize option in the webapp --- larixite/webapp/templates/index.html | 50 ++++++++++++++++------------ larixite/webapp/webapp.py | 33 ++++++++++++------ tests/test_webapp.py | 12 +++---- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/larixite/webapp/templates/index.html b/larixite/webapp/templates/index.html index 4377275..5286783 100644 --- a/larixite/webapp/templates/index.html +++ b/larixite/webapp/templates/index.html @@ -98,17 +98,23 @@ {% endfor %} - - - - - - - - + + + + + + + + + + + + + @@ -131,13 +137,13 @@ {% if zip_link: %} - - - - + + + + {% endif %} {% if cif_link: %} @@ -150,10 +156,10 @@ {% if out_links: %} {% for link, data in out_links.items() %} - + {% endfor %} diff --git a/larixite/webapp/webapp.py b/larixite/webapp/webapp.py index 82ec110..72e6f20 100644 --- a/larixite/webapp/webapp.py +++ b/larixite/webapp/webapp.py @@ -52,6 +52,7 @@ def connect(session, clear=False, cifid=None): if config is None or clear: config = {'cifdict': {}, 'with_h': 0, + 'optimize': False, 'edges': ['K', 'L3', 'L2', 'L1', 'M5', 'M4', 'M3'], 'cluster_size': 7.0, 'mineral': '', @@ -191,12 +192,12 @@ def cifs(cifid=None): cifid = config['cifid'] config['cif_link'] = (f'AMSCD_{cifid}.cif', cifid) config['zip_link'] = (f'AMSCD_{cifid}_feff.zip', cifid, - absorber, edge, cluster_size, with_h, 'feff') + absorber, edge, cluster_size, with_h, 'feff', 0) config['out_links'] = {} for slabel, sindex in config['all_sites'][absorber].items(): link = f'feff_{cifid}_{absorber}{sindex}_{edge}.inp' config['out_links'][link] = (slabel, cifid, absorber, edge, sindex, - cluster_size, with_h, 'feff') + cluster_size, with_h, 'feff', 0) elif 'fdmnes' in request.form.keys(): @@ -205,6 +206,8 @@ def cifs(cifid=None): config['cluster_size'] = cluster_size = request.form.get('cluster_size') with_h = request.form.get('with_h') in ('1', 'on', 'True', 1, True) config['with_h'] = with_h = int(with_h) + optimize = request.form.get('optimize') in ('1', 'on', 'True', 1, True) + config['optimize'] = optimize if config['ciftext'] is not None: cifid = config['cifid'] if config['ciffile'] not in ('', None, 'None'): @@ -214,15 +217,15 @@ def cifs(cifid=None): ciffile = f'AMSCD_{cifid}.cif' config['cif_link'] = (f'AMSCD_{cifid}.cif', cifid) config['zip_link'] = (f'AMSCD_{cifid}_fdmnes.zip', cifid, - absorber, edge, cluster_size, with_h, 'fdmnes') + absorber, edge, cluster_size, with_h, 'fdmnes', optimize) config['out_links'] = {} out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=ciffile) + filename=ciffile, optimize=optimize) for slabel, text in out.items(): link = f'fdmnes_{slabel}' config['out_links'][slabel] = (slabel, cifid, absorber, edge, 0, - cluster_size, with_h, 'fdmnes') + cluster_size, with_h, 'fdmnes', optimize) return render_template('index.html', **config) @@ -255,9 +258,9 @@ def get_elements(absorber): return stoich, absorber -@app.route('/zipfile//////
/') +@app.route('/zipfile////////') def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, - with_h=False, form='feff', fname=None): + with_h=False, form='feff', optimize=False, fname=None): connect(session, cifid=cifid) global cifdb, config stoich, absorber = get_elements(absorber) @@ -284,9 +287,13 @@ def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, with_h=with_h, absorber_site=int(sindex)) zfile.writestr(oname, result) elif form == 'fdmnes': + if optimize in ('1', 'on', 'True', 1, True): + optimize = 1 + else: + optimize = 0 out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=f'AMSCD_{cifid}.cif') + filename=f'AMSCD_{cifid}.cif', optimize=bool(optimize)) for key, val in out.items(): zfile.writestr(key, val) zfile.close() @@ -294,9 +301,9 @@ def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, return send_from_directory(tfolder, tfile, mimetype='application/zip', as_attachment=True, download_name=fname) -@app.route('/output////////') +@app.route('/output/////////') def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, - with_h=False, form='feff', fname=None): + with_h=False, form='feff', optimize=False, fname=None): connect(session, cifid=cifid) global cifdb, config stoich, absorber = get_elements(absorber) @@ -314,6 +321,10 @@ def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, with_h = 1 else: with_h = 0 + if optimize in ('1', 'on', 'True', 1, True): + optimize = 1 + else: + optimize = 0 if form =='feff': result = cif2feffinp(config['ciftext'], absorber, edge=edge, cluster_size=float(cluster_size), cifid=xcifid, @@ -323,7 +334,7 @@ def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, oname = f'AMSCD_{xcifid}.cif' out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=oname) + filename=oname, optimize=bool(optimize)) if fname == 'fdmfile.txt': result = out.get(fname, None) else: diff --git a/tests/test_webapp.py b/tests/test_webapp.py index 9461b81..cd1fe53 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -118,7 +118,7 @@ def _setup(self, client): def test_feff_output_zn_k(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/0/feff/feff.inp") + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/feff/0/feff.inp") assert rv.status_code == 200 data = rv.get_data(as_text=True) assert data.count("TITLE") >= 1 or data.count("*") > 0 @@ -126,19 +126,19 @@ def test_feff_output_zn_k(self, client): def test_feff_output_with_h(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/1/feff/feff.inp") + rv = client.get("/output/test.cif/Zn/1/K/7.0/1/feff/0/feff.inp") assert rv.status_code == 200 def test_fdmnes_output(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/0/fdmnes/fdmfile.txt") + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/fdmnes/0/fdmfile.txt") assert rv.status_code == 200 data = rv.get_data(as_text=True) assert "1" in data or ".inp" in data def test_fdmnes_output_o_edge(self, client): self._setup(client) - rv = client.get("/output/test.cif/O/1/K/7.0/0/fdmnes/fdmfile.txt") + rv = client.get("/output/test.cif/O/1/K/7.0/0/fdmnes/0/fdmfile.txt") assert rv.status_code == 200 @@ -155,12 +155,12 @@ def test_ciffile_download(self, client): class TestZipfile: def test_feff_zip(self, client): _upload(client, ZNO_TEXT) - rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/feff/test_zoom") + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/feff/0/test_zoom") assert rv.status_code in (200, 500) def test_fdmnes_zip(self, client): _upload(client, ZNO_TEXT) - rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/fdmnes/test_zoom") + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/fdmnes/0/test_zoom") assert rv.status_code in (200, 500) From d70aba336469bc60911ae940bed8019120226ee7 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 10:14:51 +0200 Subject: [PATCH 37/46] SCF enabled by `optimize --- larixite/fdmnes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index afb783d..08c5e65 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -209,6 +209,10 @@ def optimize_params(self) -> dict: self.vmax = -6 logger.info(f"Vmax set to {self.vmax} for molecule") + #enable SCF + params["SCF"] = True + logger.info("SCF enabled") + return params def get_structure(self, struct_type: str | None = None) -> str: @@ -435,6 +439,7 @@ def struct2fdmnes( filename: None | str = None, radius: float = 7, edge: str | None = None, + optimize: bool = True, ) -> dict: """convert CIF/XYZ into a dictionary of {name: text} for FDMNES output files @@ -450,6 +455,8 @@ def struct2fdmnes( format of text : 'cif' or 'xyz' ['cif'] filename : str full path to filename ['unknown.{format}'] + optimize : bool + optimize input parameters based on absorber and structure [True] Returns ------- @@ -481,5 +488,6 @@ def struct2fdmnes( edge=edge, radius=radius, fileout_prefix=fout_prefix, + optimize=optimize, ) return {"fdmfile.txt": f"1\n{fout_name}\n", fout_name: fdm.get_input()} From d5bf8cca9698275892bfd26c958fc77e82420f71 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 10:19:12 +0200 Subject: [PATCH 38/46] update changes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04735fe..958675f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ The versioning scheme followed is `YYYY.MAJOR.MINOR`: - Update to FDMNES version March 2026 - Add `make_sbatch` with a template for the ESRF SLURM scheduler +- Add `optimize : bool = False` option in `FdmnesXasInput` class to control if + the input parameters are optimized (= more accurate calcuation, but slower) or + not +- Add FDMNES optimization checkbox in the Web application +- Added more tests ### `struc2xas` From a1ba4194ad1c57412a45895db22a1314baa0b34c Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 10:29:24 +0200 Subject: [PATCH 39/46] separate FEFF and FDMNES sections --- larixite/webapp/templates/index.html | 70 +++++++++++++++++----------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/larixite/webapp/templates/index.html b/larixite/webapp/templates/index.html index 5286783..08cf3e2 100644 --- a/larixite/webapp/templates/index.html +++ b/larixite/webapp/templates/index.html @@ -98,32 +98,50 @@ {% endfor %} -
- - - - - - - - - - - - -
File Link Crystal Site/File Type
{{ zip_link[0] }}   Zip File, all outputs
{{ zip_link[0] }}   Zip File, all outputs
{{ link }}{{ link }}   {{ data[0] }}
- - -
-
- - - -
+ + + + + + + + + + +
+ + + + + + + + +
FEFF
+ +
+ +
+ + + + + + + + + + + + +
FDMNES
+
+ +

CIF Text:

{{ciftext|safe}}
From 5e6110d9dc588cdee42ca967e20026e3138fdbd3 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 11:07:17 +0200 Subject: [PATCH 40/46] added Green formalism checkbox --- CHANGELOG.md | 22 +++++++----- larixite/fdmnes.py | 6 +++- larixite/webapp/templates/index.html | 50 ++++++++++++++++------------ larixite/webapp/webapp.py | 31 +++++++++++------ tests/test_webapp.py | 8 ++--- 5 files changed, 70 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 958675f..515dd41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,21 @@ The versioning scheme followed is `YYYY.MAJOR.MINOR`: ## [2026.1.0 - not released yet] +### web application + + - Separate FEFF and FDMNES sections for XAS input generation + - FDMNES: added optimization checkbox + - FDMNES: added Green formalism checkbox + - Added more tests + ### `fdmnes` -- Update to FDMNES version March 2026 -- Add `make_sbatch` with a template for the ESRF SLURM scheduler -- Add `optimize : bool = False` option in `FdmnesXasInput` class to control if - the input parameters are optimized (= more accurate calcuation, but slower) or - not -- Add FDMNES optimization checkbox in the Web application -- Added more tests + - Update to FDMNES version March 2026 + - Add `make_sbatch` with a template for the ESRF SLURM scheduler + - Add `optimize : bool = False` option in `FdmnesXasInput` class to control if the input parameters are optimized (= more accurate calcuation, but slower) or not ### `struc2xas` -- Fully migrated from `larch.xrd.struc2xas` (and removed there) -- Make `struc2xas` *deprecated* and start refactoring \ No newline at end of file + - Fully migrated from `larch.xrd.struc2xas` (and removed there) + - Make `struc2xas` *deprecated* and start refactoring + \ No newline at end of file diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 08c5e65..1aa48e5 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -212,7 +212,7 @@ def optimize_params(self) -> dict: #enable SCF params["SCF"] = True logger.info("SCF enabled") - + return params def get_structure(self, struct_type: str | None = None) -> str: @@ -440,6 +440,7 @@ def struct2fdmnes( radius: float = 7, edge: str | None = None, optimize: bool = True, + green: bool = True, ) -> dict: """convert CIF/XYZ into a dictionary of {name: text} for FDMNES output files @@ -457,6 +458,8 @@ def struct2fdmnes( full path to filename ['unknown.{format}'] optimize : bool optimize input parameters based on absorber and structure [True] + green : bool + use muffin-tin approximation [True] Returns ------- @@ -490,4 +493,5 @@ def struct2fdmnes( fileout_prefix=fout_prefix, optimize=optimize, ) + fdm.green = green return {"fdmfile.txt": f"1\n{fout_name}\n", fout_name: fdm.get_input()} diff --git a/larixite/webapp/templates/index.html b/larixite/webapp/templates/index.html index 08cf3e2..daf2429 100644 --- a/larixite/webapp/templates/index.html +++ b/larixite/webapp/templates/index.html @@ -125,23 +125,29 @@
- - - - - - - - - - - - -
FDMNES
-
- -
+ + + + + + + + + + + + + + + + +
FDMNES
+
+
+ +

CIF Text:

{{ciftext|safe}}
@@ -159,7 +165,7 @@ {{ zip_link[0] }} + form=zip_link[6], optimize=zip_link[7], green=zip_link[8], fname=zip_link[0]) }}"> {{ zip_link[0] }}   Zip File, all outputs {% endif %} @@ -174,10 +180,10 @@ {% if out_links: %} {% for link, data in out_links.items() %} - {{ link }} + {{ link }}   {{ data[0] }} {% endfor %} diff --git a/larixite/webapp/webapp.py b/larixite/webapp/webapp.py index 72e6f20..2ec6faa 100644 --- a/larixite/webapp/webapp.py +++ b/larixite/webapp/webapp.py @@ -52,6 +52,7 @@ def connect(session, clear=False, cifid=None): if config is None or clear: config = {'cifdict': {}, 'with_h': 0, + 'green': True, 'optimize': False, 'edges': ['K', 'L3', 'L2', 'L1', 'M5', 'M4', 'M3'], 'cluster_size': 7.0, @@ -192,12 +193,12 @@ def cifs(cifid=None): cifid = config['cifid'] config['cif_link'] = (f'AMSCD_{cifid}.cif', cifid) config['zip_link'] = (f'AMSCD_{cifid}_feff.zip', cifid, - absorber, edge, cluster_size, with_h, 'feff', 0) + absorber, edge, cluster_size, with_h, 'feff', 0, 0) config['out_links'] = {} for slabel, sindex in config['all_sites'][absorber].items(): link = f'feff_{cifid}_{absorber}{sindex}_{edge}.inp' config['out_links'][link] = (slabel, cifid, absorber, edge, sindex, - cluster_size, with_h, 'feff', 0) + cluster_size, with_h, 'feff', 0, 0) elif 'fdmnes' in request.form.keys(): @@ -206,6 +207,8 @@ def cifs(cifid=None): config['cluster_size'] = cluster_size = request.form.get('cluster_size') with_h = request.form.get('with_h') in ('1', 'on', 'True', 1, True) config['with_h'] = with_h = int(with_h) + green = request.form.get('green') in ('1', 'on', 'True', 1, True) + config['green'] = green optimize = request.form.get('optimize') in ('1', 'on', 'True', 1, True) config['optimize'] = optimize if config['ciftext'] is not None: @@ -217,15 +220,15 @@ def cifs(cifid=None): ciffile = f'AMSCD_{cifid}.cif' config['cif_link'] = (f'AMSCD_{cifid}.cif', cifid) config['zip_link'] = (f'AMSCD_{cifid}_fdmnes.zip', cifid, - absorber, edge, cluster_size, with_h, 'fdmnes', optimize) + absorber, edge, cluster_size, with_h, 'fdmnes', optimize, green) config['out_links'] = {} out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=ciffile, optimize=optimize) + filename=ciffile, optimize=optimize, green=green) for slabel, text in out.items(): link = f'fdmnes_{slabel}' config['out_links'][slabel] = (slabel, cifid, absorber, edge, 0, - cluster_size, with_h, 'fdmnes', optimize) + cluster_size, with_h, 'fdmnes', optimize, green) return render_template('index.html', **config) @@ -258,9 +261,9 @@ def get_elements(absorber): return stoich, absorber -@app.route('/zipfile////////') +@app.route('/zipfile/////////') def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, - with_h=False, form='feff', optimize=False, fname=None): + with_h=False, form='feff', optimize=False, green=False, fname=None): connect(session, cifid=cifid) global cifdb, config stoich, absorber = get_elements(absorber) @@ -293,7 +296,8 @@ def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, optimize = 0 out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=f'AMSCD_{cifid}.cif', optimize=bool(optimize)) + filename=f'AMSCD_{cifid}.cif', optimize=bool(optimize), + green=bool(green) if green else False) for key, val in out.items(): zfile.writestr(key, val) zfile.close() @@ -301,9 +305,9 @@ def zipfile(cifid=None, absorber=None, edge='K', cluster_size=7.0, return send_from_directory(tfolder, tfile, mimetype='application/zip', as_attachment=True, download_name=fname) -@app.route('/output/////////') +@app.route('/output//////////') def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, - with_h=False, form='feff', optimize=False, fname=None): + with_h=False, form='feff', optimize=False, green=False, fname=None): connect(session, cifid=cifid) global cifdb, config stoich, absorber = get_elements(absorber) @@ -325,6 +329,10 @@ def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, optimize = 1 else: optimize = 0 + if green in ('1', 'on', 'True', 1, True): + green = 1 + else: + green = 0 if form =='feff': result = cif2feffinp(config['ciftext'], absorber, edge=edge, cluster_size=float(cluster_size), cifid=xcifid, @@ -334,7 +342,8 @@ def output(cifid=None, absorber=None, site=1, edge='K', cluster_size=7.0, oname = f'AMSCD_{xcifid}.cif' out = struct2fdmnes(config['ciftext'], absorber=absorber, radius=float(cluster_size), edge=edge, - filename=oname, optimize=bool(optimize)) + filename=oname, optimize=bool(optimize), + green=bool(green)) if fname == 'fdmfile.txt': result = out.get(fname, None) else: diff --git a/tests/test_webapp.py b/tests/test_webapp.py index cd1fe53..aaacf6b 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -118,7 +118,7 @@ def _setup(self, client): def test_feff_output_zn_k(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/0/feff/0/feff.inp") + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/feff/0/0/feff.inp") assert rv.status_code == 200 data = rv.get_data(as_text=True) assert data.count("TITLE") >= 1 or data.count("*") > 0 @@ -126,19 +126,19 @@ def test_feff_output_zn_k(self, client): def test_feff_output_with_h(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/1/feff/0/feff.inp") + rv = client.get("/output/test.cif/Zn/1/K/7.0/1/feff/0/0/feff.inp") assert rv.status_code == 200 def test_fdmnes_output(self, client): self._setup(client) - rv = client.get("/output/test.cif/Zn/1/K/7.0/0/fdmnes/0/fdmfile.txt") + rv = client.get("/output/test.cif/Zn/1/K/7.0/0/fdmnes/0/0/fdmfile.txt") assert rv.status_code == 200 data = rv.get_data(as_text=True) assert "1" in data or ".inp" in data def test_fdmnes_output_o_edge(self, client): self._setup(client) - rv = client.get("/output/test.cif/O/1/K/7.0/0/fdmnes/0/fdmfile.txt") + rv = client.get("/output/test.cif/O/1/K/7.0/0/fdmnes/0/0/fdmfile.txt") assert rv.status_code == 200 From 6b2214da04712d1cc11a5c7084f81ffab30c866e Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 11:07:36 +0200 Subject: [PATCH 41/46] more documented template --- larixite/templates/fdmnes_xas.tmpl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/larixite/templates/fdmnes_xas.tmpl b/larixite/templates/fdmnes_xas.tmpl index 656e211..2e9d901 100755 --- a/larixite/templates/fdmnes_xas.tmpl +++ b/larixite/templates/fdmnes_xas.tmpl @@ -13,11 +13,12 @@ Edge ! Range {erange} -{Energpho} !output energy as photon energy (default relative to Fermi level) +{Energpho} !output energy as photon energy (default: relative to the Fermi level) Radius {radius} -! -{Green} +! +{Green} ! use Green formalism (multiple scattering) on a muffin-tin potential +! (default: Finite Difference Method (FDM) on a free-shape potential) ! {Quadrupole} ! From 11179991a017d384ac23297e40c0d5798420e688 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 11:15:36 +0200 Subject: [PATCH 42/46] fix test --- tests/test_webapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_webapp.py b/tests/test_webapp.py index aaacf6b..1c2befc 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -155,12 +155,12 @@ def test_ciffile_download(self, client): class TestZipfile: def test_feff_zip(self, client): _upload(client, ZNO_TEXT) - rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/feff/0/test_zoom") + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/feff/0/0/test_zoom") assert rv.status_code in (200, 500) def test_fdmnes_zip(self, client): _upload(client, ZNO_TEXT) - rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/fdmnes/0/test_zoom") + rv = client.get("/zipfile/test.cif/Zn/K/7.0/0/fdmnes/0/1/test_zoom") assert rv.status_code in (200, 500) From 35fc45e2e6b9ae27adaa7ee3d549d942fa481b06 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 14:32:43 +0200 Subject: [PATCH 43/46] minor fix sbatch --- larixite/templates/fdmnes_sbatch_esrf.tmpl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/larixite/templates/fdmnes_sbatch_esrf.tmpl b/larixite/templates/fdmnes_sbatch_esrf.tmpl index 8fbbdc9..2cce9e3 100644 --- a/larixite/templates/fdmnes_sbatch_esrf.tmpl +++ b/larixite/templates/fdmnes_sbatch_esrf.tmpl @@ -1,11 +1,10 @@ -#!/bin/zsh -l -#SBATCH --nodes=1 #: Number of nodes +#!/bin/bash -l +#SBATCH --nodes=1 #: Number of nodes / if more nodes, add `#SBATCH --constraint="hpc7"` (type of machines) #SBATCH --ntasks-per-node={ncpus} #: Total number of tasks (=CPUs cores) per node -#SBATCH --cpus-per-task=1 #: CPUs for each task +#SBATCH --cpus-per-task=1 #: CPUs for each task / DON'T CHANGE THIS - required by FDMNES #SBATCH --mem-per-cpu=16G #: Total memory for each CPU #SBATCH --time=120:00:00 #: Wall-time limit #SBATCH --partition=nice,nice-long #: ESRF cluster -#SBATCH --constraint="hpc5" #: ESRF cluster #SBATCH --job-name="{jobname}" #: Name used to set the output files #SBATCH --output=%x_output.txt #SBATCH --error=%x_output.txt From f9acbc9e451e7318905eff658b43e6a3b9f54ea7 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 15:10:44 +0200 Subject: [PATCH 44/46] store jobs in user home --- larixite/fdmnes.py | 55 +++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 1aa48e5..506c308 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -9,7 +9,7 @@ spectroscopy (XAS, XES, RIXS) from the atomic structures """ - +import time from dataclasses import dataclass from pathlib import Path from pymatgen.core import __version__ as pymatgen_version, Element, Molecule @@ -73,6 +73,7 @@ class FdmnesXasInput: params: dict[str, bool] | None = None #: enable/disable parameters for FDMNES optimize: bool = False #: optimize the input parameters spacer: str = " " #: spacer string for the FDMNES input text + outdir: str | Path | None = None #: path to the output directory of the FDMNES jobs def __post_init__(self): """Validate and optimize attributes""" @@ -113,7 +114,13 @@ def __post_init__(self): if self.optimize: self.params = self.optimize_params() #: set outdir to None before writing the input - self.outdir = None + if isinstance(self.outdir, str): + self.outdir = Path(self.outdir) + if self.outdir is None: + self.outdir = Path.home() / "larixite" + + #: store a list of jobs + self._jobs = [] @property def xs(self): @@ -378,35 +385,34 @@ def get_input( def write_input( self, inputtext: str | None = None, outdir: str | Path | None = None ) -> Path: - """Write the FDMNES input text to disk and return the output directory""" + """Write the FDMNES input text to disk and return the job output directory""" if inputtext is None: inputtext = self.get_input() - if outdir is None: - import tempfile - - outdir = ( - Path(tempfile.gettempdir()) / "larixite" / "fdmnes" / str(self._xs.name) - ) - outdir.mkdir(parents=True, exist_ok=True) - outdir = tempfile.mkdtemp(dir=outdir, prefix="job_") if isinstance(outdir, str): outdir = Path(outdir) - outdir.mkdir(parents=True, exist_ok=True) + if outdir is None: + outdir = self.outdir + tstamp = time.strftime("%y%m%d_%H%M%S") #: 260518_144910 + jobdir = outdir / f"{self.fileout_prefix}_{tstamp}" + jobdir.mkdir(parents=True, exist_ok=True) fileout_name = f"{self.fileout_prefix}.inp" - fnout = outdir / fileout_name + fnout = jobdir / fileout_name with open(fnout, "w") as fp: fp.write(inputtext) - with open(outdir / "fdmfile.txt", "w") as fp: + with open(jobdir / "fdmfile.txt", "w") as fp: fp.write(f"1\n{fileout_name}\n") - logger.info(f"written `{fnout}`") - self.outdir = outdir - return outdir + logger.info(f"written {fp.name}") + self._jobs.append(jobdir) + return jobdir - def write_sbatch(self, template: str | Path | None = None, ncpus: int = 8): + def write_sbatch(self, jobdir: str | Path | None = None, template: str | Path | None = None, ncpus: int = 8): """Generates a SBATCH file (SLURM workload manager) using a template Arguments --------- + jobdir: str, Path or None + path to the job directory to write the SBATCH file + if None: uses last job directory template: str, Path or None path to the SBATCH template file if None, the default template will be used (`fdmnes_sbatch_esrf.tmpl`) @@ -420,10 +426,15 @@ def write_sbatch(self, template: str | Path | None = None, ncpus: int = 8): """ if template is None: template = self.tmplpath / "fdmnes_sbatch_esrf.tmpl" - if self.outdir is None: - logger.error("execute `write_input` first") - return - sbatchout = self.outdir / f"{self.fileout_prefix}.sbatch" + if jobdir is None: + try: + jobdir = self._jobs[-1] + except IndexError: + logger.error("execute `write_input` first") + return + if isinstance(jobdir, str): + jobdir = Path(jobdir) + sbatchout = jobdir / f"{self.fileout_prefix}.sbatch" kwargs = {"jobname": self.fileout_prefix, "ncpus": ncpus,} with open(sbatchout, "w") as fp, open(template) as tp: From 475600d944130a90d7892da927fcd8ae1ceca360 Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 15:15:45 +0200 Subject: [PATCH 45/46] use .larixite --- examples/fdmnes_xas_workflow.ipynb | 329 ++--------------------------- larixite/fdmnes.py | 2 +- larixite/struct2xas.py | 2 +- 3 files changed, 22 insertions(+), 311 deletions(-) diff --git a/examples/fdmnes_xas_workflow.ipynb b/examples/fdmnes_xas_workflow.ipynb index d2eb26f..455ed41 100644 --- a/examples/fdmnes_xas_workflow.ipynb +++ b/examples/fdmnes_xas_workflow.ipynb @@ -10,7 +10,7 @@ "- Author: Mauro Rovezzi\n", "- Contact: mauro.rovezzi@esrf.fr\n", "- Status: *in progress...*\n", - "- Last update: 2026-05-12" + "- Last update: 2026-05-19" ] }, { @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -60,25 +60,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 4 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", - " struct = structs.parse_structures(primitive=False)[frame]\n", - "\u001b[32;21m[larixite.struct | INFO ] 0: ZnO_wurtzite_Weber1923_COD-1011259.cif\u001b[0m\n", - "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 8 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", - " struct = structs.parse_structures(primitive=False)[frame]\n", - "\u001b[32;21m[larixite.struct | INFO ] 1: ZnO_mp-2133.cif\u001b[0m\n", - "/home/esrf/rovezzi/devel/larixite/larixite/struct/__init__.py:88: UserWarning: Issues encountered while parsing CIF: 32 fractional coordinates rounded to ideal values to avoid issues with finite precision.\n", - " struct = structs.parse_structures(primitive=False)[frame]\n", - "\u001b[32;21m[larixite.struct | INFO ] 2: ZnO_mp-997630.cif\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "structs = get_structs_from_dir(structsdir, abs, globstr=f\"*{abs}*\")" ] @@ -92,20 +76,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unique sites for ZnO_wurtzite_Weber1923_COD-1011259.cif\n", - "iunique\tistruct\tlabel\tfrac_coords \toccu\tcart_coords \tnsites\twyckoff\tspecies_string\n", - "0 (abs)\t0 \tZn1 \t[0.33333333 0.66666667 0. ]\t1 \t[3.58065429e-16 1.93470075e+00 2.05189571e-16]\t2 \t2b \tZn2+ \n", - "1 \t2 \tO1 \t[0.33333333 0.66666667 0.375 ]\t1 \t[3.58065429e-16 1.93470075e+00 1.95975000e+00]\t2 \t2b \tO2- \n" - ] - } - ], + "outputs": [], "source": [ "sg = structs[0]\n", "sg.show_unique_sites()" @@ -120,31 +93,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sg.struct" ] @@ -158,20 +109,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unique sites for ZnO_wurtzite_Weber1923_COD-1011259.cif\n", - "iunique\tistruct\tlabel\tfrac_coords \toccu\tcart_coords \tnsites\twyckoff\tspecies_string\n", - "0 (abs)\t0 \tZn1 \t[0.33333333 0.66666667 0. ]\t1 \t[3.58065429e-16 1.93470075e+00 2.05189571e-16]\t2 \t2b \tZn2+ \n", - "1 \t2 \tO1 \t[0.33333333 0.66666667 0.375 ]\t1 \t[3.58065429e-16 1.93470075e+00 1.95975000e+00]\t2 \t2b \tO2- \n" - ] - } - ], + "outputs": [], "source": [ "sg.absorber_idx = 0\n", "sg.show_unique_sites()" @@ -186,76 +126,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "! FDMNES input file generated by larixite\n", - "! 2026-05-12 16:00:04\n", - "! larixite : 2025.5.1\n", - "! pymatgen : 2025.6.14\n", - "Header\n", - "Python\n", - "Comment\n", - " ZnO_wurtzite_Weber1923_COD-1011259.cif: Zn (30) K edge\n", - "Filout\n", - " job\n", - "Edge\n", - " K\n", - "!\n", - "Range\n", - " -5 0.5 10\n", - "! Energpho !output energy as photon energy (default relative to Fermi level)\n", - "Radius\n", - " 5.00\n", - "!\n", - "Green\n", - "!\n", - "Quadrupole\n", - "!\n", - "! Polarize \n", - "! =>TODO\n", - "! TDDFT\n", - "Memory_save\n", - "! Relativism\n", - "! Spinorbit\n", - "! Density\n", - "! Density_all\n", - "!\n", - "! Hedin !PBE96 is default since March 2026\n", - "!\n", - "! SpGr_atom !`Full_atom` is default since March 2026\n", - "!\n", - "! SCF\n", - "R_self\n", - " 3.5\n", - "N_self\n", - " 100\n", - "P_self\n", - " 0.025\n", - "! SCF_exc\n", - "! Vmax\n", - "! Atom_conf\n", - "!\n", - "Z_absorber\n", - " 30\n", - "Spgroup\n", - " 186\n", - "Occupancy\n", - "Crystal\n", - " 3.351 3.351 5.226 90.0 90.0 119.99999999999999\n", - " 30 0.3333333333 0.6666666667 0.0000000000 1.000 ! Zn1 2b Zn2+\n", - " 8 0.3333333333 0.6666666667 0.3750000000 1.000 ! O1 2b O2-\n", - "!\n", - "Convolution\n", - "End\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "f = FdmnesXasInput(sg, absorber=abs, struct_type=\"crystal\", radius=5, erange=\"-5 0.5 10\", optimize=True)\n", "text = f.get_input()\n", @@ -271,191 +144,29 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32;21m[larixite.fdmnes | INFO ] written `/tmp_14_days/larixite/job.inp`\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ - "outdir = f.write_input(outdir=\"/tmp_14_days/larixite\")" + "jobdir = f.write_input()" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32;21m[larixite.fdmnes | INFO ] written /tmp_14_days/larixite/job.sbatch\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "f.write_sbatch()" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " FDMNES program, Revision 3rd of October 2025 \n", - " Date = 12 05 2026 \n", - " Time = 14 h 00 mn 05 s \n", - "\n", - " python \n", - " edge \n", - " range \n", - " radius \n", - " green \n", - " quadrupol\n", - " memory_sa\n", - " r_self \n", - " n_self \n", - " p_self \n", - " z_absorbe\n", - " spgroup \n", - " occupancy\n", - " crystal \n", - "\n", - " Filout: job\n", - " Threshold: Zinc K1 edge\n", - "\n", - " Sequential calculation\n", - "\n", - " Number of calculated non equivalent absorbing atom = 1\n", - "\n", - " E_edge = 9659.00 eV\n", - "\n", - " Cluster radius = 5.00 A, nb. of atom = 42\n", - " Fermi energy calculation : cluster radius = 3.50 A, nb. of atom = 18\n", - " Potential sup calculation: cluster radius = 7.50 A, nb. of atom = 144\n", - "\n", - " Point group : 3m (C3v )\n", - "\n", - " Point group used : 3 (C3 )\n", - "\n", - " ia Z mult ch_val ch_core ch_total ch_out Atom charge\n", - " 1 30 1 11.520 16.998 28.518 0.001 1.482\n", - " 2 8 1 5.374 2.000 7.374 0.004 0.626\n", - " 3 8 3 5.372 2.000 7.372 0.003 0.628\n", - " 4 30 3 10.478 17.992 28.470 0.140 1.530\n", - " 5 30 3 10.478 17.992 28.470 0.140 1.530\n", - " 6 8 1 5.359 2.000 7.359 0.094 0.641\n", - " 7 30 3 10.477 17.992 28.469 0.172 1.531\n", - " 8 30 3 10.477 17.992 28.469 0.172 1.531\n", - "\n", - " Cycle 1, Fermi energy = -7.401 eV, Occupancy val abs = 10.187\n", - "\n", - " Cycle 1: Total energy = -641497.9 eV,\n", - "\n", - " ia Z Energy_Tot Charge Ch-Ch_i pop_orb_val(L) L Radius\n", - " 1 30 -48748.1 1.988 0.506 10.187 2 1.19147\n", - " 2 8 -2102.7 -0.206 -0.832 4.370 1 0.96556\n", - " 3 8 -2101.3 -0.196 -0.824 4.361 1 0.96556\n", - " 4 30 -48530.7 1.826 0.296 9.657 2 1.09321\n", - " 5 30 -48511.3 1.870 0.339 9.627 2 1.09321\n", - " 6 8 -2109.0 -0.309 -0.950 4.386 1 0.96556\n", - " 7 30 -48518.0 1.838 0.307 9.624 2 1.09321\n", - " 8 30 -48518.0 1.838 0.307 9.624 2 1.09321\n", - "\n", - " ia Z it igr ipr iap posx posy posz igrpt PtGrName Comp Axe Mag\n", - " 1 30 0 1 0 1 0.00000 0.00000 0.00000 16 3 T T F\n", - " 2 8 2 3 2 2 0.00000 0.00000 1.95975 16 3 T T F\n", - " 3 8 2 4 2 5 0.00000 1.93470 -0.65325 1 1 T F F\n", - " 4 30 1 2 1 9 0.00000 1.93470 -2.61300 1 1 T F F\n", - " 5 30 1 2 1 11 0.00000 1.93470 2.61300 1 1 T F F\n", - " 6 8 2 3 2 12 0.00000 0.00000 -3.26625 16 3 T T F\n", - " 7 30 1 1 1 17 3.35100 0.00000 0.00000 1 1 T F F\n", - " 8 30 1 1 1 18 1.67550 2.90205 0.00000 1 1 T F F\n", - " 9 8 2 3 2 22 3.35100 0.00000 1.95975 1 1 T F F\n", - " 10 8 2 3 2 24 1.67550 2.90205 1.95975 1 1 T F F\n", - " 11 8 2 4 2 25 3.35100 1.93470 -0.65325 1 1 T F F\n", - " 12 30 1 2 1 29 3.35100 1.93470 -2.61300 1 1 T F F\n", - " 13 30 1 2 1 32 3.35100 1.93470 2.61300 1 1 T F F\n", - " 14 8 2 3 2 35 1.67550 2.90205 -3.26625 1 1 T F F\n", - " 15 8 2 3 2 37 3.35100 0.00000 -3.26625 1 1 T F F\n", - " 16 8 2 4 2 42 0.00000 1.93470 4.57275 1 1 T F F\n", - "\n", - " V0bdcF = -12.49955 eV, Vhbdc = -3.671 eV, rsbdc = 2.194 a.u.\n", - "\n", - " ipr Z Atom-Charge Atom-Radius Ionic-Charge Ionic-Radius Vmft\n", - " 0 30* 1.482 1.191 3.825 0.740 -18.328\n", - " 1 30 1.530 1.093 3.518 0.740 -18.328\n", - " 2 8 0.626 0.966 -1.318 1.400 -18.328\n", - "Epsii used = 9497.266 eV\n", - " Number of Energies = 31\n", - " Energy \n", - " -5.00000 2.6106000E-02\n", - " -4.50000 1.4966086E-04\n", - " -4.00000 4.1089473E-03\n", - " -3.50000 2.6891110E-02\n", - " -3.00000 8.4292987E-03\n", - " -2.50000 1.0047078E-03\n", - " -2.00000 6.6510232E-04\n", - " -1.50000 7.4967038E-04\n", - " -1.00000 8.9585450E-04\n", - " -0.50000 1.0778311E-03\n", - " 0.00000 1.3036424E-03\n", - " 0.50000 1.6056663E-03\n", - " 1.00000 2.0341465E-03\n", - " 1.50000 2.6863665E-03\n", - " 2.00000 4.0265447E-03\n", - " 2.50000 7.2399656E-03\n", - " 3.00000 1.4748046E-02\n", - " 3.50000 2.8747562E-02\n", - " 4.00000 3.3309915E-02\n", - " 4.50000 2.6993936E-02\n", - " 5.00000 3.3241095E-02\n", - " 5.50000 3.5329115E-02\n", - " 6.00000 3.2082621E-02\n", - " 6.50000 3.2082223E-02\n", - " 7.00000 3.6461065E-02\n", - " 7.50000 4.4630562E-02\n", - " 8.00000 5.8971353E-02\n", - " 8.50000 7.4217152E-02\n", - " 9.00000 8.8816372E-02\n", - " 9.50000 1.0986046E-01\n", - " 10.0000 1.2665657E-01\n", - "\n", - " Arctangent model\n", - " Gamma_max = 15.00, Ecent = 30.00, Elarg = 30.00\n", - " Gamma_hole = 1.67, E_cut = -2.000, Shift = 0.000 eV\n", - " E_(eV) Width_(eV) lambda_(A)\n", - " -5.000 1.670 0.000\n", - " -2.000 1.670 0.000\n", - " -1.500 1.673 2923.701\n", - " -1.000 1.680 780.025\n", - " -0.500 1.693 367.185\n", - " 0.000 1.711 217.447\n", - " 1.000 1.761 105.634\n", - " 2.000 1.832 64.037\n", - " 3.000 1.924 43.703\n", - " 4.000 2.037 32.095\n", - " 5.000 2.171 24.769\n", - " 6.000 2.327 19.810\n", - " 7.000 2.505 16.274\n", - " 8.000 2.706 13.653\n", - " 9.000 2.930 11.650\n", - " 10.000 3.177 10.080\n" - ] - } - ], + "outputs": [], "source": [ - "! module purge && module load fdmnes && cd {outdir.as_posix()} && {fdmnes_executable}\n" + "! module purge && module load fdmnes && cd {jobdir.as_posix()} && {fdmnes_executable}\n" ] }, { diff --git a/larixite/fdmnes.py b/larixite/fdmnes.py index 506c308..f463382 100644 --- a/larixite/fdmnes.py +++ b/larixite/fdmnes.py @@ -117,7 +117,7 @@ def __post_init__(self): if isinstance(self.outdir, str): self.outdir = Path(self.outdir) if self.outdir is None: - self.outdir = Path.home() / "larixite" + self.outdir = Path.home() / ".larixite" / "fdmnes" #: store a list of jobs self._jobs = [] diff --git a/larixite/struct2xas.py b/larixite/struct2xas.py index e2287be..6389dd9 100644 --- a/larixite/struct2xas.py +++ b/larixite/struct2xas.py @@ -34,7 +34,7 @@ #from larch.io import read_ascii #from larch.math.convolution1D import lin_gamma, conv -user_larchdir = Path.home() / ".larch" +user_larchdir = Path.home() / ".larixite" try: import py3Dmol From 660712e810e61a1b930d5c7b73ad8828d04e198e Mon Sep 17 00:00:00 2001 From: Mauro Rovezzi Date: Tue, 19 May 2026 15:27:59 +0200 Subject: [PATCH 46/46] latest changes before release --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 515dd41..4c45d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The versioning scheme followed is `YYYY.MAJOR.MINOR`: - Update to FDMNES version March 2026 - Add `make_sbatch` with a template for the ESRF SLURM scheduler - Add `optimize : bool = False` option in `FdmnesXasInput` class to control if the input parameters are optimized (= more accurate calcuation, but slower) or not + - `write_input()` defaults to `$HOME/.larixite/fdmnes/{job_prefix}_{YYMMDD_HHMMSS}` ### `struc2xas`