diff --git a/.gitignore b/.gitignore index c317064..93750fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,17 @@ +# Python-generated files AGENTS.md +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Documentation build output +docs/build/ + +# Virtual environments +.venv + +# Extracted test data (kept as ZIP archive) +tests/data/test_loading/ diff --git a/docs/build/doctrees/api.doctree b/docs/build/doctrees/api.doctree deleted file mode 100644 index 90c95b5..0000000 Binary files a/docs/build/doctrees/api.doctree and /dev/null differ diff --git a/docs/build/doctrees/contributing.doctree b/docs/build/doctrees/contributing.doctree deleted file mode 100644 index b0a1f2d..0000000 Binary files a/docs/build/doctrees/contributing.doctree and /dev/null differ diff --git a/docs/build/doctrees/development.doctree b/docs/build/doctrees/development.doctree deleted file mode 100644 index 4906f73..0000000 Binary files a/docs/build/doctrees/development.doctree and /dev/null differ diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle deleted file mode 100644 index fe5489c..0000000 Binary files a/docs/build/doctrees/environment.pickle and /dev/null differ diff --git a/docs/build/doctrees/examples.doctree b/docs/build/doctrees/examples.doctree deleted file mode 100644 index 42cd537..0000000 Binary files a/docs/build/doctrees/examples.doctree and /dev/null differ diff --git a/docs/build/doctrees/index.doctree b/docs/build/doctrees/index.doctree deleted file mode 100644 index db567f7..0000000 Binary files a/docs/build/doctrees/index.doctree and /dev/null differ diff --git a/docs/build/doctrees/installation.doctree b/docs/build/doctrees/installation.doctree deleted file mode 100644 index 3532ca0..0000000 Binary files a/docs/build/doctrees/installation.doctree and /dev/null differ diff --git a/docs/build/doctrees/introduction.doctree b/docs/build/doctrees/introduction.doctree deleted file mode 100644 index c56e0f3..0000000 Binary files a/docs/build/doctrees/introduction.doctree and /dev/null differ diff --git a/docs/build/doctrees/license.doctree b/docs/build/doctrees/license.doctree deleted file mode 100644 index cd2d89b..0000000 Binary files a/docs/build/doctrees/license.doctree and /dev/null differ diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo deleted file mode 100644 index b86aef7..0000000 --- a/docs/build/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: a6a60257adb09a6b8793dd150e758cef -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/.buildinfo.bak b/docs/build/html/.buildinfo.bak deleted file mode 100644 index a3aaac2..0000000 --- a/docs/build/html/.buildinfo.bak +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 69ef75d2d28c03cf9e107b4a1f000fd6 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/.doctrees/api.doctree b/docs/build/html/.doctrees/api.doctree deleted file mode 100644 index 90c95b5..0000000 Binary files a/docs/build/html/.doctrees/api.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/contributing.doctree b/docs/build/html/.doctrees/contributing.doctree deleted file mode 100644 index 3008315..0000000 Binary files a/docs/build/html/.doctrees/contributing.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/development.doctree b/docs/build/html/.doctrees/development.doctree deleted file mode 100644 index 4906f73..0000000 Binary files a/docs/build/html/.doctrees/development.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/environment.pickle b/docs/build/html/.doctrees/environment.pickle deleted file mode 100644 index a6b719f..0000000 Binary files a/docs/build/html/.doctrees/environment.pickle and /dev/null differ diff --git a/docs/build/html/.doctrees/examples.doctree b/docs/build/html/.doctrees/examples.doctree deleted file mode 100644 index 42cd537..0000000 Binary files a/docs/build/html/.doctrees/examples.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/index.doctree b/docs/build/html/.doctrees/index.doctree deleted file mode 100644 index db567f7..0000000 Binary files a/docs/build/html/.doctrees/index.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/installation.doctree b/docs/build/html/.doctrees/installation.doctree deleted file mode 100644 index 3532ca0..0000000 Binary files a/docs/build/html/.doctrees/installation.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/introduction.doctree b/docs/build/html/.doctrees/introduction.doctree deleted file mode 100644 index c56e0f3..0000000 Binary files a/docs/build/html/.doctrees/introduction.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/license.doctree b/docs/build/html/.doctrees/license.doctree deleted file mode 100644 index cd2d89b..0000000 Binary files a/docs/build/html/.doctrees/license.doctree and /dev/null differ diff --git a/docs/build/html/_modules/index.html b/docs/build/html/_modules/index.html deleted file mode 100644 index ad0c9d4..0000000 --- a/docs/build/html/_modules/index.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - Overview: module code — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
- - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/_interface_handler.html b/docs/build/html/_modules/spinanalysis/_interface_handler.html deleted file mode 100644 index 370d91a..0000000 --- a/docs/build/html/_modules/spinanalysis/_interface_handler.html +++ /dev/null @@ -1,1294 +0,0 @@ - - - - - - - - spinanalysis._interface_handler — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - - -
  • -
  • -
-
-
-
-
- -

Source code for spinanalysis._interface_handler

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-(c) M. Sc. Florian Quintes, 2021-2026
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-from typing import Any, Sequence
-
-import numpy as np
-from numpy.typing import NDArray
-from static_radical_pair.radpair import do_simulation_multicore, do_simulation
-from teacups.simulations import teacups_multicore
-from teacups.simulations import teacups
-from oop_eseem.opossum import (
-    oop_eseem,
-    oop_eseem_distribution,
-    _get_multi_gauss_distribution,
-)
-
-from PySpin.plotter import plot
-import scipy.optimize as optimize
-from copy import deepcopy
-from functools import partial
-
-
-
-[docs] -def plot_callback( - xk: NDArray[np.float64], - *_, - Sys: Any = None, - Exp: Any = None, - SimOpt: Any = None, - FitOpt: Any = None, - Var: Any = None, - **_kwargs, -) -> None: - """ - Plot the current optimization state in the graphical user interface. - - Parameters - ---------- - xk : np.array - Current best guess vector. - *_ : arbitrary - Unused arguments passed by some optimization routines to the callback - function. - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - *_kwargs : arbitrary - Unused keyword arguments passed by some optimization routines to the - callback function. - - Raises - ------ - ValueError - Raised if the selected simulation routine is unknown. - - Returns - ------- - None. - - """ - current_Sys = guess2Sys(xk, Sys, Var, SimOpt) - SimOpt.mode = "simulation" - - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation_multicore(current_Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - simulated_spectra = teacups_multicore(current_Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(current_Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(current_Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() - Exp.spec_sim = simulated_spectra - data = np.array([Exp.spec_sim, Exp.int]) - label = ["current best Fit", "exp. Data"] - if SimOpt.routine.lower() in ("static_radpair", "teacups"): - plot(FitOpt.window, Exp.B_z, data, FitOpt.window.canvas_1, labels=label) - else: - plot( - FitOpt.window, - Exp.time_axis, - data, - FitOpt.window.canvas_1, - labels=label, - ) - - plot( - FitOpt.window, - current_Sys.distribution[0], - current_Sys.distribution[1], - FitOpt.window.canvas_distribution, - labels=["current Distribution"], - )
- - - -
-[docs] -def spinanalysis2scipy(x: NDArray[np.float64], *objects: Any) -> float: - """ - Objective function for the scipy.optimize interface. - - Used by scipy.optimize routines. - - Parameters - ---------- - x : np.array - Current parameter vector of the optimizer. - *objects : object - Additional objects required for the simulation and fitting interface. - - Raises - ------ - ValueError - Raised if the selected simulation routine is unknown. - - Returns - ------- - error : float - Sum of squared residuals between experimental and simulated data. - - """ - Sys_def, Exp, SimOpt, Var = objects - - Sys = guess2Sys(x, Sys_def, Var, SimOpt) - - SimOpt.mode = "simulation" - - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - simulated_spectra = teacups_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() - Exp.spec_sim = simulated_spectra - - error = ((Exp.int.real - Exp.spec_sim.real) ** 2).sum() - - return error
- - - -
-[docs] -def spinanalysis2scipy_singlecore(x: NDArray[np.float64], *objects: Any) -> float: - """ - Objective function for the scipy.optimize interface. - - Used by scipy.optimize.differential_evolution. - - Parameters - ---------- - x : np.array - Current parameter vector of the optimizer. - *objects : object - Additional objects required for the simulation and fitting interface. - - Raises - ------ - ValueError - Raised if the selected simulation routine is unknown. - - Returns - ------- - error : float - Sum of squared residuals between experimental and simulated data. - - """ - Sys_def, Exp, SimOpt, Var = objects - - Sys = guess2Sys(x, Sys_def, Var, SimOpt) - - SimOpt.mode = "fitting" - - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - simulated_spectra = teacups(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() - Exp.spec_sim = simulated_spectra - - error = ((Exp.int.real - Exp.spec_sim.real) ** 2).sum() - - return error
- - - -
-[docs] -def spinanalysis2scipy_res( - x: NDArray[np.float64], *objects: Any -) -> NDArray[np.float64]: - """ - Residual function for scipy.optimize least-squares algorithms. - - Returns the absolute residuals between simulation and experiment. - - Used by scipy.optimize routines. - - Parameters - ---------- - x : np.array - Current parameter vector of the optimizer. - *objects : object - Additional objects required for the simulation and fitting interface. - - Raises - ------ - ValueError - Raised if the selected simulation routine is unknown. - - Returns - ------- - error : np.array - One-dimensional array containing the absolute residuals. - - """ - Sys_def, Exp, SimOpt, Var = objects - - Sys = guess2Sys(x, Sys_def, Var, SimOpt) - - SimOpt.mode = "simulation" - - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - simulated_spectra = teacups_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() - Exp.spec_sim = simulated_spectra - - error = abs(Exp.int.real - Exp.spec_sim.real) - - return error
- - - -
-[docs] -def guess2Sys(x: NDArray[np.float64], Sys: Any, Var: Any, SimOpt: Any) -> Any: - """ - Create a spin-system object from the current optimizer vector. - - Parameters - ---------- - x : np.array - Current parameter vector of the optimizer. - Sys : object - Reference spin-system object. - Var : object - Variation object. - SimOpt : object - Simulation options object. - - Returns - ------- - Sys_mod : object - Spin-system object corresponding to the current optimizer vector. - - """ - Sys_mod = deepcopy(Sys) - - n = 0 - for key in vars(Var): - if key in Var.non_vars: - pass - elif key in Var.single_vars: - if vars(Var)[key] > 0: - vars(Sys_mod)[key] = x[n] - n += 1 - else: - for i, parameter in enumerate(vars(Var)[key]): - if parameter > 0: - vars(Sys_mod)[key][i] = x[n] - n += 1 - - if hasattr(Var, "isotropic"): - for par in Var.isotropic: - vars(Sys_mod)[par][1] = vars(Sys_mod)[par][0] - vars(Sys_mod)[par][2] = vars(Sys_mod)[par][0] - - for key in vars(Sys): - if key.startswith("frame_group"): - for i, frame in enumerate(vars(Sys)[key]): - if not frame.endswith("_frame"): - frame = frame + "_frame" - if i == 0: - reference_frame = vars(Sys_mod)[frame] - else: - vars(Sys_mod)[frame] = reference_frame - - if Var.fit_distribution: - pos = np.zeros(Sys.distribution_order) - sigma = np.zeros(Sys.distribution_order) - intens = np.zeros(Sys.distribution_order) - for i in range(Sys.distribution_order): - intens[i] = x[n + i * 3] - sigma[i] = x[n + i * 3 + 1] - pos[i] = x[n + i * 3 + 2] - - intens /= intens.sum() - r_axis = np.linspace(SimOpt.min_r, SimOpt.max_r, SimOpt.r_points) - mg_distribution = _get_multi_gauss_distribution(intens, sigma, pos, r_axis) - Sys_mod.distribution = np.array([r_axis, mg_distribution]) - Sys._mgf_parameters = np.array([pos, intens, sigma]) - - return Sys_mod
- - - -
-[docs] -def get_random_x0( - boundaries: Sequence[tuple[float, float]], -) -> NDArray[np.float64]: - """ - Generate a random initial guess within the variation boundaries. - - Parameters - ---------- - boundaries : list - Lower and upper bounds of the fitted parameters. - - Returns - ------- - x0 : np.array - Randomly generated initial parameter vector. - - """ - x0 = np.empty(len(boundaries)) - - for i, bounds in enumerate(boundaries): - x0[i] = np.random.uniform(bounds[0], bounds[1]) - - return x0
- - - -
-[docs] -def dualannealing(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.dual_annealing for global optimization. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - - """ - Var.get_boundaries(Sys) - - settings = dict(args=(Sys, Exp, SimOpt, Var), bounds=Var.boundaries) - - if FitOpt.gui: - settings["callback"] = partial( - plot_callback, - Sys=Sys, - Exp=Exp, - SimOpt=SimOpt, - FitOpt=FitOpt, - Var=Var, - ) - - if FitOpt.x0 is not None: - settings["x0"] = FitOpt.x0 - - if FitOpt.method is not None: - settings["minimizer_kwargs"] = dict() - settings["minimizer_kwargs"]["method"] = FitOpt.method - - if FitOpt.maxiter_minimizer is not None: - settings["minimizer_kwargs"]["maxiter"] = FitOpt.maxiter_minimizer - - if FitOpt.maxiter is not None: - settings["maxiter"] = FitOpt.maxiter - - if FitOpt.initial_temp is not None: - settings["initial_temp"] = FitOpt.initial_temp - - if FitOpt.restart_temp_ratio is not None: - settings["restart_temp_ratio"] = FitOpt.restart_temp_ratio - - if FitOpt.visit is not None: - settings["visit"] = FitOpt.visit - - if FitOpt.accept is not None: - settings["accept"] = FitOpt.accept - - if FitOpt.maxfun is not None: - settings["maxfun"] = FitOpt.maxfun - - if FitOpt.no_local_search is not None: - settings["no_local_search"] = FitOpt.no_local_search - - results = optimize.dual_annealing(spinanalysis2scipy, **settings) - - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -def shgo(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.shgo for global optimization. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - - """ - Var.get_boundaries(Sys) - - settings = dict(args=(Sys, Exp, SimOpt, Var), bounds=Var.boundaries) - - if FitOpt.gui: - settings["callback"] = partial( - plot_callback, - Sys=Sys, - Exp=Exp, - SimOpt=SimOpt, - FitOpt=FitOpt, - Var=Var, - ) - - if FitOpt.method is not None: - settings["minimizer_kwargs"] = dict() - settings["minimizer_kwargs"]["method"] = FitOpt.method - - if FitOpt.maxiter_minimizer is not None: - options = dict(maxiter=FitOpt.maxiter_minimizer) - settings["minimizer_kwargs"]["options"] = options - - settings["options"] = dict() - - if FitOpt.maxiter is not None: - settings["options"]["maxiter"] = FitOpt.maxiter - - if FitOpt.n is not None: - settings["n"] = FitOpt.n - - if FitOpt.iters is not None: - settings["iters"] = FitOpt.iters - - if FitOpt.maxfev is not None: - settings["maxfev"] = FitOpt.maxfev - - if FitOpt.f_tol is not None: - settings["options"]["f_tol"] = FitOpt.f_tol - - if FitOpt.maxev is not None: - settings["options"]["maxev"] = FitOpt.maxev - - if FitOpt.maxtime is not None: - settings["options"]["maxtime"] = FitOpt.maxtime - - if FitOpt.minimize_every_iter is not None: - settings["options"]["minimize_every_iter"] = FitOpt.minimize_every_iter - - if FitOpt.local_iter is not None: - settings["options"]["local_iter"] = FitOpt.local_iter - - if FitOpt.sampling_method is not None: - settings["sampling_method"] = FitOpt.sampling_method - - results = optimize.shgo(spinanalysis2scipy, **settings) - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -def differential_evolution( - Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any -) -> Any: - """ - Run scipy.optimize.differential_evolution for global optimization. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - - """ - Var.get_boundaries(Sys) - - settings = dict(args=(Sys, Exp, SimOpt, Var), bounds=Var.boundaries) - - if FitOpt.gui: - settings["callback"] = partial( - plot_callback, - Sys=Sys, - Exp=Exp, - SimOpt=SimOpt, - FitOpt=FitOpt, - Var=Var, - ) - - if FitOpt.strategy is not None: - settings["strategy"] = FitOpt.strategy - - if FitOpt.maxiter is not None: - settings["maxiter"] = FitOpt.maxiter - - if FitOpt.popsize is not None: - settings["popsize"] = FitOpt.popsize - - if FitOpt.tol is not None: - settings["tol"] = FitOpt.tol - - if FitOpt.mutation is not None: - settings["mutation"] = FitOpt.mutation - - if FitOpt.recombination is not None: - settings["recombination"] = FitOpt.recombination - - if FitOpt.seed is not None: - settings["seed"] = FitOpt.seed - - if FitOpt.polish is not None: - settings["polish"] = FitOpt.polish - - if FitOpt.init is not None: - settings["init"] = FitOpt.init - - if FitOpt.atol is not None: - settings["atol"] = FitOpt.atol - - if FitOpt.updating is not None: - settings["updating"] = FitOpt.updating - - if FitOpt.x0 is not None: - settings["x0"] = FitOpt.x0 - - if FitOpt.cpu_cores == 0: - settings["workers"] = -1 - else: - settings["workers"] = FitOpt.cpu_cores - - results = optimize.differential_evolution(spinanalysis2scipy_singlecore, **settings) - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -def basinhopping(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.basinhopping for global optimization. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - - """ - Var.get_boundaries(Sys) - - settings = dict() - - if FitOpt.gui: - settings["callback"] = partial( - plot_callback, - Sys=Sys, - Exp=Exp, - SimOpt=SimOpt, - FitOpt=FitOpt, - Var=Var, - ) - - if FitOpt.x0 is not None: - settings["x0"] = FitOpt.x0 - else: - settings["x0"] = get_random_x0(Var.boundaries) - - basinhopping_bounds = BasinhoppingBounds(Var) - settings["accept_test"] = basinhopping_bounds - - settings["minimizer_kwargs"] = dict() - settings["minimizer_kwargs"]["args"] = (Sys, Exp, SimOpt, Var) - - if FitOpt.method is not None: - settings["minimizer_kwargs"]["method"] = FitOpt.method - settings["minimizer_kwargs"]["bounds"] = Var.boundaries - - if FitOpt.T is not None: - settings["T"] = FitOpt.T - - if FitOpt.niter is not None: - settings["niter"] = FitOpt.niter - - if FitOpt.stepsize is not None: - basinhopping_steps = BasinhoppingStep(Var, FitOpt.stepsize) - else: - basinhopping_steps = BasinhoppingStep(Var) - settings["take_step"] = basinhopping_steps - - if FitOpt.interval is not None: - settings["interval"] = FitOpt.interval - - if FitOpt.disp is not None: - settings["disp"] = FitOpt.disp - - basinhopping_status = BasinhoppingStatus(Sys, Var, FitOpt.disp) - settings["callback"] = basinhopping_status - - if FitOpt.niter_success is not None: - settings["niter_success"] = FitOpt.niter_success - - if FitOpt.seed is not None: - settings["seed"] = FitOpt.seed - - if FitOpt.target_accept_rate is not None: - settings["target_accept_rate"] = FitOpt.target_accept_rate - - if FitOpt.stepwise_factor is not None: - settings["stepwise_factor"] = FitOpt.stepwise_factor - - results = optimize.basinhopping(spinanalysis2scipy, **settings) - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -def least_squares(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.least_squares for nonlinear optimization. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - - """ - Var.get_boundaries(Sys) - - lb = [] - ub = [] - for bounds in Var.boundaries: - lb.append(bounds[0]) - ub.append(bounds[1]) - - lb = np.array(lb) - ub = np.array(ub) - - settings = dict(args=(Sys, Exp, SimOpt, Var), bounds=(lb, ub)) - - if FitOpt.x0 is not None: - settings["x0"] = FitOpt.x0 - else: - settings["x0"] = get_random_x0(Var.boundaries) - - if FitOpt.method is not None: - settings["method"] = FitOpt.method - - if FitOpt.method == "lm": - del settings["bounds"] - - if FitOpt.ftol is not None: - settings["ftol"] = FitOpt.ftol - - if FitOpt.xtol is not None: - settings["xtol"] = FitOpt.xtol - - if FitOpt.gtol is not None: - settings["gtol"] = FitOpt.gtol - - if FitOpt.loss is not None: - settings["loss"] = FitOpt.loss - - if FitOpt.f_scale is not None: - settings["f_scale"] = FitOpt.f_scale - - if FitOpt.max_nfev is not None: - settings["max_nfev"] = FitOpt.max_nfev - - if FitOpt.tr_solver is not None: - settings["tr_solver"] = FitOpt.tr_solver - - if FitOpt.verbose is not None: - settings["verbose"] = FitOpt.verbose - - results = optimize.least_squares(spinanalysis2scipy_res, **settings) - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -def minimize(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.minimize for local optimization. - - Minimize provides multiple local optimization routines such as Nelder-Mead, - COBYLA, Powell, CG and so on. - - Parameters - ---------- - Sys : object - Reference spin-system object. - Exp : object - Experimental data object. - SimOpt : object - Simulation options object. - Var : object - Variation object describing the fitted parameters. - FitOpt : object - Fitting options object. - - Returns - ------- - best_Sys : object - Best spin-system object found by the optimizer. - results : str, optional - Results of the scipy optimization. Only for the GUI. - - """ - Var.get_boundaries(Sys) - - settings = dict(args=(Sys, Exp, SimOpt, Var), bounds=Var.boundaries) - if FitOpt.gui: - settings["callback"] = partial( - plot_callback, - Sys=Sys, - Exp=Exp, - SimOpt=SimOpt, - FitOpt=FitOpt, - Var=Var, - ) - - if FitOpt.x0 is not None: - settings["x0"] = FitOpt.x0 - else: - settings["x0"] = get_random_x0(Var.boundaries) - - if FitOpt.method is not None: - settings["method"] = FitOpt.method - - if FitOpt.maxiter is not None: - settings["options"] = dict() - settings["options"]["maxiter"] = FitOpt.maxiter - - results = optimize.minimize(spinanalysis2scipy, **settings) - best_Sys = guess2Sys(results["x"], Sys, Var, SimOpt) - - if FitOpt.gui: - return best_Sys, results - print(results) - - return best_Sys
- - - -
-[docs] -class BasinhoppingBounds: - """ - Acceptance test for the scipy.optimize.basinhopping algorithm. - - Attributes - ---------- - xmin : np.array - Lower bounds for the varied parameters. - xmax : np.array - Upper bounds for the varied parameters. - Var : object - Object of class Variation from the epr_setup module. - - Methods - ------- - __call__(**kwargs) - Check if the current guess is within the bounds. - - """ - - def __init__(self, Var: Any) -> None: - self.xmin, self.xmax = np.array(Var.boundaries).T - - return None - -
-[docs] - def __call__(self, **kwargs) -> bool: - """ - Check if the current guess is within the bounds. - - Parameters - ---------- - **kwargs : list - Varied parameters. - - Returns - ------- - bool - True if the guess is within the bounds, False if not. - - """ - x = kwargs["x_new"] - test_min = bool(np.all(x >= self.xmin)) - test_max = bool(np.all(x <= self.xmax)) - - return test_max and test_min
-
- - - -
-[docs] -class BasinhoppingStep: - """ - Step generator for the scipy.optimize.basinhopping algorithm. - - Attributes - ---------- - stepsize : float, optional - Relative size of the random step with respect to the variation range. - rng : object - NumPy random number generator. - Var : object - Object of class Variation from the epr_setup module. - bounds : np.array - Parameter boundaries as a two-dimensional array. - lb : np.array - Lower bounds. - ub : np.array - Upper bounds. - var_range : np.array - Half the difference between lower and upper bounds. - dim_var : int - Number of variables. - - Methods - ------- - __call__(x) - Generate the next random step. - - """ - - def __init__(self, Var: Any, stepsize: float = 0.75) -> None: - self.stepsize = stepsize - self.rng = np.random.default_rng() - self.bounds = np.array(Var.boundaries) - self.lb = self.bounds[:, 0] - self.ub = self.bounds[:, 1] - self.var_range = 0.5 * (self.ub - self.lb) - self.dim_var = len(self.var_range) - - return None - -
-[docs] - def __call__(self, x: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Generate the next random step. - - Parameters - ---------- - x : np.array - Current guess. - - Returns - ------- - x : np.array - Current parameter vector after applying a random step. - - """ - x = self.check_guess(x) - - step_range = self.stepsize * self.var_range - - mask_lb = (x - self.lb) >= step_range - mask_ub = (self.ub - x) >= step_range - - l_range = np.zeros(self.dim_var) - u_range = np.zeros(self.dim_var) - - l_range[mask_lb] = step_range[mask_lb] - l_range[~mask_lb] = (x - self.lb)[~mask_lb] - - u_range[mask_ub] = step_range[mask_ub] - u_range[~mask_ub] = (self.ub - x)[~mask_ub] - - x += self.rng.uniform(-l_range, u_range) - - return x
- - -
-[docs] - def check_guess(self, x: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Check whether the current parameter vector is within the bounds. - - Out-of-bound values are replaced by randomly generated values inside the bounds. - - Parameters - ---------- - x : np.array - Current guess. - - Returns - ------- - x : np.array - Current guess. - - """ - choose_outliers = ~((x >= self.lb) & (x <= self.ub)) - - if choose_outliers.sum() > 0: - x_new = self.rng.uniform(self.lb, self.ub) - x[choose_outliers] = x_new[choose_outliers] - - return x
-
- - - -
-[docs] -class BasinhoppingStatus: - """ - Status callback for the scipy.optimize.basinhopping algorithm. - - Attributes - ---------- - best : float - Best objective-function value found so far. - def_Sys : object - Reference spin-system object. - Var : object - Variation object containing the parameter ranges. - verbose : bool, optional - Controls whether status information is printed. - xmin : np.array - Lower bounds for the varied parameters. - xmax : np.array - Upper bounds for the varied parameters. - - Methods - ------- - __call__(x, value, accepted) - Print status information. - - """ - - def __init__(self, Sys: Any, Var: Any, verbose: bool = False) -> None: - self.best = np.inf - self.def_Sys = deepcopy(Sys) - self.Var = Var - self.xmin, self.xmax = np.array(Var.boundaries).T - - if verbose is not None: - self.verbose = verbose - else: - self.verbose = False - - return None - -
-[docs] - def __call__(self, x: NDArray[np.float64], value: float, accepted: int) -> None: - """ - Print status information about the basinhopping progress. - - Parameters - ---------- - x : np.array - Current guess. - value : float - Objective-function value of the current parameter vector. - accepted : int - Acceptance status returned by the basinhopping algorithm. - - Returns - ------- - None - No return value. - - """ - mes_1 = "The current minimum with an error of {:.4f} ".format(value) - if self.check_bounds(x): - mes_2 = "was accepted" - - if value < self.best: - self.best = value - self.save_best(x) - mes_3 = " and is a new best minimum!" - else: - mes_3 = "." - else: - mes_2 = "was not accepted" - mes_3 = "." - - if self.verbose: - print(mes_1 + mes_2 + mes_3) - - return None
- - -
-[docs] - def check_bounds(self, x: NDArray[np.float64]) -> bool: - """ - Check if the current guess is within the bounds. - - Parameters - ---------- - x : np.array - Current guess. - - Returns - ------- - bool - True if the guess is within the bounds, False if not. - - """ - test_min = bool(np.all(x >= self.xmin)) - test_max = bool(np.all(x <= self.xmax)) - - return test_min and test_max
- - -
-[docs] - def save_best(self, x: NDArray[np.float64]) -> None: - """ - Create a spin-system object from the best parameter vector and save it. - - Parameters - ---------- - x : np.array - Current guess. - - Returns - ------- - None - No return value. - - """ - Sys = guess2Sys(x, self.def_Sys, self.Var, self.SimOpt) - Sys.save_spinsystem("basinhopping_current_best") - - return None
-
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/_wrappers.html b/docs/build/html/_modules/spinanalysis/_wrappers.html deleted file mode 100644 index 6807e87..0000000 --- a/docs/build/html/_modules/spinanalysis/_wrappers.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - - spinanalysis._wrappers — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis._wrappers

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-from time import time
-from copy import deepcopy
-from multiprocessing import cpu_count, Pool
-from itertools import repeat
-import numpy as np
-
-
-
-[docs] -def timer(func: callable) -> callable: - """ - Decorator function to measure time for one function call. - - Parameters - ---------- - func : callable - Function whose runtime will be measured. - - Returns - ------- - res : any - Result(s) of the function. - - """ - - def time_wrap(*args, **kwargs): - start = time() - res = func(*args, **kwargs) - runtime = time() - start - print("The runtime of {} is {:.3f} s".format(func.__name__, runtime)) - return res - - return time_wrap
- - - -
-[docs] -def function_benchmark(func: callable, niter: int = 100) -> callable: - """ - This decorateur will run the given function niter times and print the best, - the worst and the average runtime. - - Parameters - ---------- - func : callable - Function which will be benchmarked. - niter : int - Number of function calls. - - Returns - ------- - callable - Function with automatic benchmark. - - """ - - def benchmarked_function(*args, **kwargs): - times = np.empty(niter) - for i in range(times.shape[0]): - start = time() - func(*args, **kwargs) - times[i] = time() - start - - print("Runned a benchmark of {}.".format(func.__name__)) - - if times.min() > 1: - unit = "s" - elif times.min() > 1e-3: - unit = "ms" - times *= 1e3 - else: - unit = "μs" - times *= 1e6 - - print("Average time: {:.3f} {}".format(times.mean(), unit)) - print("Best time: {:.3f} {}".format(times.min(), unit)) - print("Worst time: {:3f} {}".format(times.max(), unit)) - - return benchmarked_function
- - - -
-[docs] -def multicore(simulation: callable) -> callable: - """ - Using multiprocessing.Pool() with starmap() for parallel computing of - various simulation routines using the easypairspin function interface - simulation(Sys, Exp, SimOpt). - - Parameters - ---------- - simulation : callable - Simulation function which uses the easypairspin interface (Sys, Exp, - SimOpt). - - Returns - ------- - multicore_wrapper : callable - The origin simulation callable as multicore version. - - """ - - def simulation_with_queue( - simulation: callable, - Sys: object, - Exp: object, - SimOpt: object, - queue: object, - num: int, - ) -> None: - intensity = simulation(Sys, Exp, SimOpt) - queue.put((num, intensity)) - - return None - - def multicore_wrapper(Sys: object, Exp: object, SimOpt: object) -> np.array: - if SimOpt.cpu_cores == 0: - SimOpt.cpu_cores = cpu_count() - - whole_spectrum = 1 * Exp.magnetic_field - whole_B_z = 1 * Exp.B_z - n_field_points = whole_spectrum.shape[0] - - points_per_core = n_field_points // SimOpt.cpu_cores - - Exp_list = np.empty(SimOpt.cpu_cores, dtype=object) - for core in range(SimOpt.cpu_cores): - Experimental = deepcopy(Exp) - start = core * points_per_core - if core + 1 < SimOpt.cpu_cores: - end = (core + 1) * points_per_core - Experimental.magnetic_field = whole_spectrum[start:end] - Experimental.B_z = whole_B_z[start:end] - else: - Experimental.magnetic_field = whole_spectrum[start:] - Experimental.B_z = whole_B_z[start:] - Exp_list[core] = Experimental - - # [Multi-Core Calculation] - pool = Pool(processes=SimOpt.cpu_cores) - single_intensities = pool.starmap( - simulation, zip(repeat(Sys), Exp_list, repeat(SimOpt)) - ) - pool.close() - pool.join() - - # queue = Queue() - - # processes = [] - # for i, Exp_i in enumerate(Exp_list): - # processes.append(Process(target=simulation_with_queue, - # args=(simulation, Sys, Exp_i, SimOpt, - # queue, i) - # ) - # ) - - # for p in processes: - # p.start() - - # for p in processes: - # p.join() - - # intensities = [queue.get() for p in processes] - # intensities.sort() - - # single_intensities = [intensity[1] for intensity in intensities] - - intens_arr_tuple = tuple(single_intensities) - intensity = np.hstack(intens_arr_tuple) - - return intensity - - return multicore_wrapper
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/core.html b/docs/build/html/_modules/spinanalysis/core.html deleted file mode 100644 index 44651a1..0000000 --- a/docs/build/html/_modules/spinanalysis/core.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - - spinanalysis.core — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.core

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-import mkl
-import spinanalysis._interface_handler as spo
-from genetic_radpair.genetic_classes import Genetic_Radpair
-from oop_eseem.opossum import (
-    oop_eseem,
-    oop_eseem_distance_distribution,
-    oop_eseem_distribution,
-)
-from teacups.simulations import teacups
-from static_radical_pair.radpair import do_simulation_multicore
-import sys
-import os
-import datetime
-import logging
-import numpy as np
-
-version = "v0.1.0"
-
-
-mkl.set_num_threads(1)
-
-
-
-[docs] -def start_log() -> None: - # TODO - """ - Diese Funktion soll mal Log-Dateien während der Nutzung anlegen. - Aktuell nicht nutzbar! Reine Baustelle! - """ - date = datetime.datetime.now() - - date = date.strftime("%d_%m_%Y__%H:%M:%S") - session_name = "EasyPairSpin_Session_at_" + date + ".log" - log_path = os.path.join(sys.prefix, "easypairspin", "logs") - - if not os.path.exists(log_path): - os.makedirs(log_path) - - logfile_name = os.path.join(log_path, session_name) - - fmt = "{asctime} - [{levelname:8}] - {module} {funcName} - {message}" - dfmt = "%d/%m/%Y %H:%M:%S" - - logging.basicConfig( - filename=logfile_name, - style="{", - format=fmt, - datefmt=dfmt, - level=logging.DEBUG, - ) - logging.info("Log startet") - return None
- - - -
-[docs] -def simulate(Sys: object, Exp: object, SimOpt: object) -> np.ndarray: - # TODO Hinschreiben, welche Simulationen möglich sind + Paper. - """ - Do various simulations with (spinpolarized) radical pairs. - - Parameters - ---------- - Sys : object - Spinsystem object of module 'epr_setup'. - Exp : object - Experimental object of module 'epr_setup'. simulated_spectra will be - saved in Exp.spec_sim. - SimOpt : object - SimulationOptions object of module 'epr_setup'. - - Raises - ------ - ValueError - Will be raised, if the given simulation routine in SimOpt.routine is an - invalid string. - - Returns - ------- - simulated_spectra : np.ndarray - Simulated spectra as a numpy.ndarray. - - Examples - -------- - - Basic simulation using static_radical_pair: - - >>> from epr_setup import Spinsystem, Experimental, SimulationOptions - >>> Sys = Spinsystem() - >>> Exp = Experimental() - >>> SimOpt = SimulationOptions() - >>> SimOpt.routine = 'static_radpair' - >>> simulate(Sys, Exp, SimOpt) - - Plot your result: - - >>> from plotting import plot_2D - >>> plot_2D(Exp.B_z , Exp.spec_sim) - - """ - SimOpt.mode = "simulation" - - # TODO an match case anpassen PYTHON 3.10 - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - if SimOpt.eigval_mode is True: - teacups(Sys, Exp, SimOpt) - simulated_spectra = np.ones((Exp.t_points, len(Exp.B_z))) - else: - SimOpt.mode = "fitting" - simulated_spectra = teacups(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis_tikhonov": - simulated_spectra = oop_eseem_distance_distribution(Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() - Exp.spec_sim = simulated_spectra - - return simulated_spectra
- - - -
-[docs] -def optimize( - Sys: object, - Exp: object, - SimOpt: object, - FitOpt: object, - Var: object, -) -> object: - # Hinschreiben welche Optimierungen möglich sind + Paper. - """ - Do various optimizations with all simulations available in - 'easypairspin()'. - - Parameters - ---------- - Sys : object - Spinsystem object of module 'epr_setup'. - Exp : object - Experimental object of module 'epr_setup'. - SimOpt : object - SimulationOptions object of module 'epr_setup'. - FitOpt : object - FittingOptions object of module 'epr_setup'. - Var : object - Variation object of module 'epr_setup'. - - Raises - ------ - ValueError - Will be raised, if the given simulation routine in FitOpt.routine is an - invalid string. - - Returns - ------- - best_Spinsystem: object - The best Spinsystem found during optimization. Object is of class - Spinsystem of module 'epr_setup'. - - Examples - -------- - - Basic optimization using genetic_radpair and static_radical_pair: - - >>> from epr_setup import Spinsystem, Experimental, SimulationOptions, - ... FittingOptions, Variation - >>> Sys = Spinsystem() - >>> Exp = Experimental() - >>> SimOpt = SimulationOptions() - >>> SimOpt.routine = 'static_radpair' - >>> FitOpt = FittingOptions() - >>> Var = Variation() - >>> Var.g1 = np.array([0.001, 0.003, 0.002]) - >>> best_Sys = optimize(Sys, Exp, SimOpt, FitOpt, Var) - - Plot your result: - - >>> from plotting import plot_2D - >>> simulate(best_Sys, Exp, SimOpt) - >>> plot_2D(Exp.B_z , Exp.spec_sim) - - - """ - # TODO an match case anpassen PYTHON 3.10 - - SimOpt.mode = "fitting" - - if FitOpt.routine.lower() == "genetic": - Gen_Rad = Genetic_Radpair(Sys, Exp, SimOpt, FitOpt, Var) - best_spinsystem = Gen_Rad.best_spinsystem - elif FitOpt.routine.lower() == "dual_annealing": - best_spinsystem = spo.dualannealing(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "shgo": - best_spinsystem = spo.shgo(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "differential_evolution": - best_spinsystem = spo.differential_evolution(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "basinhopping": - best_spinsystem = spo.basinhopping(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "least_squares": - best_spinsystem = spo.least_squares(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "minimize": - best_spinsystem = spo.minimize(Sys, Exp, SimOpt, FitOpt, Var) - else: - raise ValueError( - "Can't find an optimization routine named '{0}'".format(FitOpt.routine) - ) - return best_spinsystem
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/epr.html b/docs/build/html/_modules/spinanalysis/epr.html deleted file mode 100644 index 3cfdc4b..0000000 --- a/docs/build/html/_modules/spinanalysis/epr.html +++ /dev/null @@ -1,1450 +0,0 @@ - - - - - - - - spinanalysis.epr — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.epr

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-import numpy as np
-import scipy.constants as constant
-from spinanalysis import profiles
-
-
-
-[docs] -class EPR_Parameters: - """ - A class containing all parameters for various radical pair simulations. - - Attributes - ---------- - g1 : 1d-Array, np.float64 - g-Tensor of electron 1. - g2 : 1d-Array, np.float64 - g-Tensor of electron 2. - g_tri : 1d-Array, np.float64 - g-Tensor of a triplet radical. - g : 1d-Array, np.float64 - g-Tensor of a radical. - A1 : 1d-Array, np.float64 - A-Tensor of nuclei 1 in Megahertz. - A2 : 1d-Array, np.float64 - A-Tensor of nuclei 2 in Megahertz. - A3 : 1d-Array, np.float64 - A-Tensor of nuclei 3 in Megahertz. - A4 : 1d-Array, np.float64 - A-Tensor of nuclei 4 in Megahertz. - A5 : 1d-Array, np.float64 - A-Tensor of nuclei 5 in Megahertz. - A_eseem : float64 - Hyperfine coupling for the nuclei in OOP-ESEEM in Megahertz. - omega_I : float64 - Nuclei frequency in OOP-ESEEM in Megahertz. - D : float64 - Zero field splitting parameter D in Megahertz. - D_tri : float64 - Zero field splitting parameter D of a triplet in Megahertz. - E : float64 - Zero field splitting parameter E in Megahertz. - E_tri : float64 - Zero field splitting parameter E of a triplet in Megahertz. - beta : float64 - Decay rate of the exchange coupling in Å^-1. - J_0 : float64 - Zero distance exchange coupling constant in Megahertz. - J_ex : float64 - Exchange coupling in Megahertz. - g1_frame : 1d-Array, np.float64 - Orientation of electron spin 1 in radian. - g2_frame : 1d-Array, np.float64 - Orientation of electron spin 2 in radian. - g_tri_frame : 1d-Array, np.float64 - Orientation of g_tri in radian. - g_frame : 1d-Array, np.float64 - Orientation of g in radian. - A1_frame : 1d-Array, np.float64 - Orientation of nuclei spin 1 in radian. - A2_frame : 1d-Array, np.float64 - Orientation of nuclei spin 2 in radian. - A3_frame : 1d-Array, np.float64 - Orientation of nuclei spin 3 in radian. - A4_frame : 1d-Array, np.float64 - Orientation of nuclei spin 4 in radian. - A5_frame : 1d-Array, np.float64 - Orientation of nuclei spin 5 in radian. - D_frame : 1d-Array, np.float64 - Orientation of dipol coupling in radian. - D_tri_frame : 1d-Array, np.float64 - Orientation of triplet dipol coupling in radian. - n1 : int - Number of chemically equivalent atoms. - I1 : float64 - Corresponding nuclear spin. - n2 : int - Number of chemically equivalent atoms. - I2 : float64 - Corresponding nuclear spin. - n3 : int - Number of chemically equivalent atoms. - I3 : float64 - Corresponding nuclear spin. - n4 : int - Number of chemically equivalent atoms. - I4 : float64 - Corresponding nuclear spin. - n5 : int - Number of chemically equivalent atoms. - I5 : float64 - Corresponding nuclear spin. - width_gauss : float64 - Gaussian linewidth in mT. - T_relax_1 : float64 - Longitudinal relaxation time in s. - T_relax_2 : float64 - Transversal relaxation time in s. - decay : float64 - Exponential decay time for hilbert space simulations in s. - T_pm : float64 - Phase memory time (OOP ESEEM). - population : 1d-Array, np.float64 - Populations of the initial density matrix of a triplet precursor in - zero field. - amplitude : float64 - Amplitude of the spectra for OOP-ESEEM. - - """ - - def __init__(self) -> None: - """ - Initialize object of class 'Spinsystem' for radical pair simulation. - - Returns - ------- - None. - - """ - # [SPINSYSTEM] - self.g1 = np.array([2.002, 2.002, 2.002]) - self.g2 = np.array([2.004, 2.004, 2.004]) - self.g_tri = np.array([2.002, 2.002, 2.002]) - self.g = np.array([2.002, 2.002, 2.002]) - - self.A1 = np.zeros(3) # MHz - self.A2 = np.zeros(3) # MHz - self.A3 = np.zeros(3) # MHz - self.A4 = np.zeros(3) # MHz - self.A5 = np.zeros(3) # MHz - self.A_eseem = 0.0 # MHz - self.omega_I = 0.0 # MHz - self.D = 1.0 # MHz - self.D_tri = 0.0 # MHz - self.E = 0 # MHz - self.E_tri = 0 # MHz - self.J_ex = 0 # MHz - self.J_0 = 0 # MHz - self.beta = 1.4 # Å^-1. - - # [ORIENTATIONS] - self.g1_frame = np.zeros(3) # Euler angle / rad - self.g2_frame = np.zeros(3) - self.g_tri_frame = np.zeros(3) - self.g_frame = np.zeros(3) - self.A1_frame = np.zeros(3) - self.A2_frame = np.zeros(3) - self.A3_frame = np.zeros(3) - self.A4_frame = np.zeros(3) - self.A5_frame = np.zeros(3) - self.D_frame = np.zeros(3) - self.D_tri_frame = np.zeros(3) - - # [LINEWIDTHS] - self.width_gauss = 0.5 # Gaussian linewidth / mT - - # [RELAXATION TIMES] - self.T_relax_1 = 0.0 # longitudinal - self.T_relax_2 = 0.0 # transversal - self.T_pm = 0.0 # phase memory time - self.decay = 0.0 - - # [DENSITY MATRIX] - self.population = np.array([1.0, 0.0, 0.0]) - - # [OTHER] - self.amplitude = 0.0
- - - -
-[docs] -class Spinsystem(EPR_Parameters): - """ - A class containing all parameters for various radical pair simulations. - - Attributes - ---------- - g1_iso : float64 - Isotropic g value of electron 1. - g2_iso : float64 - Isotropic g value of electron 2. - n1 : int - Number of chemically equivalent atoms. - I1 : float64 - Corresponding nuclear spin. - n2 : int - Number of chemically equivalent atoms. - I2 : float64 - Corresponding nuclear spin. - n3 : int - Number of chemically equivalent atoms. - I3 : float64 - Corresponding nuclear spin. - n4 : int - Number of chemically equivalent atoms. - I4 : float64 - Corresponding nuclear spin. - n5 : int - Number of chemically equivalent atoms. - I5 : float64 - Corresponding nuclear spin. - donor_list : np.array - Defines which atom groups are donor groups. - acceptor_list : np.array - Defines which atom groups are acceptor groups. - frame_group_i : list - Define a frame_group which will be used in optimization mode. Each - frame group contains the names of the angle lists which always will - have same values during optimization. i is a variable and can be - whatever you want. You can define as many frame groups as you want. An - example frame group would be: frame_group_1 = ['A1', 'A2', 'D']. This - list means, that A2_frame and D_frame will always have the same values - as A1_frame, no matter which values were given to them. - spin_system : str - Define the spin system by one out of: "rp" (radical pair), "doub" - (doublet), "trip" (triplet), "tdp" (triplet-doublet pair). - precursor : str - State of the precursor. One out of: "zf", "eigen", "singlet", - "triplet-zf", "triplet-eigen", "coupled", "basis". - dynamics : np.array - Matrix with rate constants of relaxation process in 1/s. For further - information see the documentation. - distribution_order : int - Number of Gaussians used for Multi-Gauss-Fitting. - distribution : np.array - Distance distribution of the radical pair. - - Methods - ------- - load(profile_name: str) - Load Spinsystem values from a config file (profile_name.ini). - - Recommended - save(profile_name: str) - Save the current spinsystem values as a config file (profile_name.ini). - - Recommended - _get_g_iso() - Determine both g_iso values. Needed in simulation. - - Examples - -------- - Initialize a new object of class <Spinsystem>: - - >>> Sys = Spinsystem() - >>> Sys.g1 - np.array([2.002, 2.002, 2.002]) - >>> Sys.g1_iso - 2.002 - - Change values: - - >>> Sys.g1 = np.array([2.0024, 2.00381, 2.0027]) - >>> Sys.get_g_iso() - >>> Sys.g1 - np.array([2.0024 , 2.00381, 2.0027 ]) - >>> Sys.g1_iso - 2.00297 - - Create a new spinsystem profile from an empty template and load it: - - >>> Sys_profile = profiles.new_spinsystem_profile() - >>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] # use list not array! - >>> profiles.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1') - >>> Sys_2 = Spinsystem() - >>> Sys_2.load_profile('Sys_prof_1') - >>> Sys_2.g1 - np.array([2.0034 , 2.00156, 2.00228]) - - You can also save your current spinsystem as a new profile: - - >>> Sys_3 = Spinsystem() - >>> Sys_3.g1 = np.array([1, 2, 3]) # either array or list - >>> Sys_3.save('Sys_prof_2') - >>> Sys_4 = Spinsystem() - >>> Sys_4.load('Sys_prof_2') - >>> Sys_4.g1 - np.array([1., 2., 3.]) - - """ - - def __init__(self) -> None: - """ - Initialize object of class 'Spinsystem' for radical pair simulation. - - Returns - ------- - None. - - """ - super().__init__() - - # [SPIN SYSTEM] - self._get_g_iso() - self.spin_system = "rp" - self.precursor = "singlet" - - # [NUCLEI] - self.acceptor_list = np.array([1, 2, 3]) - self.donor_list = np.array([4, 5]) - - self.n1 = 0 # Number of chemically equivalent atoms - self.I1 = 0 # corresponding nuclear spin - self.n2 = 0 - self.I2 = 0 - self.n3 = 0 - self.I3 = 0 - self.n4 = 0 - self.I4 = 0 - self.n5 = 0 - self.I5 = 0 - - # [DYNAMICS] - self.dynamics = None - - # [DISTRIBUTIONS] - self.distribution_order = 3 - self.distribution = None - -
-[docs] - def _get_g_iso(self) -> None: - """Get g1_iso and g2_iso.""" - self.g1_iso = self.g1.sum() / 3 - self.g2_iso = self.g2.sum() / 3
- - -
-[docs] - def load(self, profile_name: str, degree: bool = False) -> None: - """ - Load a spinsystem from a profile. - - Load the settings from [sys.prefix]/easypairspin/profiles/spinsystem/ - [profile_name].ini into the Spinsystem object. Overwrites previous - settings. - - Parameters - ---------- - profile_name : str - Name of the Spinsystem profile which will be loaded. - degree : bool, optional - If True, the angle values in the profile are given in degree not - radian. Thus, they will be converted to radian. If False, the - angles are given in radian and will therefore not be converted. The - default is 'False'. - - Returns - ------- - None. - - """ - spinsystem_profile = profiles.load_profile(profile_name, "spinsystem") - - for key in vars(self): - if key not in ("g1_iso", "g2_iso"): - try: - vars(self)[key] = spinsystem_profile["main"][key] - except KeyError: - pass - - try: - if len(vars(self)[key]) == 3: - vars(self)[key] = np.array(vars(self)[key]) - except TypeError: - pass - - if key in ("acceptor_list", "donor_list"): - vars(self)[key] = np.array(vars(self)[key]) - - if key == "distribution": - if vars(self)[key] in ("None", None): - vars(self)[key] = None - else: - vars(self)[key] = np.array(vars(self)[key]) - size = vars(self)[key].size - vars(self)[key] = vars(self)[key].reshape((2, size // 2)) - - if degree: - if key.endswith("_frame"): - vars(self)[key] *= np.pi / 180 - - self._get_g_iso()
- - -
-[docs] - def save(self, profile_name: str = "") -> None: - """ - Save the spinsystem as a profile. - - Save the spinsystem object as a spinsystem profile using - profile_management.add_profile(). Load the spinsystem using - Spinsystem.load_profile(<profile_name>). - - Parameters - ---------- - profile_name : str, optional - Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. - - """ - spinsys_profile = profiles.new_spinsystem_profile() - - for key in spinsys_profile["main"]: - if isinstance(vars(self)[key], np.ndarray): - spinsys_profile["main"][key] = list(vars(self)[key].flatten()) - else: - spinsys_profile["main"][key] = vars(self)[key] - - profiles.add_profile(spinsys_profile, "spinsystem", pname=profile_name)
-
- - - -
-[docs] -class Experimental: - """ - A class containing all experimental parameters and data. - - Attributes - ---------- - B_z : 1d-Array, np.float64 - External magnetic field points in mT used for simulation. Conversions - allowed. - freq_mw : float64 - Frequency of induced microwave radiation in Gigahertz. - magnetic_field : 1d-Array, np.float64 - Same as B_z, but will never be changed. - int : np.array, np.complex128 - Real and imaginary part of the measured intensities. 1d or 2d. - time_axis : 1d-Array, optional - Contains all experimental time points. - spec_sim : 1d-Array, np.float64 - Calculated spectrum. At initialisation empty. - - Methods - ------- - get_linear_time_axis() - Get a linear time axis using the given boundaries from self.t_scale - with self.t_points points. - - """ - - def __init__( - self, - magnetic_field: np.array = None, - real_int: np.array = None, - imag_int: np.array = None, - cmplx_int: np.array = None, - time_axis: np.array = None, - rescale: bool = True, - ): - """ - Initialize object of class 'Experimental' for radical pair simulation. - - Parameters - ---------- - magnetic_field : 1d-Array, np.float64, optional - Contains all experimental external magnetic field points. - real_int : np.array, np.float64, optional - Real part of the measured intensities. 1d or 2d. - imag_int : np.array, np.float64, optional - Imaginary part of the measured intensities. 1d or 2d. - cmplx_int : np.array, np.complex128, optional - Real and imaginary part of the measured intensities. 1d or 2d. If - this parameter is given, real_int and imag_int will be ignored. - time_axis : 1d-Array, optional - Contains all experimental time points in s. - rescale : bool, optional - If True, the experimental intensities will be scaled to a maximum - of 1. Default is 'True'. - - Returns - ------- - None. - - """ - self.magnetic_field = magnetic_field - if self.magnetic_field is None: - self.B_z = np.linspace(240, 260, 100) - else: - self.B_z = 1 * magnetic_field # external magnetic field / mT - self.freq_mw = 9.7e9 # Microwave radiation / Hz - self.B_mw = 1e-3 - - self.t_scale = [0, 2e-6] - self.t_points = 2 - self.time_axis = time_axis - if time_axis is not None: - self.t_scale[0] = time_axis.min() - self.t_scale[1] = time_axis.max() - self.t_points = time_axis.shape[0] - - self.int = None - if cmplx_int is not None: - self.int = cmplx_int - else: - if real_int is not None: - self.int = np.zeros(real_int.shape, dtype="complex128") - self.int.real = real_int - if imag_int is not None: - if self.int is not None: - self.int.imag = imag_int - else: - self.int = np.zeros(imag_int.shape, dtype="complex128") - self.int.imag = real_int - - if self.int is not None and rescale: - self.int /= np.abs(self.int).max() - self.spec_sim = np.zeros( - self.int.shape, dtype="complex128" - ) # simulated spectra - -
-[docs] - def get_linear_time_axis( - self, t_min: float = None, t_max: float = None, t_points: int = None - ) -> None: - """ - Get a linear timea axis for transient simulations. - - Get a linear time axis using the given boundaries from self.t_scale - with self.t_points points. - - Parameters - ---------- - t_min : float, optional - Left boundary of the time axis. If None is given, the current value - of self.t_scale[0] will be used. Else, the value of self.t_scale[0] - will be replaced. The default is None. - t_max : float, optional - Right boundary of the time axis. If None is given, the current - value of self.t_scale[1] will be used. Else, the value of - self.t_scale[1] will be replaced. The default is None. - t_points : int, optional - Number of time points. If None is given, the current value of - self.t_points will be used. Else, the value of self.t_points will - be replaced. The default is None. - - Returns - ------- - None - Nothing will be returned. - - """ - if t_min is not None: - self.t_scale[0] = t_min - if t_max is not None: - self.t_scale[1] = t_max - if t_points is not None: - self.t_points = t_points - - self.time_axis = np.linspace(self.t_scale[0], self.t_scale[1], self.t_points)
-
- - - -
-[docs] -class Variation(EPR_Parameters): - """ - A class containing variation ranges for all possible parameters. - - Attributes - ---------- - needed_digits : int - Number of needed digits for chromosomes. - number_of_genes : int - Number of varied parameters. Used for fp representation. - variation_array : 1d-Array, np.float64 - Array with all variation ranges greater 0. - boundaries : list - Sequence of tuples containing upper and lower bounds for all varied - parameters. Used for scipy.optimize. - freq_mw : float64 - Frequency of induced microwave radiation in Hertz. - bohr_magneton : float64 - Bohr magneton in Hertz/Tesla. - - Methods - ------- - load(profile_name: str) - Load Spinsystem values from a config file (profile_name.ini). - - Recommended - save(profile_name: str) - Save the current variation values as a config file (profile_name.ini). - - Recommended - get_digits_for_one_par(Par, digits_per_True, one_par=True) - Get the number of needed digits for one varied parameter. - get_needed_digits() - Get the total number of needed digits in binary mode. - get_number_of_genes() - Get the total number of genes. Is equal to the number of varied - parameters. - get_variation_array() - Create an array with all used variation ranges. Only used in floating - point representation. - update_digits() - Determine number of needed digits. Just calls get_needed_digits() - get_boundaries(Sys) - Get a sequence of tuples containing the boundaries for the varied - parameters. - - Examples - -------- - Initialize a new object of class <Variation>: - - >>> Var = Variation() - >>> Var.g1 - np.array([0., 0., 0.]) - - Change values: - - >>> Var.g1 = np.array([0.003, 0.004, 0.003]) - >>> Var.g1 - np.array([0.003, 0.004, 0.003]) - - Create a new variation profile from an empty template and load it: - - >>> Var_profile = profiles.new_variation_profile() - >>> Var_profile['g_1'] = [0.001, 0.007, 0.003] # use list not array! - >>> profiles.add_profile(Var.profile, 'variation', 'Var_prof_1') - >>> Var_2 = Variation() - >>> Var_2.load_profile('Var_prof_1') - >>> Var_2.g1 - np.array([0.001, 0.007, 0.003]) - - You can also save your current variation object as a new profile: - - >>> Var_3 = Variation() - >>> Var_3.g1 = np.array([1, 2, 3]) # either array or list - >>> Var_3.save('Var_prof_2') - >>> Var_4 = Variation() - >>> Var_4.load('Var_prof_2') - >>> Var_4.g1 - np.array([1., 2., 3.]) - - """ - - def __init__(self): - """ - Initialize object of class 'Variation' for radical pairs in EPR. - - Returns - ------- - None. - - """ - super().__init__() - for key in vars(self): - if isinstance(vars(self)[key], float): - vars(self)[key] = 0.0 - elif isinstance(vars(self)[key], int): - vars(self)[key] = 0 - else: - for i in range(len(vars(self)[key])): - vars(self)[key][i] = 0.0 - - self.bohr_magneton = constant.value("Bohr magneton in Hz/T") - - self.fit_distribution = False - - # [EXPERIMENTAL SETUP] - self.freq_mw = 0.0 # Microwave radiation / Hz - - # [Variable Lists] - self.non_vars = ( - "bohr_magneton", - "needed_digits", - "number_of_genes", - "variation_array", - "boundaries", - "isotropic", - "fit_distribution", - "non_vars", - "single_vars", - ) - self.single_vars = ( - "A_eseem", - "omega_I", - "D", - "D_tri", - "E", - "E_tri", - "beta", - "J_0", - "J_ex", - "T_relax_1", - "T_relax_2", - "T_pm", - "decay", - "freq_mw", - "amplitude", - "width_gauss", - ) - -
-[docs] - def load(self, profile_name: str, degree: bool = False) -> None: - """ - Laod a variation object from a profile. - - Load the settings from [sys.prefix]/easypairspin/profiles/variation/ - [profile_name].ini into the Variation object. Overwrites previous - settings. - - Parameters - ---------- - profile_name : str - Name of the Variation profile which will be loaded. - degree : bool, optional - If True, the angle values in the profile are given in degree not - radian. Thus, they will be converted to radian. If False, the - angles are given in radian and will therefore not be converted. The - default is 'False'. - - Returns - ------- - None. - - """ - variation_profile = profiles.load_profile(profile_name, "variation") - - for key in vars(self): - if key not in ("g1_iso", "g2_iso", "bohr_magneton"): - try: - vars(self)[key] = variation_profile["main"][key] - except KeyError: - pass - - try: - if len(vars(self)[key]) == 3: - vars(self)[key] = np.array(vars(self)[key]) - except TypeError: - pass - - if degree: - if key.endswith("_frame"): - vars(self)[key] *= np.pi / 180
- - -
-[docs] - def save(self, profile_name: str = "") -> None: - """ - Save a variation object as a profile. - - Save the variation object as a variation profile using - profile_management.add_profile(). Load the variation object using - Variation.load_profile(<profile_name>). - - Parameters - ---------- - profile_name : str, optional - Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. - - """ - var_profile = profiles.new_variation_profile() - - for key in var_profile["main"]: - if isinstance(vars(self)[key], np.ndarray): - var_profile["main"][key] = list(vars(self)[key]) - else: - var_profile["main"][key] = vars(self)[key] - - profiles.add_profile(var_profile, "variation", pname=profile_name)
- - -
-[docs] - def get_needed_digits(self) -> None: - """ - Get sum of needed digits for all varied parameters. - - Returns - ------- - None. - - """ - self.get_number_of_genes() - self.needed_digits = self.number_of_genes * 12 - - return None
- - -
-[docs] - def get_number_of_genes(self) -> None: - """Determine number of parameters which get varied.""" - self.number_of_genes = 0 - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0.0: - self.number_of_genes += 1 - else: - for parameter in vars(self)[key]: - if parameter > 0.0: - self.number_of_genes += 1 - - return None
- - -
-[docs] - def get_variation_array(self) -> None: - """Put all variation ranges in one 1d-Array.""" - self.get_number_of_genes() - self.variation_array = np.zeros(self.number_of_genes) - i = 0 - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0: - self.variation_array[i] = vars(self)[key] - i += 1 - else: - for parameter in vars(self)[key]: - if parameter > 0: - self.variation_array[i] = parameter - i += 1 - - return None
- - -
-[docs] - def update_digits(self) -> None: - """Update number of needed_digits.""" - self.get_needed_digits() - - return None
- - -
-[docs] - def get_boundaries(self, Sys: object) -> None: - """ - Create a sequence of pairs with all bounds for the varied parameters. - - Used for the scipy optimization routines. - - Parameters - ---------- - Sys : object - Spinsystem object. - - Returns - ------- - None - Nothing will be returned. - - """ - self.boundaries = [] - - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0: - mid = vars(Sys)[key] - var = vars(self)[key] - lb = mid - var - ub = mid + var - - if lb > ub: - lb = mid + var - ub = mid - var - - bounds = (lb, ub) - self.boundaries.append(bounds) - else: - for i, parameter in enumerate(vars(self)[key]): - if parameter > 0: - mid = vars(Sys)[key][i] - var = parameter - lb = mid - var - ub = mid + var - - bounds = (lb, ub) - self.boundaries.append(bounds) - - if self.fit_distribution: - for i in range(Sys.distribution_order): - b_int = (0, 1) - b_pos = (Sys.distribution[0].min(), Sys.distribution[0].max()) - b_sigma = (0.01, 0.7) - self.boundaries.append(b_int) - self.boundaries.append(b_sigma) - self.boundaries.append(b_pos) - - return None
-
- - - -
-[docs] -class SimulationOptions: - """ - A class containing all simulation options. - - Attributes - ---------- - routine: str - Name of the simulation routine which will be used by easypairspin() and - easypairspin_optimize(). - grid_points: int - Number of points used for spherical grid. - space: str - Name of the mathematical space used for some calculations. - pop_evolution : boolean - If set to True, the population evolution in calculated using teacups. - eigval_mode : boolean - If set to True, only the eigenvalues of the system are calculated using - teacups. - force_cpu : boolean - If True, the simulation will be executed on the CPU, even if GPU is - available. Default is False. - regularization_mode : int - Choose the regularization matrix used for the Tikhonov-Regularization. - 0 : Unitary matrix - 1 : First order derivative matrix - 2 : Second order derivative matrix (default) - - Methods - ------- - load(profile_name: str) - Load SimulationOptions values from a config file (profile_name.ini). - - Recommended - save(profile_name: str) - Save the current simulation options as a config file - (profile_name.ini). - - Examples - -------- - Initialize an object of class <SimulationOptions>: - - >>> SimOpt = SimulationOptions() - >>> SimOpt.grid_points - 500 - - Change values: - - >>> SimOpt.grid_points = 1000 - >>> SimOpt.grid_points - 1000 - - Save your current values as a new profile: - - >>> SimOpt.save('SimOpt_prof_1') - >>> SimOpt_2 = SimulationOptions() - >>> SimOpt_2.grid_points - 500 - >>> SimOpt.load('SimOpt_prof_1') - >>> SimOpt_2.grid_points - 1000 - - You can also create a simulation options profile from an empty template: - - >>> simopt_prof = profiles.new_simulation_profile() - >>> simopt_prof['static_radpair']['grid_points'] = 1100 - >>> profiles.add_profile(simopt_prof, 'simulation', 'SimOpt_prof_2') - >>> SimOpt_3 = SimulationOptions() - >>> SimOpt_3.grid_points - 500 - >>> SimOpt.load('SimOpt_prof_2') - >>> SimOpt_3.grid_points - 1100 - - - """ - - def __init__(self): - """ - Initialize object of class 'Simulation_Options' used for simulations. - - Returns - ------- - None. - - """ - # [MAIN] - self.routine = "" - self.cpu_cores = 0 - - # [STATIC_RADICAL_PAIR] - self.grid_points = 500 - self.refinement = 1 - - # [TEACUPS] - self.space = "hilbert" - self.pop_evolution = True - self.eigval_mode = False - - # [OPOSSUM] - - # [DIDELPHIS] - self.min_r = 10 - self.max_r = 50 - self.r_points = 401 - self.fast_mode = False - self.GCV = False - self.force_cpu = False - self.regularization_mode = 2 - - # [DIDELPHIS_MGF] - -
-[docs] - def load(self, profile_name: str) -> None: - """ - Load simulation options from a profile. - - Load the settings from [sys.prefix]/easypairspin/profiles/simulation/ - [profile_name].ini into the SimulationOptions object. Overwrites - previous settings. - - Parameters - ---------- - profile_name : str - Name of the simulation profile which will be loaded. - - Returns - ------- - None. - - """ - simulation_profile = profiles.load_profile(profile_name, "simulation") - - self.routine = simulation_profile["main"]["routine"] - self.cpu_cores = simulation_profile["main"]["cpu_cores"] - for key in simulation_profile[self.routine].keys(): - vars(self)[key] = simulation_profile[self.routine][key]
- - -
-[docs] - def save(self, profile_name: str = "") -> None: - """ - Save the simulation optionas as a profile. - - Save the simulation options as a simulation options profile using - profile_management.add_profile(). Load the simulation options using - SimulationOptions.load_profile(<profile_name>). - - Parameters - ---------- - profile_name : str, optional - Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. - - """ - simopt_profile = profiles.new_simulation_profile() - - for section in ["main", "static_radpair", "teacups", "opossum"]: - for key in simopt_profile[section]: - if isinstance(vars(self)[key], np.ndarray): - simopt_profile[section][key] = list(vars(self)[key]) - else: - simopt_profile[section][key] = vars(self)[key] - - profiles.add_profile(simopt_profile, "simulation", pname=profile_name)
-
- - - -
-[docs] -class FittingOptions: - """ - A class containing all optimization options. - - Attributes - ---------- - routine : str - Name of the optimization routine which will be used by - easypairspin_optimize(). - method : str - Name of the optimization method used in the scipy.optimize routines. - x0 : numpy.array, np.float64 - Array containing the initial guess for the optimization routine for - the parameters which will be varied. - cpu_cores : int - Number of cores used for the optimization. - gui : bool - Set to True if in GUI mode (PySpin). The default is False. - window : object - Plot canvas. Only needed in GUI mode. - - Methods - ------- - load(profile_name: str) - Load FittingOptions values from a config file (profile_name.ini). - - Recommended - save(profile_name: str) - Save the current fitting options as a config file (profile_name.ini). - - Recommended - - Examples - -------- - Initialize an object of class <FittingOptions>: - - >>> FitOpt = FittingOptions() - >>> FitOpt.GAVaPS - True - - Change values: - - >>> FitOpt.GAVaPS = False - >>> FitOpt.GAVaPS - False - - Save your current values as a new profile: - - >>> FitOpt.save_simulationoptions('FitOpt_prof_1') - >>> FitOpt_2 = FittingOptions() - >>> FitOpt_2.GAVaPS - True - >>> FitOpt.load_profile('FitOpt_prof_1') - >>> FitOpt_2.GAVaPS - False - - You can also create a fitting options profile from an empty template: - - >>> fitopt_prof = profiles.new_optimization_profile() - >>> fitopt_prof['genetic']['GAVaPS'] = False - >>> profiles.add_profile(fitopt_prof, 'simulation', 'FitOpt_prof_2') - >>> FitOpt_3 = FittingOptions() - >>> FitOpt_3.GAVaPS - True - >>> FitOpt.load_profile('FitOpt_prof_2') - >>> FitOpt_3.GAVaPS - False - - """ - - def __init__(self): - """ - Initialize object of class 'FittingOptions' used for optimization. - - Returns - ------- - None. - - """ - # [MAIN] - self.routine = None - self.method = None - self.x0 = None - self.cpu_cores = 0 - self.gui = False - self.window = None - - # [GENETIC] - self.GAVaPS = True - self.representation = None - self.lifetime_mode = None - self.crossover_type = None - self.mutation_type = None - self.min_lifetime = None - self.max_lifetime = None - self.reproduction_ratio = None - self.p_c = None - self.p_m = None - self.pop_size = None - self.min_pop_size = None - self.max_pop_size = None - self.convergence = None - self.peak_prominence = None - self.error_weight = None - self.max_generation = None - self.show_status = None - - # [MINIMIZE] - self.maxiter = None - - # [DUAL_ANNEALING] - self.maxiter = None - self.maxiter_minimizer = None - self.initial_temp = None - self.restart_temp_ratio = None - self.visit = None - self.accept = None - self.maxfun = None - self.no_local_search = None - - # [SHGO] - self.n = None - self.iters = None - self.maxiter_minimizer = None - self.maxfev = None - self.f_tol = None - self.maxiter = None - self.maxev = None - self.maxtime = None - self.minimize_every_iter = None - self.local_iter = None - self.sampling_method = None - - # [DIFFERENTIAL_EVOLUTION] - self.strategy = None - self.maxiter = None - self.popsize = None - self.tol = None - self.mutation = None - self.recombination = None - self.seed = None - self.disp = None - self.polish = None - self.init = None - self.atol = None - self.updating = None - - # [BASINHOPPING] - self.T = None - self.niter = None - self.stepsize = None - self.maxiter_minimizer = None - self.interval = None - self.disp = None - self.niter_success = None - self.seed = None - self.target_accept_rate = None - self.stepwise_factor = None - - # [LEAST_SQUARES] - self.ftol = None - self.xtol = None - self.gtol = None - self.loss = None - self.f_scale = None - self.max_nfev = None - self.tr_solver = None - self.verbose = None - -
-[docs] - def load(self, profile_name: str) -> None: - """ - Load fitting options from a profile. - - Load the settings from [sys.prefix]/easypairspin/profiles/optimization/ - [profile_name].ini into the FittingOptions object. Overwrites previous - settings. Only loads the section given in ['main']['routine']. - - Parameters - ---------- - profile_name : str - Name of the optimization profile which will be loaded. - - Returns - ------- - None. - - """ - fitting_profile = profiles.load_profile(profile_name, "optimization") - - self.routine = fitting_profile["main"]["routine"] - self.cpu_cores = fitting_profile["main"]["cpu_cores"] - for key in fitting_profile[self.routine].keys(): - vars(self)[key] = fitting_profile[self.routine][key]
- - -
-[docs] - def save(self, profile_name: str = "") -> None: - """ - Save the fitting options as a profile. - - Save the fitting options as a fitting options profile using - profile_management.add_profile(). Load the fitting options using - FittingOptions.load_profile(<profile_name>). - - Parameters - ---------- - profile_name : str, optional - Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. - - """ - fitopt_profile = profiles.new_optimization_profile() - - for section in [ - "main", - "genetic", - "minimize", - "dual_annealing", - "shgo", - "differential_evolution", - "basinhopping", - "least_squares", - ]: - for key in fitopt_profile[section]: - if isinstance(vars(self)[key], np.ndarray): - fitopt_profile[section][key] = list(vars(self)[key]) - else: - fitopt_profile[section][key] = vars(self)[key] - - profiles.add_profile(fitopt_profile, "optimization", pname=profile_name)
-
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/loading.html b/docs/build/html/_modules/spinanalysis/loading.html deleted file mode 100644 index e5ba830..0000000 --- a/docs/build/html/_modules/spinanalysis/loading.html +++ /dev/null @@ -1,921 +0,0 @@ - - - - - - - - spinanalysis.loading — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.loading

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-import os
-import glob
-from typing import Tuple
-import numpy as np
-from scipy.io import loadmat
-from spinanalysis._utils import strtobool
-
-
-
-[docs] -def get_full_path(directory_name: str, start_directory: str = None) -> str: - """ - Get the full path of a given directory. Search starts at home directory. - - Parameters - ---------- - directory_name : str - Name of the directory whose path is to be found. - start_directory : str, optional - Directory at which the search starts. If given, the perfomance - increases very sharply. The default is None. - - Recommended. - - Returns - ------- - full_path : str - The full path of the directory. - - """ - if start_directory is None: - start_directory = os.path.join(os.path.expanduser("~"), "**") - else: - start_directory = os.path.join(os.path.expanduser("~"), start_directory, "**") - - all_subdirectories = glob.glob(start_directory, recursive=True) - - full_path = None - for path in all_subdirectories: - if path.endswith(directory_name): - full_path = path - - return full_path
- - - -
-[docs] -def get_DSC_parameters(path_to_folder: str) -> dict: - """ - Extract all parameters from the DSC File. - - Parameters - ---------- - path_to_folder : str - Full path to the folder with .DSC and .DTA file. Files and path must - have the same basename (BRUKER STANDARD). - - Returns - ------- - DSC_parameters : dict - Dictionary with all parameters. Key is the same as in .DSC. - - """ - DSC_parameters = {} - - basename = os.path.basename(path_to_folder) # Foldername == Filename - DSC_file = os.path.join(path_to_folder, basename + ".DSC") - - with open(DSC_file, "r") as file: - for line in file.readlines(): - if line.startswith(("*", "#", "'")): - pass - elif line.startswith("FTAcqModeSlct"): - line = line.split() - DSC_parameters[line[0]] = line[-1] - else: - line = line.split() - try: - DSC_parameters[line[0]] = convert_parameter_type(line[1]) - except IndexError: - pass - - DSC_parameters["path_to_folder"] = path_to_folder - - return DSC_parameters
- - - -
-[docs] -def convert_parameter_type(value: str) -> Tuple[bool, int, float, str]: - """ - Convert the type of a given string to bool, int or float if possible. - - Parameters - ---------- - value : str - Parameter string which should be converted. - - Returns - ------- - value : bool or int or float or str - Parameter as bool, int or float. If none is possible, the stripped - string will be returned. - - """ - try: - value = bool(strtobool(value)) - return value - except ValueError: - pass - - try: - value = int(value) - return value - except ValueError: - pass - - try: - value = float(value) - return value - except ValueError: - value = value.strip("'") - return value
- - - -
-[docs] -def get_byte_mode(DSC_dict: dict, data_key: str = "IRFMT") -> str: - """ - Get the used byte mode of the BRUKER BES3T-data. - - For more information about BES3T go to BRUKER website or easyspin @ github. - - Parameters - ---------- - DSC_dict : dict - Dictionary with all parameters. Key is the same as in .DSC. - data_key : str, optional - Key for the data array. Either 'IRFMT' or 'IIFMT'. - The default is 'IRFMT'. - - Raises - ------ - ValueError - Raised if key value is not C, S, I, F or D. - - Returns - ------- - byte_mode : str - Return the used byte mode as one string for - numpy.fromfile(dtype=byte_mode). - - """ - byte_mode = "" - - if DSC_dict["BSEQ"] == "BIG": - byte_mode += ">" - elif DSC_dict["BSEQ"] == "LIT": - byte_mode += "<" - else: - byte_mode += ">" - - if DSC_dict[data_key] in ("C", "S", "I"): - byte_mode += "i" - if DSC_dict[data_key] == "I": - byte_mode += "4" - elif DSC_dict[data_key] == "S": - byte_mode += "2" - elif DSC_dict[data_key] in ("F", "D"): - byte_mode += "f" - if DSC_dict[data_key] == "D": - byte_mode += "8" - elif DSC_dict[data_key] == "A": - raise ValueError("Can't read BES3T data in ASCII format!") - else: - raise ValueError("Unknown value for keyword {} in .DSC file!".format(data_key)) - - return byte_mode
- - - -
-[docs] -def load_data_vector(DSC_dict: dict) -> np.array: - """ - Load the binary intensity vector(s) from BRUKER BES3T-File. - - Parameters - ---------- - DSC_dict : dict - Dictionary with all parameters. Key is the same as in .DSC. - - Raises - ------ - ValueError - Will be raised if IKKF isn't CPLX or REAL or if the dimension isn't 1 - or 2. - KeyError - Raised if an axis is given but not the corresponding number of points. - - Returns - ------- - cmplx_data_vector : np.array, np.complex128 - Real and imaginary part of the measured intensities. If no imaginary - part is measured, zeroes will be inserted. - - """ - get_data_dimension(DSC_dict) - byte_mode = get_byte_mode(DSC_dict) - - DTA_file = os.path.join(DSC_dict["path_to_folder"], DSC_dict["TITL"] + ".DTA") - - data_vector = np.fromfile(DTA_file, dtype=byte_mode) - - if DSC_dict["IKKF"] == "CPLX": - data_vector_real = data_vector[0::2] - data_vector_imag = data_vector[1::2] - elif DSC_dict["IKKF"] == "REAL": - data_vector_real = 1 * data_vector - data_vector_imag = np.zeros(len(data_vector)) - else: - raise ValueError("Unknown value for keyword IKKF!") - - if DSC_dict["dimensions"] == 2: - if "XPTS" and "YPTS" in DSC_dict: - data_vector_real = data_vector_real.reshape( - (DSC_dict["YPTS"], DSC_dict["XPTS"]) - ) - data_vector_imag = data_vector_imag.reshape( - (DSC_dict["YPTS"], DSC_dict["XPTS"]) - ) - - elif "XPTS" and "ZPTS" in DSC_dict: - data_vector_real = data_vector_real.reshape( - (DSC_dict["ZPTS"], DSC_dict["XPTS"]) - ) - data_vector_imag = data_vector_imag.reshape( - (DSC_dict["ZPTS"], DSC_dict["XPTS"]) - ) - else: - raise KeyError("Can't find needed keys from (XPTS, YPTS, ZPTS)!") - - elif DSC_dict["dimensions"] == 1: - pass - else: - raise ValueError("Can't handle dimensions {0}".format(DSC_dict["dimensions"])) - - cmplx_data_vector = data_vector_real + 1j * data_vector_imag - - return cmplx_data_vector.T
- - - -
-[docs] -def load_axis_vector(axis: str, DSC_dict: dict) -> np.array: - """ - Load the points of a given axis (x, y, z). - - Parameters - ---------- - axis : str - Name of the axis. Needs to start with x,X,y,Y or z,Z. E. g.: x-axis. - DSC_dict : dict - Dictionary with all parameters. Key is the same as in .DSC. - - Raises - ------ - ValueError - Will be raised if the given axis doesn't starts with an allowed - character. - - Returns - ------- - axis_vector : np.array - Array with all axis points. - - """ - axis = axis.upper() - if axis.startswith("X"): - axis = "X" - elif axis.startswith("Y"): - axis = "Y" - elif axis.startswith("Z"): - axis = "Z" - else: - raise ValueError("Axis needs to start with 'x', 'y' or 'z'!") - - axis_file = os.path.join( - DSC_dict["path_to_folder"], DSC_dict["TITL"] + "." + axis + "GF" - ) - data_key = axis + "FMT" - - try: - byte_mode = get_byte_mode(DSC_dict, data_key=data_key) - axis_vector = np.fromfile(axis_file, dtype=byte_mode) - except (KeyError, FileNotFoundError): - try: - minimum = DSC_dict[axis + "MIN"] - except KeyError: - axis_vector = np.array([]) - else: - width = DSC_dict[axis + "WID"] - points = DSC_dict[axis + "PTS"] - axis_vector = np.linspace(minimum, minimum + width, points) - - return axis_vector
- - - -
-[docs] -def get_data_dimension(DSC_dict: dict) -> None: - """ - Get the dimension of the measured spectrum (1d/2d). - - Parameters - ---------- - DSC_dict : dict - Dictionary with all parameters. Key is the same as in .DSC. - - Returns - ------- - None - The dimension will be safed in the given dictionary to the key - 'dimensions'. - - """ - dim = 0 - for axis in ("XTYP", "YTYP", "ZTYP"): - if DSC_dict[axis] != "NODATA": - dim += 1 - - DSC_dict["dimensions"] = dim - - return None
- - - -
-[docs] -def load_epr_bruker_bes3t( - folder: str, start_directory: str = None -) -> Tuple[tuple, np.array]: - """ - Load the whole dataset from BRUKER BES3T data folder into numpy.arrays. - - Time axis will be rescaled for OOP-ESEEM experiments, if 'FTAcqModeSlct' - is 'Run from Tables'. - - Parameters - ---------- - folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional - Give the path starting from your home folder which the search for the - data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='nice'. So - the function call would be: - load_epr_bruker_bes3t(this_folder, 'nice') - - Recommended: - Without this parameter, the search for the right folder will be - much longer. - - Returns - ------- - axis : tuple - Tuple of all axis vectors as three numpy.arrays (x, y, z). - data : np.array, np.complex128 - All intensity values as one complex numpy.array. - - """ - path = get_full_path(folder, start_directory) - DSC_parameters = get_DSC_parameters(path) - - data = load_data_vector(DSC_parameters) - x = load_axis_vector("x", DSC_parameters) - try: - if DSC_parameters["FTAcqModeSlct"] == "Tables": - x /= 2 - except KeyError: - pass - y = load_axis_vector("y", DSC_parameters) - z = load_axis_vector("z", DSC_parameters) - axis = (x, y, z) - - return axis, data
- - - -
-[docs] -def load_epr_ESP_transient( - folder: str, start_directory: str = None -) -> Tuple[tuple, np.array]: - """ - Load data from a transient epr experiment measured with ESP380E. - - Parameters - ---------- - folder : str - Name of the folder with all data files. Data files need to have the - same basename as folder, e. g. '/home/user/exp_1/exp_1.001'. - start_directory : str, optional - Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='nice'. So - the function call would be: - load_epr_ESP_transient(this_folder, 'nice') - - Recommended: - Without this parameter, the search for the right folder will be - much longer. - - Returns - ------- - axis : tuple - Return a tuple with all axis vectors as two numpy.arrays - (magnetic_field, time). - data : np.array, np.complex128 - All intensity values as one complex numpy.array. Imaginary part is - always 0. - - - """ - path = get_full_path(folder, start_directory) - # tr_info = get_transient_info(path) - # time_axis = np.linspace(0, tr_info[0], tr_info[1]) - # points = int((abs(tr_info[3] - tr_info[2])) / tr_info[4] + 1) - # magnetic_field = np.linspace(tr_info[2], tr_info[3], points) - # axis = (magnetic_field, time_axis) - - # dimensions = (magnetic_field.shape[0], time_axis.shape[0]) - axis, data = get_transient_data(path) - - return axis, data
- - - -
-[docs] -def get_transient_info(fpath: str) -> tuple: - """ - Get all informations about the time axis and magnetic field vector from the - .info file. - - Parameters - ---------- - fpath : str - Full path to the folder with the experimental data. - - Raises - ------ - KeyError - Raised if section 'MAGNETIC FIELD' or 'TRANSIENT' could not be found in - .info file. Take care that the sections must be all upper case. - - Returns - ------- - transient_info : tuple - Contains the needed informations in the following order (time_length, - time_points, mag_field_start, mag_field_stop, mag_field_step). - - """ - fbasename = os.path.basename(fpath) - fname = os.path.join(fpath, fbasename + ".info") - transient_info = [None] * 5 - - with open(fname, "r") as info_file: - current_section = "" - for line in info_file.readlines(): - line = line.strip().split() - if line == []: - pass - else: - if line[0].isupper(): - current_section = line[0] - else: - if current_section == "MAGNETIC": - if line[0] == "Start:": - transient_info[2] = float(line[1]) - elif line[0] == "Stop:": - transient_info[3] = float(line[1]) - elif line[0] == "Step:": - transient_info[4] = float(line[1]) - else: - pass - elif current_section == "TRANSIENT": - if line[0] == "Points:": - transient_info[1] = int(line[1]) - elif line[0] == "Length:": - transient_info[0] = float(line[1]) - else: - pass - else: - pass - - if None not in transient_info: - break - - if transient_info[0] is None: - raise KeyError("Couldn't find section 'TRANSIENT' in .info file!") - if transient_info[2] is None: - raise KeyError("Couldn't find section 'MAGNETIC FIELD' in .info file!") - - return tuple(transient_info)
- - - -
-[docs] -def get_transient_data(fpath: str) -> Tuple[tuple, np.array]: - """ - Get the measured intensities of the whole spectrum from a transient epr - experiment measured by ESP380E. - - Parameters - ---------- - fpath : str - Full path to the folder with the experimental data. - - Returns - ------- - axis : tuple - Return a tuple with all axis vectors as two numpy.arrays - (magnetic_field, time). - cmplx_data_vector : np.array, np.complex128 - Measured real intensities. All imaginary parts are zero. - - """ - data_vector_real = [] - magnetic_field = [] - - file_found = False - max_digits = 1 - while not file_found: - fname = os.path.join( - fpath, - os.path.basename(fpath) + ".{0:0>{1}s}".format("1", str(max_digits)), - ) - if os.path.isfile(fname): - file_found = True - else: - max_digits += 1 - - if max_digits >= 10: - raise FileNotFoundError("No first ESP file could be found!") - - file_exists = True - file = 1 - while file_exists: - try: - if file == 1: - (field_point, time_axis), data = read_single_transient_file( - fpath, file, max_digits, time=True - ) - else: - field_point, data = read_single_transient_file(fpath, file, max_digits) - file += 1 - except OSError: - file_exists = False - else: - data_vector_real.append(data) - magnetic_field.append(round(field_point, 1)) - - magnetic_field = np.array(magnetic_field) - sorting = np.argsort(magnetic_field) - magnetic_field = magnetic_field[sorting] - cmplx_data_vector = np.array(data_vector_real, dtype=np.complex128) - cmplx_data_vector = cmplx_data_vector[sorting] - axis = (magnetic_field, time_axis) - - return axis, cmplx_data_vector.T
- - - -
-[docs] -def read_single_transient_file( - fpath: str, - filenumber: int, - digits: int, - time: bool = False, -) -> Tuple[float, np.array]: - """ - Get the measured intensities of a single field point from a transient epr - experiment measured by ESP380E. - - Parameters - ---------- - fpath : str - Full path to the folder with the experimental data. - filenumber : int - Number of the dataset for the magnetic field point e. g. 003. - digits : int - Number of digits from the highest filenumber (ESP380E has an increasing - number as fileextension for each magnetic field point e. g. data.001). - time : bool, optional - If True, the time axis will be returned. - - Returns - ------- - field : float - Magnetic field point. - time_axis : np.array - Linear time axis. - data_vector : np.array - Measured intensities. - - """ - fbasename = os.path.basename(fpath) - ending = ".{0:0>{1}s}".format(str(filenumber), str(digits)) - fname = os.path.join(fpath, fbasename + ending) - - try: - data_vector = np.genfromtxt(fname, skip_header=5) - data_vector = data_vector.flatten() - except ValueError: - data_vector = [] - with open(fname) as file: - for i, line in enumerate(file.readlines()): - if i < 5: - continue - else: - line = line.strip().split() - data_vector.extend(line) - data_vector = np.array(data_vector, dtype=np.complex128) - - with open(fname) as file: - for i, line in enumerate(file.readlines()): - if line.startswith("B0"): - line = line.strip().split() - field = float(line[2]) - if i == 3 and time: - line = line.strip().split() - points = int(line[1]) - start = float(line[2]) - stop = float(line[3]) - time_axis = np.linspace(start, stop, points) - if i == 4: - break - - if time: - return (field, time_axis), data_vector - - return field, data_vector
- - - -
-[docs] -def load_simulated_data( - folder: str, start_directory: str = None -) -> Tuple[np.array, np.array, np.array]: - """ - Load the simulated data from EasyPairSpin or data saved with - saving.save_simulation(). Using numpy.loadtxt(). - - Parameters - ---------- - folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional - Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: - load_simulated_data(this_folder, 'cooluser/nice') - - Recommended: - Without this parameter, the search for the right folder - will be much longer. - - Returns - ------- - x : np.array - Axis vector for the x-axis. - y : np.array - Axis vector for the y-axis. Only returned, if the simulated data is 2d. - intensity : np.array, np.complex128 - Simulated intensities. Either 1d or 2d. - - """ - path = get_full_path(folder, start_directory) - - x = np.loadtxt(os.path.join(path, "x_axis.txt")) - try: - intensity = np.loadtxt(os.path.join(path, "intensity.txt")) - except ValueError: - intensity = np.loadtxt(os.path.join(path, "intensity.txt"), dtype=np.complex_) - - try: - y = np.loadtxt(os.path.join(path, "y_axis.txt")) - except IOError: - return (x, intensity) - else: - return (x, y, intensity)
- - - -
-[docs] -def load_matlab( - folder: str, - start_directory: str = None, - field: str = "field", - signal: str = "signal", -) -> Tuple[np.array, np.array, np.array]: - """ - Load EPR data from a matlab data file .mat - - Parameters - ---------- - folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional - Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: - load_simulated_data(this_folder, 'cooluser/nice') - field : str, optional - Name of the field array. The default is 'field'. - signal : str, optional - Name of the signal array. The default is 'signal'. - - Returns - ------- - axis : np.array - Tuple of np.array containing the x and y axis. - data : np.array - np.array with the measured intensities. - - """ - path = get_full_path(folder, start_directory) - mat_data = loadmat(path) - x = mat_data[field].flatten() - y = np.array([]) - data = mat_data[signal].flatten() - axis = (x, y) - - return axis, data
- - - -
-[docs] -def load_txt( - folder: str, start_directory: str = None -) -> Tuple[np.array, np.array, np.array]: - """ - Load EPR data from a .txt file. - - Parameters - ---------- - folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional - Give the path starting from your home folder which the search for - the data folder should start at. The default is ''. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: - load_simulated_data(this_folder, 'cooluser/nice') - - Returns - ------- - axis : np.array - Tuple of np.array containing the x and y axis. - data : np.array - np.array with the measured intensities. - - """ - path = get_full_path(folder, start_directory) - try: - x, data = np.loadtxt(path) - y = np.array([]) - except ValueError: - try: - x, data = np.loadtxt(path, unpack=True) - y = np.array([]) - except ValueError: - try: - x, y, data = np.loadtxt(path) - except ValueError: - x, y, data = np.loadtxt(path, unpack=True) - axis = (x, y) - - return axis, data
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/plotting.html b/docs/build/html/_modules/spinanalysis/plotting.html deleted file mode 100644 index 6b62aee..0000000 --- a/docs/build/html/_modules/spinanalysis/plotting.html +++ /dev/null @@ -1,754 +0,0 @@ - - - - - - - - spinanalysis.plotting — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.plotting

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-from matplotlib import cm
-import matplotlib.pyplot as plt
-import numpy as np
-import os
-import sys
-from spinanalysis import profiles
-
-
-
-[docs] -class HiddenPrints: - """Supress Error Messages in a context manager.""" - - def __enter__(self): - """Deactivate the error stream when entering the context manager.""" - self._original_stderr = sys.stderr - sys.stderr = open(os.devnull, "w") - - def __exit__(self, exc_type, exc_val, exc_tb): - """Activate the error stream when leaving the context manager.""" - sys.stderr.close() - sys.stderr = self._original_stderr
- - - -
-[docs] -def plot_2D( - x: np.array, - y: np.array, - mpl_stylesheet: str = "default_stylesheet", - labels: list[str] = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot the given y value(s) against the given x array. - - Using matplotlib.pylab.plot(). The plot can be configured via plot - profiles. - - Parameters - ---------- - x : np.array - Array with values for the x axis. - y : np.array - 1D-Array or 2D-Array with values for y axis. - mpl_stylesheet : str, optional - Name of the matplotlib style sheet (see: matplotlib documentation). - If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : list[str], optional - List of labels for the legend. If only one label is given, all labels - will be the same. The default is 'no_label'. - ax : object, optional - Axes object, used for the PySpin GUI. - **kwargs : optional - Keyword arguments passed to the matplotlib plot function. Overrides the - arguments given in the stylesheet. - - Returns - ------- - fig : object - Figure object of matplotlib.pylab. - - """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) - - with HiddenPrints(): - with plt.style.context(default_styles): - if ax is None: - # [NEW FIGURE] - fig, ax = plt.subplots() - else: - fig, _ = plt.subplots() - - # [LABELS] - if y.ndim > 1 and not (type(labels) is list): - labels = _get_label_list(y.shape[0], labels) - - # [PLOT DATA] - if y.ndim == 1: - ax.plot(x, y, label=labels, **kwargs) - else: - for y_set in range(len(y)): - ax.plot(x, y[y_set], label=labels[y_set], **kwargs) - - # [Axes] - _set_axis(ax, profile, x, y) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) - - return fig
- - - -
-[docs] -def shifted_2D( - x: np.array, - Y: np.array, - mpl_stylesheet: str = "default_stylesheet", - labels: list[str] = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot multiples lines in 2D, shifted vertically. - - The plot can be configured via plot profiles. - - Parameters - ---------- - x : np.array - Array with values for the x axis. - Y : np.array - 2D-Array with values for y axis. - mpl_stylesheet : str, optional - Name of the matplotlib style sheet (see: matplotlib documentation). - If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : list[str], optional - List of labels for the legend. If only one label is given, all labels - will be the same. The default is 'no_label'. - ax : object, optional - Axes object, used for the PySpin GUI. - **kwargs : optional - Keyword arguments passed to the matplotlib plot function. Overrides the - arguments given in the stylesheet. - - Returns - ------- - fig : object - Figure object of matplotlib.pylab. - - """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) - - with HiddenPrints(): - with plt.style.context(default_styles): - # [NORMALIZE DATA] - Y = Y / np.max(np.abs(Y)) - - # [SHIFT DATA] - shift_matrix = _get_shift_matrix(Y.shape) - Y += shift_matrix - - if ax is None: - # [NEW FIGURE] - fig, ax = plt.subplots() - else: - fig = None - - # [LABELS] - if not type(labels) is list: - labels = _get_label_list(Y.shape[0], labels) - - # [PLOT DATA] - for y_set in range(Y.shape[0]): - ax.plot(x, Y[y_set], label=labels[y_set], **kwargs) - - # [AXES] - _set_axis(ax, profile, x, Y, y_axis=False) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) - - return fig
- - - -
-[docs] -def plot_3D( - x: np.array, - y: np.array, - Z: np.array, - mpl_stylesheet: str = "default_stylesheet", - labels: str = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data in 3D using matplotlib.pylab.plot_surface(). - - The plot can be configured via plot profiles. - - Parameters - ---------- - x : np.array - Array with values for the x axis. - y : np.array - Array with values for the y axis. - Z : np.array - 2D-Array with intensities. - mpl_stylesheet : str, optional - Name of the matplotlib style sheet (see: matplotlib documentation). - If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : str, optional - At the moment no function. The default is 'no_label'. # TODO - ax : object, optional - Axes object, used for the PySpin GUI. - **kwargs : optional - Keyword arguments passed to the matplotlib plot function. Overrides the - arguments given in the stylesheet. - - Returns - ------- - fig : object - Figure object of matplotlib.pylab. - - """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) - - kwargs_ = {"cmap": cm.coolwarm, "antialiased": True, "linewidth": 0} - kwargs_.update(kwargs) - - with HiddenPrints(): - with plt.style.context(default_styles): - if ax is None: - # [NEW FIGURE] - fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) - else: - fig = None - - # [GRID] - X, Y = np.meshgrid(x, y) - - # [PLOT DATA] - try: - ax.plot_surface(X, Y, Z, **kwargs_) - except ValueError: - ax.plot_surface(X, Y, Z.T, **kwargs_) - - # [AXES] - _set_axis(ax, profile, X, Y, Z) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) - - return fig
- - - -
-[docs] -def plot_3D_multiple_lines( - x: np.array, - y: np.array, - Z: np.array, - mpl_stylesheet: str = "default_stylesheet", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data in 3D using matplotlib.pylab.plot(). - - Each y trace as a single line plot. The plot can be configured via plot - profiles. - - Parameters - ---------- - x : np.array - Array with values for the x axis. - y : np.array - Array with values for the y axis. - Z : np.array - 2D-Array with intensities. - mpl_stylesheet : str, optional - Name of the matplotlib style sheet (see: matplotlib documentation). - If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - ax : object, optional - Axes object, used for the PySpin GUI. - **kwargs : optional - Keyword arguments passed to the matplotlib plot function. Overrides the - arguments given in the stylesheet. - - Returns - ------- - fig : object - Figure object of matplotlib.pylab. - - """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) - - with HiddenPrints(): - with plt.style.context(default_styles): - if ax is None: - # [NEW FIGURE] - fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) - else: - fig = None - - # [PLOT DATA] - # TODO: Hier muss drüber nachgedacht werden, wann Z transponiert werden - # muss, damit man die x- und y-Achse in beliebiger Reihenfolge geben - # kann. - X = np.ones((len(y), len(x))) * x - X = X.T - for dataset in range(len(Z.T)): - ax.plot(X[dataset], y, Z[:, dataset], **kwargs) - - # [AXES] - _set_axis(ax, profile, x, y, Z) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) - - return fig
- - - -
-[docs] -def heatmap( - x: np.array, - y: np.array, - Z: np.array, - mpl_stylesheet: str = "default_stylesheet", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data as a heatmap using matplotlib.pylab.pcolormesh(). - - The plot can be configured via plot profiles. - - Parameters - ---------- - x : np.array - Array with values for the x axis. - y : np.array - Array with values for the y axis. - Z : np.array - 2D-Array with intensities. - mpl_stylesheet : str, optional - Name of the matplotlib style sheet (see: matplotlib documentation). - If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - ax : object, optional - Axes object, used for the PySpin GUI. - **kwargs : optional - Keyword arguments passed to the matplotlib plot function. Overrides the - arguments given in the stylesheet. - - Returns - ------- - fig : object - Figure object of matplotlib.pylab. - - """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) - - kwargs_ = {"cmap": "RdBu", "shading": "auto"} - kwargs_.update(kwargs) - - with HiddenPrints(): - with plt.style.context(default_styles): - if ax is None: - # [NEW FIGURE] - fig, ax = plt.subplots() - else: - fig = None - - # [GRID] - X, Y = np.meshgrid(x, y) - - # [PLOT DATA] - try: - c = ax.pcolormesh(X, Y, Z, **kwargs_) - except TypeError: - c = ax.pcolormesh(X, Y, Z.T, **kwargs_) - - # [PLOT COLORBAR] - if profile["colorbar"]: - fig.colorbar(c, ax=ax) - - # [AXES] - _set_axis(ax, profile, X, Y) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) - - return fig
- - - -def _get_shift_matrix(shape: tuple[int, int]) -> np.array: - """ - Create a matrix to shift the single lines vertically. - - Needed for plot_shifted_2D(). - - Parameters - ---------- - shape : tuple[int, int] - Shape of the data which will be shifted. Use np.array.shape to get the - shape from your data. - - Returns - ------- - shift_matrix : np.array - Matrix to shift your data vertically by adding the shift matrix. - E. g. : structure of the shift matrix for shape (3, 2): - :: - - [[0, 0], - [2, 2], - [4, 4]] - - """ - shift_matrix = 2 * np.linspace( - 0, np.full((1, shape[1]), (shape[0] - 1))[0], shape[0] - ) - - return shift_matrix - - -def _set_axis( - ax: object, figure_par: dict, *axes: np.array, y_axis: bool = True -) -> object: - """ - Set up all axis for a plot created by matplotlib. - - Parameters - ---------- - ax : object - Axis object of matplotlib.pylab. - figure_par : dict - Dictionary containing all settings for the figure. - *axes : np.array - All axes used for the plot. Used to determine the dimension an set up - the z axis if needed. - y_axis : bool, optional - If a y axis is needed. If False, all y ticks will be removed. The - default is True. - - Returns - ------- - ax : object - Modified axis object of matplotlib.pylab. - - """ - ax.set_xlabel(figure_par["xlabel"]) - ax.set_ylabel(figure_par["ylabel"]) - ax.set_xlim( - _get_axis_limit(axes[0], figure_par["xlim"], figure_par["percentage_mode"]) - ) - if y_axis: - ax.set_ylim( - _get_axis_limit(axes[1], figure_par["ylim"], figure_par["percentage_mode"]) - ) - else: - ax.set_yticks([]) - - if len(axes) == 3: - ax.set_zlabel(figure_par["zlabel"]) - ax.set_zlim( - _get_axis_limit(axes[2], figure_par["zlim"], figure_par["percentage_mode"]) - ) - - return ax - - -def _set_figure(ax: object, figure_par: dict, mplstylesheet: str = None) -> object: - """ - Set up the figure of a plot created by matplotlib. - - Parameters - ---------- - ax : object - Axis object of matplotlib.pylab. - figure_par : dict - Dictionary containing all settings for the figure. - mplstylesheet : str, optional - Name of the stylesheet which should be used instead of a plotting - profile. If no filename is given, the settings from the given plotting - profile will be used. The default is None. - - Returns - ------- - ax : object - Modified axis object of matplotlib.pylab. - - """ - if figure_par["show_title"]: - ax.set_title(figure_par["title"]) - if figure_par["legend"]: - ax.legend() - - return ax - - -def _get_style_path(mplstylesheet: str = None) -> str: - """ - Get the path to the choosen mpl stylesheet. - - Parameters - ---------- - mplstylesheet : str, optional - Name of the mpl stylesheet. The default is None. - - Returns - ------- - str - Path to the choosen stylesheet. - - """ - if mplstylesheet is None: - style = os.path.join( - sys.prefix, - "easypairspin", - "profiles", - "plot", - "default_stylesheet", - ) - elif mplstylesheet in plt.style.available: - style = mplstylesheet - else: - style = os.path.join( - sys.prefix, "easypairspin", "profiles", "plot", mplstylesheet - ) - - return style - - -def _load_default_styles() -> list: - """ - Load the pathes for the default matplotlib styles. - - Returns - ------- - default_styles : list - Pathes for the default style sheetss. - - """ - default_styles = [] - for def_style in ["axis", "figure", "plot", "line", "text"]: - def_style = def_style + "_style" - default_styles.append(_get_style_path(def_style)) - - return default_styles - - -def _get_label_list(length: int, label: str) -> list[str]: - """ - Get a list with length times the label. - - Parameters - ---------- - length : int - Number of labels. - label : str - String which will be multiplied. - - Returns - ------- - list[str] - Length times label as a list. E. g.: ['label', 'label', 'label']. - - """ - labels = [label] * length - - return labels - - -def _get_axis_limit( - data_axis: np.array, ax_lim: list, percentage_mode: bool -) -> [float, float]: - """ - Get the plot limits for a given data axis. - - If no limit is specified, the minimal and maximal value of the given - dataset will be taken. - - Parameters - ---------- - data_axis : np.array - Data vector for an axis. - ax_lim : list - Limits for the data vector taken from a profile. - percentage_mode : bool - If True, the ax.lim will be handled as a percentage in context to the - lowest and highest values with respect to the given axis. - E. g.: x-vector with values [0, 200], ax_lim: [-2., 2.] -> [4, 204]. - - Returns - ------- - [float, float] - Limits for plotting. - - """ - if percentage_mode: - if ax_lim == "": - min_val = data_axis.min() - max_val = data_axis.max() - limits = [min_val, max_val] - else: - value_range = data_axis.max() - data_axis.min() - lower_diff = value_range * ax_lim[0] / 100 - upper_diff = value_range * ax_lim[1] / 100 - lower_border = data_axis.min() - lower_diff - upper_border = data_axis.max() + upper_diff - limits = [lower_border, upper_border] - else: - if ax_lim == "": - min_val = data_axis.min() - max_val = data_axis.max() - limits = [min_val, max_val] - else: - limits = ax_lim - - return limits[0], limits[1] -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/processing.html b/docs/build/html/_modules/spinanalysis/processing.html deleted file mode 100644 index 856efb5..0000000 --- a/docs/build/html/_modules/spinanalysis/processing.html +++ /dev/null @@ -1,467 +0,0 @@ - - - - - - - - spinanalysis.processing — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.processing

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-import numpy as np
-from scipy.optimize import curve_fit
-from scipy import integrate
-from statsmodels.tsa.ar_model import AutoReg, ar_select_order
-
-
-
-[docs] -def normalization(x: np.array, mode: str = None, dx: float = None) -> np.array: - r""" - Normalize the given data. - - .. math:: - x_{\mathrm{norm}} = \frac{x_i - min(x)}{max(x)-min(x)} - - Parameters - ---------- - x : np.array - Unnormalized data. - mode : str, optional - If 'area', the total AUC will be 1, if 'value', the maximum value will - be 1 or -1, else, the formula mentioned above will be used. Default is - None. - dx : float, optional - Distance between two points on the x axis. Only used for simpson - integration. Default is None. - - Returns - ------- - x_norm : np.array - Normalized data. - - """ - if mode is None: - if x.dtype == "complex": - x_norm = np.zeros(x.shape, dtype=np.complex128) - x_norm.real = (x.real) / (x.real.max() - x.real.min()) - x_norm.imag = (x.imag) / (x.imag.max() - x.imag.min()) - else: - x_norm = (x) / (x.max() - x.min()) - elif mode == "area": - if x.dtype == "complex": - x_norm = np.zeros(x.shape, dtype=np.complex128) - x_norm.real = x.real / abs(integrate.simpson(abs(x), dx=dx)) - try: - x_norm.imag = x.imag / abs(integrate.simpson(abs(x), dx=dx)) - except ZeroDivisionError: - pass - else: - x_norm = x / abs(integrate.simpson(x, dx=dx)) - elif mode == "value": - if x.dtype == "complex": - x_norm = np.zeros(x.shape, dtype=np.complex128) - x_norm.real = x.real / max(abs(x.real)) - try: - x_norm.imag = x.imag / max(abs(x.imag)) - except ZeroDivisionError: - pass - else: - x_norm = x / max(abs(x)) - - return x_norm
- - - -
-[docs] -def reduce_offset(x: np.array) -> np.array: - """ - Eliminate the offset of the data by using the mean of the last quarter. - - Parameters - ---------- - x : np.array - Given data, e. g. measured intensities. - - Returns - ------- - x_shifted : np.array - Shifted data without offset. - - """ - start = 3 * x.shape[0] // 4 - if x.dtype == "complex": - x_shifted = np.zeros(x.shape, dtype=np.complex128) - x_shifted.real = x.real - x.real[start:].mean() - x_shifted.imag = x.imag - x.imag[start:].mean() - else: - x_shifted = x - x[start:].mean() - - return x_shifted
- - - -
-[docs] -def background_corr(x: np.array, y: np.array, mode: str = "biexp") -> np.array: - """ - Perform a background correction of measured data. - - Available correction modes are: biexp, exp, lin, poly2, poly3 and poly4. - biexp and exp are using exponential models for the background. lin, poly2-4 - are polynominal models of first to fourth order. Default is 'biexp'. - - .. warning:: - poly3 and poly4 can lead to overfitting! - - Parameters - ---------- - x : np.array - x axis of the dataset. - y : np.array - y data which will be background corrected. - mode : str, optional - Select the type of the background. The default is 'biexp'. - - Returns - ------- - y_corr : np.array - Background corrected y data. - - """ - if mode == "biexp": - p0 = [0.9, -0.002, 0.05, -0.0009, 0.0] - popt, pcov = curve_fit(biexp_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - biexp_fun(x, *popt) - elif mode == "exp": - p0 = [1.0, -0.002, 0.0] - popt, pcov = curve_fit(exp_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - exp_fun(x, *popt) - elif mode == "lin": - p0 = [1.0, -0.002] - popt, pcov = curve_fit(lin_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - lin_fun(x, *popt) - elif mode == "poly2": - p0 = [1.0, -0.002, 0.0] - popt, pcov = curve_fit(poly2_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - poly2_fun(x, *popt) - elif mode == "poly3": - p0 = [1.0, 1.0, -0.002, 0.0] - popt, pcov = curve_fit(poly3_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - poly3_fun(x, *popt) - elif mode == "poly4": - p0 = [1.0, 1.0, 1.0, -0.002, -1.0] - popt, pcov = curve_fit(poly4_fun, x, y, p0=p0, maxfev=10000) - y_corr = y - poly4_fun(x, *popt) - - return y_corr
- - - -
-[docs] -def exp_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized monoexponential function for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the monoexponential function which will be fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b, c = coeff - y = a * np.exp(b * x) + c - return y
- - - -
-[docs] -def biexp_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized biexponential function for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the biexponential function which will be fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b, c, d, e = coeff - y = a * np.exp(b * x) + c * np.exp(d * x) + e - return y
- - - -
-[docs] -def lin_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized linear function for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the linear function which will be fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b = coeff - y = a * x + b - return y
- - - -
-[docs] -def poly2_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 2 for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the polynominal function of degree 2 which will be - fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b, c = coeff - y = a * x**2 + b * x + c - return y
- - - -
-[docs] -def poly3_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 3 for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the polynominal function of degree 3 which will be - fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b, c, d = coeff - y = a * x**3 + b * x**2 + c * x + d - return y
- - - -
-[docs] -def poly4_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 4 for background correction. - - Parameters - ---------- - x : np.array - x values used to calculate corresponding y values. - *coeff : float - Variables for the polynominal function of degree 4 which will be - fitted. - - Returns - ------- - y : np.array - Calculated y values. - - """ - a, b, c, d, e = coeff - y = a * x**4 + b * x**3 + c * x**2 + d * x + e - return y
- - - -
-[docs] -def reconstruct(x: np.array, y: np.array) -> np.array: - """ - Reconstruction of a time signal use the Yule-Walker algorithm. - - Parameters - ---------- - x : np.array - x axis. - y : np.array - Intensities. - - Returns - ------- - x_new : np.array - Reconstructed x axis. - y : np.array - Reconstructed intensities. - - """ - # prepare the new array - x_step = x[1] - x[0] - if x[0] % x_step != 0: - x_fill_points = int(x[0] / x_step) + 1 - else: - x_fill_points = int(x[0] / x_step) - x_new = np.concatenate( - (np.linspace(x[0] - x_step * x_fill_points, x[0] - x_step, x_fill_points), x) - ) - - # determine the order of the p value for the reconstruction - order = ar_select_order(y[::-1], maxlag=40) - nlag = len(order.ar_lags) - - # Fit the model to the data and make a predicition - AutoRegFit = AutoReg(y[::-1], lags=order.ar_lags).fit() - y_pred = AutoRegFit.predict(start=0, end=x_new.shape[0] + nlag - 1) - - y_pred = np.roll(y_pred[nlag:], nlag) - y_flip = y_pred - y_flip = np.concatenate((y[::-1], y_flip[len(y) :])) - - return x_new, y_flip[::-1]
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/profiles.html b/docs/build/html/_modules/spinanalysis/profiles.html deleted file mode 100644 index e852aee..0000000 --- a/docs/build/html/_modules/spinanalysis/profiles.html +++ /dev/null @@ -1,1024 +0,0 @@ - - - - - - - - spinanalysis.profiles — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.profiles

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-from configobj import ConfigObj
-from matplotlib.pyplot import style
-from spinanalysis._utils import strtobool
-import validate
-import os
-import sys
-from zipfile import ZipFile
-
-
-
-[docs] -def import_profiles(zipfile: str, override: bool = False) -> None: - """ - Import profiles from a zip archive. - - Parameters - ---------- - zipfile : str - Path to the zip archive. - override : bool, optional - If True, existing profile with the same name will be overriden. The - default is False. - - Returns - ------- - None - Nothing will be returned. - - """ - zipfile = os.path.abspath(zipfile) - profiles_folder = os.path.join(sys.prefix, "easypairspin", "profiles") - - with ZipFile(zipfile, "r") as zipfile: - for file in zipfile.namelist(): - if os.path.isfile(os.path.join(profiles_folder, file)) and not override: - continue - - zipfile.extract(file, profiles_folder) - - zipfile.close()
- - - -
-[docs] -def export(path: str = None, pkind: [str, list] = "all", pname: str = "all") -> None: - """ - Export the choosen profile(s) as a zip archive. - - Parameters - ---------- - path : str, optional - Path where the zip file will be stored. If no path given, the zip file - will be stored in the current working directory. The default is None. - pkind : str, optional - Define which kind(s) of profiles should be exported. Multiple kinds of - profiles possible. Options are 'plot', 'spinsystem', 'optimization', - 'save', 'variation', 'simulation' and 'all'. The default is 'all'. - pname : str, optional - Give the basename of the profile. The default is 'all'. - - Returns - ------- - None - Nothing will be returned. - - """ - profiles_folder = os.path.join(sys.prefix, "easypairspin", "profiles") - - if pkind == "all": - pkind = [ - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ] - else: - if not isinstance(pkind, list): - stack = [] - for el in pkind.lower().strip().split(): - if el in ( - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ): - stack.append(el) - elif el == "optimisation": - stack.append("optimization") - else: - continue - pkind = stack - - else: - stack = [] - for el in pkind: - if el.lower() in ( - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ): - stack.append(el.lower()) - elif el.lower() == "optimisation": - stack.append("optimization") - else: - continue - pkind = stack - - file_paths = [] - for kind in pkind: - file_paths.extend(_get_profile_paths(os.path.join(profiles_folder, kind))) - - if path is None: - path = os.path.join(os.getcwd(), "profiles_easypairspin.zip") - else: - path = os.path.join(path, "profiles_easypairspin.zip") - - with ZipFile(path, "w") as zipfile: - for file in file_paths: - arcname = file[len(profiles_folder) + 1 :] - zipfile.write(file, arcname) - - zipfile.close()
- - - -def _get_profile_paths(pfolder: str, pname: str = "all") -> None: - """ - Get the pathes of all profile. - - Parameters - ---------- - pfolder: : str - Folder with profiles. - pname : str, optional - Basename of the profile. The default is "all". - - Returns - ------- - None - Nothing will be returned. - - """ - paths = [] - - for root, directories, files in os.walk(pfolder): - for file in files: - if file.endswith("configspec.ini"): - continue - - if pname.lower() == "all": - paths.append(os.path.join(pfolder, file)) - elif pname.lower() == os.path.basename(file): - paths.append(os.path.join(pfolder, file)) - else: - continue - - return paths - - -
-[docs] -def add_profile(profile: dict, pkind: str, pname: str = "") -> None: - """ - Add a new profile for EasyPairSpin. - - Parameters - ---------- - profile : dict - Dictionary with all profile settings. - pkind : str - Give the kind of the kind of the profile. Not case sensitive. pkind can - be 'plot', 'save', 'simulation', 'optimization', 'spinsystem' or - 'variation'. - pname : str, optional - Name of the profile. If no profile name is given, a default one will - be generated by _get_profile_name(). The default is ''. - - - Raises - ------ - ValueError - Raised if pkind isn't 'plot', 'save', 'simulation', 'optimization', - 'spinsystem' or 'variation'. - - Returns - ------- - None - Nothing will be returned. - - Examples - -------- - Creating and adding a new profile: - - >>> Sys = epr_setup.Spinsystem() - >>> Sys_profile = new_spinsystem_profile() - >>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] - >>> prom.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1') - - """ - pkind = pkind.lower() - if pkind == "optimisation": - pkind = "optimization" - - if pkind not in ( - "plot", - "save", - "simulation", - "optimization", - "spinsystem", - "variation", - ): - raise ValueError( - "pkind must be 'plot', 'save', 'simulation'," - " 'optimization', 'spinsystem' or 'variation'! " - ) - - if pname == "": - pname = _get_profile_name(pkind) - - config_path = os.path.join(sys.prefix, "easypairspin", "profiles", pkind) - if pkind == "plot": - config_name = os.path.join(config_path, pname) - else: - config_name = os.path.join(config_path, pname + ".ini") - path_to_configspec = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, "configspec.ini" - ) - - if not os.path.exists(config_path): - os.makedirs(config_path) - - config = ConfigObj(config_name, configspec=path_to_configspec) - - # TODO Exception Handling, ob alle relevanten Werte (korrekt) gegeben sind - # TODO match case Struktur einführen (wenn Python 3.10 möglich) - if pkind == "plot": - _save_config_plot(profile, config_name) - elif pkind == "spinsystem": - config = _set_configobj_spinsystem(profile, config) - elif pkind == "variation": - config = _set_configobj_variation(profile, config) - elif pkind == "optimization": - config = _set_configobj_optimization(profile, config) - elif pkind == "save": - config = _set_configobj_save(profile, config) - elif pkind == "simulation": - config = _set_configobj_simulation(profile, config) - - if not (pkind == "plot"): - validator = validate.Validator() - config.validate(validator) - config.write() - - return None
- - - -def _save_config_plot(profile: dict, path: str) -> object: - """ - Write profile parameters into the ConfigObject for a plot profile. - - Parameters - ---------- - profile : dict - Plot profile. - path : str - Path, where the profile will be stored. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - preamble = "## {:*^76s}\n## {:*^76s}\n## {:*^76s}\n".format( - "", " EASYPAIRSPIN SPECIAL SETTINGS ", "" - ) - with open(path, "w") as f: - f.write(preamble) - f.write("\n") - for key in profile: - if key.endswith("lim"): - line = "{}: {}, {}".format( - str(key), str(profile[key][0]), str(profile[key][1]) - ) - else: - if isinstance(profile[key], (int, float, bool)): - line = "{}: {}".format(str(key), str(profile[key])) - else: - line = '{}: "{}"'.format(str(key), str(profile[key])) - f.write(line) - f.write("\n") - f.close() - - -def _set_configobj_spinsystem(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a spinsystem profile. - - Parameters - ---------- - profile : dict - Spinsystem profile. - ConObj : object - ConfigObj from module configobj. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - return ConObj - - -def _set_configobj_variation(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a variation profile. - - Parameters - ---------- - profile : dict - Variation profile. - ConObj : object - ConfigObj from module configobj. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - return ConObj - - -def _set_configobj_optimization(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for an optimization profile. - - Parameters - ---------- - profile : dict - Optimization profile. - ConObj : object - ConfigObj from module configobj. - - Raises - ------ - ValueError - If no valid optimization routine is given. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - routine = profile["main"]["routine"] - if routine in [ - "genetic", - "minimize", - "dual_annealing", - "shgo", - "differential_evolution", - "basinhopping", - "least_squares", - ]: - ConObj[routine] = profile[routine] - else: - raise ValueError("{} is no valid optimization routine!".format(routine)) - - return ConObj - - -def _set_configobj_simulation(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a simulation profile. - - Parameters - ---------- - profile : dict - Simulation profile. - ConObj : object - ConfigObj from module configobj. - - Raises - ------ - ValueError - If no valid simulation routine is given. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - routine = profile["main"]["routine"] - if routine in ["static_radpair", "teacups", "opossum", "didelphis"]: - ConObj[routine] = profile[routine] - else: - raise ValueError("{} is no valid simulation routine!".format(routine)) - - return ConObj - - -def _set_configobj_save(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a save profile. - - Parameters - ---------- - profile : dict - Save profile. - ConObj : object - ConfigObj from module configobj. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - return ConObj - - -def _get_profile_name(pkind: str) -> str: - """ - Search for the smallest number available for the default profile name. - - Scheme for default profile name is 'profile_[number]' - - Parameters - ---------- - pkind : str - Give the kind of the kind of the profile. Not case sensitive. pkind can - be 'plot', 'save', 'simulation', 'optimization', 'spinsystem' or - 'variation'. - - Returns - ------- - name : str - String with the available profile name e.g. 'profile_12'. - - """ - name = "profile_" - suffix = "" if pkind == "plot" else ".ini" - profile_number = 0 - profile_number_used = True - - while profile_number_used: - profile_number += 1 - for root, dirs, files in os.walk( - os.path.join(sys.prefix, "easypairspin", "profiles", pkind) - ): - counter = 0 - for file in files: - if file.endswith("profile_" + str(profile_number) + suffix): - counter += 1 - - profile_number_used = counter - - name += str(profile_number) - - return name - - -
-[docs] -def load_profile(pname: str, pkind: str) -> dict: - """ - Load a given plotting profile. - - Parameters - ---------- - pname : str - Name of the profile. Case sensitive. Either with .ini or not. - E. g.: load_profile('test') or load_profile('test.ini'). - pkind : str - Give the kind of the profile. Not case sensitive. pkind can be 'save', - 'simulation', 'optimization', 'spinsystem' or 'variation'. - - Returns - ------- - profile: dict - Loaded profile as a dictionary. - - """ - if pname.endswith(".ini"): - pname = pname[:-4] - - path_to_profile = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, pname + ".ini" - ) - path_to_configspec = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, "configspec.ini" - ) - - config = ConfigObj(path_to_profile, configspec=path_to_configspec, file_error=True) - validator = validate.Validator() - config.validate(validator) - - profile = {} - - for section in config.sections: - profile[section] = config[section] - - return profile
- - - -
-[docs] -def load_plot_profile(pname: str) -> dict: - """ - Load a plotting profile from a mplstylesheet. - - Parameters - ---------- - pname : str - Name of the profile. Case sensitive. - - Returns - ------- - dict - Contains the settings for the plotting functions. - - """ - if pname is None or pname in style.available: - pname = "default_stylesheet" - - path_to_profile = os.path.join( - sys.prefix, "easypairspin", "profiles", "plot", pname - ) - - profile = {} - with open(path_to_profile, "r") as file: - for line in file.readlines(): - if line.startswith("#"): - continue - else: - line = line.strip().split(":") - if line[0] in ( - "percentage_mode", - "xlim", - "ylim", - "zlim", - "xlabel", - "ylabel", - "zlabel", - "show_title", - "title", - "legend", - "colorbar", - ): - if "lim" in line[0] and len(line[0]) == 4: - bounds = line[1].split("#")[0].strip() - bounds = bounds.split(",") - lb = float(bounds[0]) - ub = float(bounds[1]) - profile[line[0]] = [lb, ub] - elif ("label" in line[0] and len(line[0]) == 6) or ( - line[0] == "title" - ): - label = line[1].split("#")[0].strip() - label = label.strip('"').strip("'") - profile[line[0]] = label - else: - boolean = line[1].split("#")[0].strip() - boolean = boolean.strip('"').strip("'") - profile[line[0]] = bool(strtobool(boolean)) - - return profile
- - - -
-[docs] -def new_plot_profile() -> dict: - """ - Get an empty plotting profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for plottings. - - """ - default_profile = { - "percentage_mode": False, - "xlim": [0, 0], - "ylim": [0, 0], - "zlim": [0, 0], - "xlabel": "", - "ylabel": "", - "zlabel": "", - "legend": "off", - "show_title": "off", - "title": "", - "colorbar": "on", - } - - return default_profile
- - - -
-[docs] -def new_spinsystem_profile() -> dict: - """ - Get a default spinsystem profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for a spinsystem. - - """ - default_profile = { - "main": { - "spin_system": "rp", - "precursor": "triplet-zf", - "population": [1.0, 0.0, 0.0], - "g1": [2.002, 2.002, 2.002], - "g2": [2.004, 2.004, 2.004], - "g_tri": [2.002, 2.002, 2.002], - "g": [2.004, 2.004, 2.004], - "g1_frame": [0, 0, 0], - "g2_frame": [0, 0, 0], - "g_tri_frame": [0, 0, 0], - "g_frame": [0, 0, 0], - "width_gauss": 0.5, - "acceptor_list": [1, 2, 3], - "donor_list": [4, 5], - "A1": [0, 0, 0], - "A2": [0, 0, 0], - "A3": [0, 0, 0], - "A4": [0, 0, 0], - "A5": [0, 0, 0], - "A_eseem": 0.0, - "omega_I": 0.0, - "A1_frame": [0, 0, 0], - "A2_frame": [0, 0, 0], - "A3_frame": [0, 0, 0], - "A4_frame": [0, 0, 0], - "A5_frame": [0, 0, 0], - "n1": 0, - "I1": 0, - "n2": 0, - "I2": 0, - "n3": 0, - "I3": 0, - "n4": 0, - "I4": 0, - "n5": 0, - "I5": 0, - "D": 0.0, - "E": 0.0, - "D_tri": 700.0, - "E_tri": 0.0, - "beta": 1.4, - "J_0": 1e10, - "J_ex": 0.1, - "D_frame": [0, 0, 0], - "D_tri_frame": [0, 0, 0], - "T_relax_1": 0.0, - "T_relax_2": 0.0, - "decay": 0.0, - "dynamics": [0.0, 0.0, 0.0, 0.0], - "T_pm": 0.1, - "amplitude": 0.0, - "distribution": None, - "distribution_order": 3, - } - } - - return default_profile
- - - -
-[docs] -def new_variation_profile() -> dict: - """ - Get a default variation profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for variation. - - """ - default_profile = { - "main": { - "g1": [0, 0, 0], - "g2": [0, 0, 0], - "g_tri": [0, 0, 0], - "g": [0, 0, 0], - "A1": [0, 0, 0], - "A2": [0, 0, 0], - "A3": [0, 0, 0], - "A4": [0, 0, 0], - "A5": [0, 0, 0], - "A_eseem": 0.0, - "omega_I": 0.0, - "D": 0.0, - "D_tri": 0.0, - "E": 0.0, - "E_tri": 0.0, - "beta": 0.0, - "J_0": 0.0, - "J_ex": 0.0, - "g1_frame": [0, 0, 0], - "g2_frame": [0, 0, 0], - "g_tri_frame": [0, 0, 0], - "g_frame": [0, 0, 0], - "A1_frame": [0, 0, 0], - "A2_frame": [0, 0, 0], - "A3_frame": [0, 0, 0], - "A4_frame": [0, 0, 0], - "A5_frame": [0, 0, 0], - "D_frame": [0, 0, 0], - "D_tri_frame": [0, 0, 0], - "width_gauss": 0.5, - "T_relax_1": 0.0, - "T_relax_2": 0.0, - "T_pm": 0.0, - "population": [0.0, 0.0, 0.0], - "freq_mw": 0.0, - "amplitude": 0, - } - } - - return default_profile
- - - -
-[docs] -def new_save_profile() -> dict: - """ - Get a default save profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for saving. - - """ - default_profile = {"main": {}} - # TODO Profil verwenden um Projektordner festzulegen -> Speicherort für - # Bilder, Simulations/Optimierungsergebnisse, out-Files etc. - - return default_profile
- - - -
-[docs] -def new_optimization_profile() -> dict: - """ - Get a default optimization profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for optimization routines. - - """ - default_profile = { - "main": {"routine": "", "method": None, "cpu_cores": 0}, - "genetic": { - "GAVaPS": True, - "representation": "", - "lifetime_mode": "", - "crossover_type": "", - "mutation_type": "", - "min_lifetime": 0, - "max_lifetime": 10, - "reproduction_ratio": 0.2, - "p_c": 0.5, - "p_m": 0.001, - "pop_size": 500, - "min_pop_size": 1, - "max_pop_size": 100, - "convergence": 0.02, - "peak_prominence": 0.5, - "error_weight": [1.0, 0.2], - "max_generation": 1000, - "show_status": True, - }, - "minimize": {"maxiter": ""}, - "dual_annealing": { - "maxiter": None, - "maxiter_minimizer": None, - "initial_temp": None, - "restart_temp_ratio": None, - "visit": None, - "accept": None, - "maxfun": None, - "no_local_search": None, - }, - "shgo": { - "maxiter": None, - "maxiter_minimizer": None, - "n": None, - "iters": None, - "maxfev": None, - "ftol": None, - "maxev": None, - "maxtime": None, - "minimize_every_iter": None, - "local_iter": None, - "sampling_method": None, - }, - "differential_evolution": { - "strategy": None, - "maxiter": None, - "popsize": None, - "tol": None, - "mutation": None, - "recombination": None, - "seed": None, - "atol": None, - "polish": None, - "init": None, - "disp": None, - "updating": None, - }, - "basinhopping": { - "T": None, - "stepsize": None, - "disp": None, - "stepwise_factor": None, - "maxiter_minimizer": None, - "seed": None, - "interval": None, - "niter_success": None, - "niter": None, - "target_accept_rate": None, - }, - "least_squares": { - "ftol": None, - "xtol": None, - "gtol": None, - "loss": None, - "f_scale": None, - "max_nfev": None, - "tr_solver": None, - "verbose": None, - }, - } - - return default_profile
- - - -
-[docs] -def new_simulation_profile() -> dict: - """ - Get an empty simulation profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for simulation profiles. - - """ - default_profile = { - "main": {"routine": "", "cpu_cores": 0}, - "static_radpair": {"grid_points": 500}, - "teacups": { - "grid_points": 500, - "space": "hilbert", - }, - "opossum": {}, - "didelphis_tikhonov": { - "min_r": 10, - "max_r": 50, - "r_points": 401, - "force_cpu": False, - "regularization_mode": 2, - }, - "didelphis": {"min_r": 10, "max_r": 50, "r_points": 401}, - } - - return default_profile
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_modules/spinanalysis/saving.html b/docs/build/html/_modules/spinanalysis/saving.html deleted file mode 100644 index da141e6..0000000 --- a/docs/build/html/_modules/spinanalysis/saving.html +++ /dev/null @@ -1,419 +0,0 @@ - - - - - - - - spinanalysis.saving — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for spinanalysis.saving

-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-© M. Sc. Florian Quintes, 2021-2022
-
-@contact: florian.quintes@pc.uni.freiburg.de
-
-@author: Florian Quintes
-"""
-
-import numpy as np
-import os
-
-
-
-[docs] -def save_plot(fname: str, *figures: object, path: str = None, **kwargs: dict) -> None: - """ - Save the figures plotted with matplotlib. - - Parameters - ---------- - fname : str - Filename for the figure(s). If multiple figures are given, '_[number]' - will be append to the filename. - *figures : object - Matplotlib figure object(s). - path : str, optional - Absolute path where the figures will be stored. The default is - '~/Results/Plots/'. - **kwargs : dict - Other keyword arguments. Will be passed to plt.savefig(). See - matplotlib documentation for further informations. - - Returns - ------- - None - Nothing will be returned. - - """ - # [SETUP] - if fname[-4] == ".": - fmt = fname[-4:] - fname = fname[:-4] - elif fname[-5] == ".": - fmt = fname[-5:] - fname = fname[:-5] - else: - fmt = "" - - if type(figures[0]) == list: - figures = tuple(*figures) - - if len(figures) == 1: - multi_plot = False - else: - multi_plot = True - - if path is None: - save_path = os.path.join(os.path.expanduser("~"), "Results", "Plots") - else: - save_path = path - - if not os.path.exists(save_path): - os.makedirs(save_path) - - # [SAVE] - for n, fig in enumerate(figures): - if multi_plot: - save_name = os.path.join(save_path, fname + "_" + str(n + 1) + fmt) - else: - save_name = os.path.join(save_path, fname + fmt) - - if "format" in kwargs: - save_name += "." + kwargs["format"] - - fig.savefig(save_name, **kwargs) - - return None
- - - -
-[docs] -def save_simulation(name: str, *data: "np.array", path: str = None) -> None: - """ - Save the simulated data at '[path]/[name]/[files]' using np.savetxt. - - Parameters - ---------- - name : str - Foldername for the dataset. - *data : np.array - Arrays with the simulated data. Must be 2d or 3d. If 2d: x_axis, int; - if 3d: x_axis, y_axis, int. - path : str, optional - Full path where the data will be stored. The default is - '~/Results/Simulated Data/[name]'. - - Raises - ------ - ValueError - Will be raised, if dimension of the data isn't 2d or 3d. - - Returns - ------- - None - Nothing will be returned. - - """ - # [SETUP] - if name.lower().endswith(".txt"): - name = name[:-4] - if path is None: - save_path = os.path.join(os.path.expanduser("~"), "Results", "Simulated Data") - else: - save_path = path - - if not os.path.exists(save_path): - os.makedirs(save_path) - - save_folder = os.path.join(save_path, name) - if not os.path.exists(save_folder): - os.makedirs(save_folder) - - if len(data) == 2: - x, intensity = data - elif len(data) == 3: - x, y, intensity = data - else: - raise ValueError("Can't handle {0}D Data. Need 2D or 3D.".format(len(data))) - - # [SAVE] - - np.savetxt(os.path.join(save_folder, "x_axis.txt"), x) - np.savetxt(os.path.join(save_folder, "intensity.txt"), intensity) - - if len(data) == 3: - np.savetxt(os.path.join(save_folder, "y_axis.txt"), y) - - return None
- - - -
-[docs] -def write_out_file( - Sys: object, - Exp: object, - SimOpt: object, - *FitOpt: object, - current_best: bool = False, -) -> None: - """ - Write an output file with all datas from Sys, Exp, SimOpt and, if running - in optimization mode, FitOpt. - - Parameters - ---------- - Sys : object - Spinsystem object of module 'epr_setup'. - Exp : object - Experimental object of module 'epr_setup'. - SimOpt : object - SimulationOptions object of module 'epr_setup'. - *FitOpt : object - FittingOptions object of module 'epr_setup'. - current_best : bool, optional - True if the given Sys and Exp are the current best while running in - optimization mode. False if Sys and Exp are the final result / are the - given Sys and Exp in normal simulation mode. The default is False. - - Returns - ------- - None - Nothing will be returned. - - """ - - if current_best: - title_part = "_current_best" - else: - title_part = "_result" - - # TODO Pfad überlegen um Ergebnisse zu speichern? - if len(FitOpt) == 1: - fit_mode = True - out = open(FitOpt[0].routine + title_part + ".eps_out", "w") - else: - fit_mode = False - out = open(SimOpt.routine + title_part + ".eps_out", "w") - - out.write("{:#^50s}\n".format("")) - - if current_best: - out.write("{:#^50s}\n".format(" CURRENT BEST ")) - else: - out.write("{:#^50s}\n".format(" OUTPUT-FILE ")) - - if fit_mode: - out.write("{:#^50s}\n".format(" " + FitOpt[0].routine + " ")) - else: - out.write("{:#^50s}\n".format(" " + SimOpt.routine + " ")) - - out.write("{:#^50s}\n".format(" AUTHOR: FLORIAN QUINTES ")) - out.write("{:#^50s}\n".format("")) - out.write("\n") - - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" SPINSYSTEM ")) - out.write("\n") - out.write("\n") - - out.write("{: ^15s} {: ^20s}\n".format("g-Tensor", "Value")) - np.set_printoptions(formatter={"float": "{:0.7f}".format}) - - for key in vars(Sys): - if key in ("g1", "g2"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) - - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("A-Tensor", "Value / MHz")) - np.set_printoptions(formatter={"float": "{:2.2f}".format}) - - for key in vars(Sys): - if key.startswith("A") and len(key) < 3: - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) - - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("ZFS", "Value / MHz")) - - for key in vars(Sys): - if key in ("D", "E", "J_ex"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) - - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("Orientations", "Angle / rad")) - np.set_printoptions(formatter={"float": "{: 0.1f}".format}) - - for key in vars(Sys): - if key.endswith("_frame"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) - - out.write("\n") - out.write("{: ^15s}|{: ^20s}\n".format("Nuclear spin", "Number of cores")) - - spins = [] - cores = [] - for key in vars(Sys): - if (key.startswith("n") or key.startswith("I")) and len(key) < 3: - if key.startswith("n"): - cores.append(key) - else: - spins.append(key) - cores.sort() - spins.sort() - - atoms = zip(cores, spins) - for core, spin in atoms: - out.write( - "{0:>7s}: {1:<6.1f}|{2:>9s}: {3:<9}\n".format( - core, vars(Sys)[core], spin, vars(Sys)[spin] - ) - ) - - out.write("\n") - out.write("{:>14s}: {:.2f} mT\n".format("width_gauss", Sys.width_gauss)) - - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" EXPERIMENTAL ")) - out.write("\n") - out.write("\n") - - out.write("{0:>20s}: {1:<9.6f} mT\n".format("microwave amplitude", Exp.B_mw)) - out.write( - "{0:>20s}: {1:<9.6f} GHz\n".format("microwave frequency", Exp.freq_mw / 1e9) - ) - out.write("{0:>20s}: {1:<7.2f} mT\n".format("min B_z", Exp.B_z.min())) - out.write("{0:>20s}: {1:<7.2f} mT\n".format("max B_z", Exp.B_z.max())) - out.write("{0:>20s}: {1}\n".format("B_z points", len(Exp.B_z))) - - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" SIMULATION OPTIONS ")) - out.write("\n") - out.write("\n") - - out.write("{0:>20s}: {1:<20s}\n".format("routine", SimOpt.routine)) - out.write("{0:>20s}: {1} cores\n".format("run at", SimOpt.cpu_cores)) - out.write("{0:>20s}: {1:<20}\n".format("grid points", SimOpt.grid_points)) - out.write("{0:>20s}: {1} space\n".format("using", SimOpt.space)) - - if fit_mode: - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" FITTING OPTIONS ")) - out.write("\n") - out.write("\n") - - for key in vars(FitOpt[0]): - if key is not None: - out.write( - ("{0:>20s}: " + "{1:<20s}\n").format(key, str(vars(FitOpt[0])[key])) - ) - - out.close() - - return None
- -
- -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/_sources/api.rst.txt b/docs/build/html/_sources/api.rst.txt deleted file mode 100644 index 98cbca3..0000000 --- a/docs/build/html/_sources/api.rst.txt +++ /dev/null @@ -1,72 +0,0 @@ -API Reference -============= - -The API reference is generated automatically from the package source. - -Core ----- - -.. automodule:: spinanalysis.core - :members: - :show-inheritance: - -EPR ---- - -.. automodule:: spinanalysis.epr - :members: - :show-inheritance: - -Loading -------- - -.. automodule:: spinanalysis.loading - :members: - :show-inheritance: - -Processing ----------- - -.. automodule:: spinanalysis.processing - :members: - :show-inheritance: - -Plotting --------- - -.. automodule:: spinanalysis.plotting - :members: - :show-inheritance: - -Saving ------- - -.. automodule:: spinanalysis.saving - :members: - :show-inheritance: - -Profiles --------- - -.. automodule:: spinanalysis.profiles - :members: - :show-inheritance: - -Internal Modules ----------------- - -The following modules support internal execution and integration workflows. - -Wrappers -~~~~~~~~ - -.. automodule:: spinanalysis._wrappers - :members: - :show-inheritance: - -Interface Handler -~~~~~~~~~~~~~~~~~ - -.. automodule:: spinanalysis._interface_handler - :members: - :show-inheritance: diff --git a/docs/build/html/_sources/contributing.rst.txt b/docs/build/html/_sources/contributing.rst.txt deleted file mode 100644 index f589ea3..0000000 --- a/docs/build/html/_sources/contributing.rst.txt +++ /dev/null @@ -1,102 +0,0 @@ -Contributing -============ - -Thank you for contributing to ``spinanalysis``. The project is a Python -framework for loading, processing, plotting, simulating, optimizing, and -saving electron paramagnetic resonance (EPR) spectra. - -Project Structure ------------------ - -The main package modules are organized by workflow: - -* ``core.py`` orchestrates simulation and optimization. -* ``epr.py`` defines EPR parameter and workflow objects. -* ``loading.py`` reads EPR data files. -* ``processing.py`` transforms and reconstructs spectra. -* ``plotting.py`` renders spectra and figures. -* ``saving.py`` writes figures, simulated data, and output files. -* ``profiles.py`` manages configuration and profile files. -* ``_wrappers.py`` provides internal timing and multiprocessing wrappers. -* ``_interface_handler.py`` provides internal simulation and optimization - interfaces. - -Profile templates and plotting styles are package data under -``src/spinanalysis/data/profiles``. Changes to a profile schema should update -the corresponding ``configspec.ini`` and relevant fixtures under -``tests/data``. - -Development Setup ------------------ - -The project requires Python 3.13 or newer. The repository uses ``uv`` for -dependency and environment management. - -Clone the repository and change into its directory: - -.. code-block:: console - - git clone https://github.com/florianquintes/spinanalysis.git - cd spinanalysis - -Install the locked development environment from the repository root: - -.. code-block:: console - - uv sync --dev - -The package uses a ``src`` layout. Application code belongs under -``src/spinanalysis`` and tests belong under ``tests``. - -Branches --------- - -Create a focused branch for each change and keep unrelated changes separate. -The repository does not currently document a required branch naming scheme. - -Pull Requests -------------- - -Pull requests should explain the change and include the relevant validation -results. Before opening a pull request, run the checks that apply to the -change: - -.. code-block:: console - - uv run pytest - uv run ruff check . - uv build - -If documentation is changed, also build the documentation as described in the -Documentation section. Mention known baseline failures or warnings rather -than presenting them as regressions. - -Code Style ----------- - -Follow the existing Python and reStructuredText style in the surrounding -files. Keep importable Python modules under ``src/spinanalysis`` and add tests -under ``tests``. The repository has no configured formatter or type checker. - -Ruff is the configured code-quality tool, using its default rules: - -.. code-block:: console - - uv run ruff check . - -Documentation -------------- - -Documentation source files are under ``docs/source``. The Sphinx configuration -adds ``src`` to the module search path and reads the package version from -``pyproject.toml``. - -Build the documentation from the ``docs`` directory: - -.. code-block:: console - - uv run sphinx-build -M html source build - -The generated HTML is written to ``docs/build/html``. Add a title to every -document included by the root ``index.rst`` so Sphinx can include it in the -table of contents. diff --git a/docs/build/html/_sources/development.rst.txt b/docs/build/html/_sources/development.rst.txt deleted file mode 100644 index 7d06f4d..0000000 --- a/docs/build/html/_sources/development.rst.txt +++ /dev/null @@ -1,44 +0,0 @@ -Development -=========== - -Development Setup ------------------ - -The project requires Python 3.13 or newer and uses ``uv`` for dependency and -environment management. From the repository root, install the development -environment with: - -.. code-block:: console - - uv sync --dev - -Running Tests -------------- - -Run the test suite from the repository root: - -.. code-block:: console - - uv run pytest - -Code Formatting and Linting ---------------------------- - -Check the code with Ruff: - -.. code-block:: console - - uv run ruff check . - -The repository does not currently configure a separate formatter. - -Build the Documentation ------------------------ - -Build the HTML documentation from the ``docs`` directory: - -.. code-block:: console - - uv run sphinx-build -M html source build - -The generated documentation is written to ``docs/build/html``. diff --git a/docs/build/html/_sources/examples.rst.txt b/docs/build/html/_sources/examples.rst.txt deleted file mode 100644 index e69de29..0000000 diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt deleted file mode 100644 index ef48670..0000000 --- a/docs/build/html/_sources/index.rst.txt +++ /dev/null @@ -1,33 +0,0 @@ -.. spinanalysis documentation master file, created by - sphinx-quickstart on Thu Jul 23 10:51:19 2026. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -spinanalysis -============ - -``spinanalysis`` is a high-level Python framework for the analysis of -electron paramagnetic resonance (EPR) spectra. - -The framework provides modules for loading, processing, plotting, simulating, -optimizing, and saving EPR spectra. It is designed to support complete -analysis workflows from experimental data to simulated and fitted spectra. - -.. important:: - - The simulation routines for spin-correlated, singlet-born radical pairs, - out-of-phase ESEEM signals, and transiently coupled systems are currently - provided through external dependencies. - -.. toctree:: - :maxdepth: 2 - :caption: Contents - - introduction - installation - examples - api - development - contributing - license - diff --git a/docs/build/html/_sources/installation.rst.txt b/docs/build/html/_sources/installation.rst.txt deleted file mode 100644 index 4430904..0000000 --- a/docs/build/html/_sources/installation.rst.txt +++ /dev/null @@ -1,27 +0,0 @@ -Installation -============ - -Requirements ------------- - -``spinanalysis`` requires Python 3.13 or newer. ``uv`` or ``pip`` is required -to install the package. - -Installation Commands ---------------------- - -The following commands cover the common installation use cases: - -.. list-table:: - :header-rows: 1 - :widths: 20 40 40 - - * - - - uv - - pip - * - User - - ``uv add spinanalysis`` - - ``pip install spinanalysis`` - * - Developer - - ``uv sync --dev`` - - ``pip install -e .`` diff --git a/docs/build/html/_sources/introduction.rst.txt b/docs/build/html/_sources/introduction.rst.txt deleted file mode 100644 index 5eb4edb..0000000 --- a/docs/build/html/_sources/introduction.rst.txt +++ /dev/null @@ -1,52 +0,0 @@ -Introduction -============ - -``spinanalysis`` is a Python framework for the analysis of electron -paramagnetic resonance (EPR) spectra. It is designed to support a complete -workflow, from loading experimental data and applying common processing steps -to visualizing, simulating, optimizing, and saving results. - -The package supports common EPR data formats, including Bruker BES3T, -ESP380E, MATLAB, simulated, and plain-text data. Processing tools cover tasks -such as normalization, offset reduction, background correction, and -reconstruction of out-of-phase ESEEM spectra. Matplotlib-based plotting -functions provide consistent two-dimensional and three-dimensional figures -using configurable plot styles. - -Simulation and optimization workflows are organized around EPR parameter -objects and profile-based configuration. The profile system stores reusable -settings for spin systems, simulation and optimization routines, plotting, -and saving. Some specialized simulation routines are provided through -external dependencies and may require additional installation steps. - -Module overview ---------------- - -The package is organized into modules that support different parts of an EPR -analysis workflow: - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - Module - - Description - * - ``core`` - - Orchestrates simulation and optimization workflows. - * - ``epr`` - - Defines EPR parameter, experimental, simulation, and optimization - objects. - * - ``loading`` - - Loads EPR data from Bruker, ESP380E, MATLAB, simulated, and text files. - * - ``processing`` - - Normalizes, corrects, and reconstructs spectra. - * - ``plotting`` - - Creates two-dimensional and three-dimensional spectrum plots. - * - ``saving`` - - Saves figures, simulated data, and simulation output files. - * - ``profiles`` - - Creates, loads, saves, imports, and exports configuration profiles. - * - ``_wrappers`` - - Provides internal timing and multiprocessing wrappers. - * - ``_interface_handler`` - - Provides internal interfaces for simulation and optimization routines. diff --git a/docs/build/html/_sources/license.rst.txt b/docs/build/html/_sources/license.rst.txt deleted file mode 100644 index 6d0ab03..0000000 --- a/docs/build/html/_sources/license.rst.txt +++ /dev/null @@ -1,25 +0,0 @@ -License -======= - -``spinanalysis`` is licensed under the GNU General Public License v3.0 (GPLv3). - -.. important:: - - The GPLv3 allows commercial use. It does not prohibit others from selling - software based on ``spinanalysis``. - -In summary, the GPLv3: - -* allows use, modification, and redistribution; -* requires modified or derived distributed software to remain licensed under - the GPLv3; -* requires the corresponding source code to be made available when distributing - binaries; -* permits commercial use, including selling GPLv3-licensed software; -* provides no warranty. - -The complete license text is available in the -`LICENSE `_ file. - -For more information, see the -`official GPLv3 license text `_. \ No newline at end of file diff --git a/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js b/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 8141580..0000000 --- a/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,123 +0,0 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/build/html/_static/base-stemmer.js b/docs/build/html/_static/base-stemmer.js deleted file mode 100644 index e6fa0c4..0000000 --- a/docs/build/html/_static/base-stemmer.js +++ /dev/null @@ -1,476 +0,0 @@ -// @ts-check - -/**@constructor*/ -BaseStemmer = function() { - /** @protected */ - this.current = ''; - this.cursor = 0; - this.limit = 0; - this.limit_backward = 0; - this.bra = 0; - this.ket = 0; - - /** - * @param {string} value - */ - this.setCurrent = function(value) { - this.current = value; - this.cursor = 0; - this.limit = this.current.length; - this.limit_backward = 0; - this.bra = this.cursor; - this.ket = this.limit; - }; - - /** - * @return {string} - */ - this.getCurrent = function() { - return this.current; - }; - - /** - * @param {BaseStemmer} other - */ - this.copy_from = function(other) { - /** @protected */ - this.current = other.current; - this.cursor = other.cursor; - this.limit = other.limit; - this.limit_backward = other.limit_backward; - this.bra = other.bra; - this.ket = other.ket; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.in_grouping = function(s, min, max) { - /** @protected */ - if (this.cursor >= this.limit) return false; - var ch = this.current.charCodeAt(this.cursor); - if (ch > max || ch < min) return false; - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; - this.cursor++; - return true; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.go_in_grouping = function(s, min, max) { - /** @protected */ - while (this.cursor < this.limit) { - var ch = this.current.charCodeAt(this.cursor); - if (ch > max || ch < min) - return true; - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) - return true; - this.cursor++; - } - return false; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.in_grouping_b = function(s, min, max) { - /** @protected */ - if (this.cursor <= this.limit_backward) return false; - var ch = this.current.charCodeAt(this.cursor - 1); - if (ch > max || ch < min) return false; - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; - this.cursor--; - return true; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.go_in_grouping_b = function(s, min, max) { - /** @protected */ - while (this.cursor > this.limit_backward) { - var ch = this.current.charCodeAt(this.cursor - 1); - if (ch > max || ch < min) return true; - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true; - this.cursor--; - } - return false; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.out_grouping = function(s, min, max) { - /** @protected */ - if (this.cursor >= this.limit) return false; - var ch = this.current.charCodeAt(this.cursor); - if (ch > max || ch < min) { - this.cursor++; - return true; - } - ch -= min; - if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) { - this.cursor++; - return true; - } - return false; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.go_out_grouping = function(s, min, max) { - /** @protected */ - while (this.cursor < this.limit) { - var ch = this.current.charCodeAt(this.cursor); - if (ch <= max && ch >= min) { - ch -= min; - if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) { - return true; - } - } - this.cursor++; - } - return false; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.out_grouping_b = function(s, min, max) { - /** @protected */ - if (this.cursor <= this.limit_backward) return false; - var ch = this.current.charCodeAt(this.cursor - 1); - if (ch > max || ch < min) { - this.cursor--; - return true; - } - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) { - this.cursor--; - return true; - } - return false; - }; - - /** - * @param {number[]} s - * @param {number} min - * @param {number} max - * @return {boolean} - */ - this.go_out_grouping_b = function(s, min, max) { - /** @protected */ - while (this.cursor > this.limit_backward) { - var ch = this.current.charCodeAt(this.cursor - 1); - if (ch <= max && ch >= min) { - ch -= min; - if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) { - return true; - } - } - this.cursor--; - } - return false; - }; - - /** - * @param {string} s - * @return {boolean} - */ - this.eq_s = function(s) - { - /** @protected */ - if (this.limit - this.cursor < s.length) return false; - if (this.current.slice(this.cursor, this.cursor + s.length) != s) - { - return false; - } - this.cursor += s.length; - return true; - }; - - /** - * @param {string} s - * @return {boolean} - */ - this.eq_s_b = function(s) - { - /** @protected */ - if (this.cursor - this.limit_backward < s.length) return false; - if (this.current.slice(this.cursor - s.length, this.cursor) != s) - { - return false; - } - this.cursor -= s.length; - return true; - }; - - /** - * @param {Among[]} v - * @return {number} - */ - this.find_among = function(v) - { - /** @protected */ - var i = 0; - var j = v.length; - - var c = this.cursor; - var l = this.limit; - - var common_i = 0; - var common_j = 0; - - var first_key_inspected = false; - - while (true) - { - var k = i + ((j - i) >>> 1); - var diff = 0; - var common = common_i < common_j ? common_i : common_j; // smaller - // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional) - var w = v[k]; - var i2; - for (i2 = common; i2 < w[0].length; i2++) - { - if (c + common == l) - { - diff = -1; - break; - } - diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2); - if (diff != 0) break; - common++; - } - if (diff < 0) - { - j = k; - common_j = common; - } - else - { - i = k; - common_i = common; - } - if (j - i <= 1) - { - if (i > 0) break; // v->s has been inspected - if (j == i) break; // only one item in v - - // - but now we need to go round once more to get - // v->s inspected. This looks messy, but is actually - // the optimal approach. - - if (first_key_inspected) break; - first_key_inspected = true; - } - } - do { - var w = v[i]; - if (common_i >= w[0].length) - { - this.cursor = c + w[0].length; - if (w.length < 4) return w[2]; - var res = w[3](this); - this.cursor = c + w[0].length; - if (res) return w[2]; - } - i = w[1]; - } while (i >= 0); - return 0; - }; - - // find_among_b is for backwards processing. Same comments apply - /** - * @param {Among[]} v - * @return {number} - */ - this.find_among_b = function(v) - { - /** @protected */ - var i = 0; - var j = v.length - - var c = this.cursor; - var lb = this.limit_backward; - - var common_i = 0; - var common_j = 0; - - var first_key_inspected = false; - - while (true) - { - var k = i + ((j - i) >> 1); - var diff = 0; - var common = common_i < common_j ? common_i : common_j; - var w = v[k]; - var i2; - for (i2 = w[0].length - 1 - common; i2 >= 0; i2--) - { - if (c - common == lb) - { - diff = -1; - break; - } - diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2); - if (diff != 0) break; - common++; - } - if (diff < 0) - { - j = k; - common_j = common; - } - else - { - i = k; - common_i = common; - } - if (j - i <= 1) - { - if (i > 0) break; - if (j == i) break; - if (first_key_inspected) break; - first_key_inspected = true; - } - } - do { - var w = v[i]; - if (common_i >= w[0].length) - { - this.cursor = c - w[0].length; - if (w.length < 4) return w[2]; - var res = w[3](this); - this.cursor = c - w[0].length; - if (res) return w[2]; - } - i = w[1]; - } while (i >= 0); - return 0; - }; - - /* to replace chars between c_bra and c_ket in this.current by the - * chars in s. - */ - /** - * @param {number} c_bra - * @param {number} c_ket - * @param {string} s - * @return {number} - */ - this.replace_s = function(c_bra, c_ket, s) - { - /** @protected */ - var adjustment = s.length - (c_ket - c_bra); - this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket); - this.limit += adjustment; - if (this.cursor >= c_ket) this.cursor += adjustment; - else if (this.cursor > c_bra) this.cursor = c_bra; - return adjustment; - }; - - /** - * @return {boolean} - */ - this.slice_check = function() - { - /** @protected */ - if (this.bra < 0 || - this.bra > this.ket || - this.ket > this.limit || - this.limit > this.current.length) - { - return false; - } - return true; - }; - - /** - * @param {number} c_bra - * @return {boolean} - */ - this.slice_from = function(s) - { - /** @protected */ - var result = false; - if (this.slice_check()) - { - this.replace_s(this.bra, this.ket, s); - result = true; - } - return result; - }; - - /** - * @return {boolean} - */ - this.slice_del = function() - { - /** @protected */ - return this.slice_from(""); - }; - - /** - * @param {number} c_bra - * @param {number} c_ket - * @param {string} s - */ - this.insert = function(c_bra, c_ket, s) - { - /** @protected */ - var adjustment = this.replace_s(c_bra, c_ket, s); - if (c_bra <= this.bra) this.bra += adjustment; - if (c_bra <= this.ket) this.ket += adjustment; - }; - - /** - * @return {string} - */ - this.slice_to = function() - { - /** @protected */ - var result = ''; - if (this.slice_check()) - { - result = this.current.slice(this.bra, this.ket); - } - return result; - }; - - /** - * @return {string} - */ - this.assign_to = function() - { - /** @protected */ - return this.current.slice(0, this.limit); - }; -}; diff --git a/docs/build/html/_static/basic.css b/docs/build/html/_static/basic.css deleted file mode 100644 index 4738b2e..0000000 --- a/docs/build/html/_static/basic.css +++ /dev/null @@ -1,906 +0,0 @@ -/* - * Sphinx stylesheet -- basic theme. - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin-top: 10px; -} - -ul.search li { - padding: 5px 0; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a:visited { - color: #551A8B; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} - -nav.contents, -aside.topic, -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ - -nav.contents, -aside.topic, -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -nav.contents::after, -aside.topic::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -aside.footnote > span, -div.citation > span { - float: left; -} -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { - margin-bottom: 0em; -} -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -.sig dd { - margin-top: 0px; - margin-bottom: 0px; -} - -.sig dl { - margin-top: 0px; - margin-bottom: 0px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/build/html/_static/check-solid.svg b/docs/build/html/_static/check-solid.svg deleted file mode 100644 index 92fad4b..0000000 --- a/docs/build/html/_static/check-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/build/html/_static/clipboard.min.js b/docs/build/html/_static/clipboard.min.js deleted file mode 100644 index 54b3c46..0000000 --- a/docs/build/html/_static/clipboard.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * clipboard.js v2.0.8 - * https://clipboardjs.com/ - * - * Licensed MIT © Zeno Rocha - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 - - - - diff --git a/docs/build/html/_static/copybutton.css b/docs/build/html/_static/copybutton.css deleted file mode 100644 index f1916ec..0000000 --- a/docs/build/html/_static/copybutton.css +++ /dev/null @@ -1,94 +0,0 @@ -/* Copy buttons */ -button.copybtn { - position: absolute; - display: flex; - top: .3em; - right: .3em; - width: 1.7em; - height: 1.7em; - opacity: 0; - transition: opacity 0.3s, border .3s, background-color .3s; - user-select: none; - padding: 0; - border: none; - outline: none; - border-radius: 0.4em; - /* The colors that GitHub uses */ - border: #1b1f2426 1px solid; - background-color: #f6f8fa; - color: #57606a; -} - -button.copybtn.success { - border-color: #22863a; - color: #22863a; -} - -button.copybtn svg { - stroke: currentColor; - width: 1.5em; - height: 1.5em; - padding: 0.1em; -} - -div.highlight { - position: relative; -} - -/* Show the copybutton */ -.highlight:hover button.copybtn, button.copybtn.success { - opacity: 1; -} - -.highlight button.copybtn:hover { - background-color: rgb(235, 235, 235); -} - -.highlight button.copybtn:active { - background-color: rgb(187, 187, 187); -} - -/** - * A minimal CSS-only tooltip copied from: - * https://codepen.io/mildrenben/pen/rVBrpK - * - * To use, write HTML like the following: - * - *

Short

- */ - .o-tooltip--left { - position: relative; - } - - .o-tooltip--left:after { - opacity: 0; - visibility: hidden; - position: absolute; - content: attr(data-tooltip); - padding: .2em; - font-size: .8em; - left: -.2em; - background: grey; - color: white; - white-space: nowrap; - z-index: 2; - border-radius: 2px; - transform: translateX(-102%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); -} - -.o-tooltip--left:hover:after { - display: block; - opacity: 1; - visibility: visible; - transform: translateX(-100%) translateY(0); - transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); - transition-delay: .5s; -} - -/* By default the copy button shouldn't show up when printing a page */ -@media print { - button.copybtn { - display: none; - } -} diff --git a/docs/build/html/_static/copybutton.js b/docs/build/html/_static/copybutton.js deleted file mode 100644 index 2ea7ff3..0000000 --- a/docs/build/html/_static/copybutton.js +++ /dev/null @@ -1,248 +0,0 @@ -// Localization support -const messages = { - 'en': { - 'copy': 'Copy', - 'copy_to_clipboard': 'Copy to clipboard', - 'copy_success': 'Copied!', - 'copy_failure': 'Failed to copy', - }, - 'es' : { - 'copy': 'Copiar', - 'copy_to_clipboard': 'Copiar al portapapeles', - 'copy_success': '¡Copiado!', - 'copy_failure': 'Error al copiar', - }, - 'de' : { - 'copy': 'Kopieren', - 'copy_to_clipboard': 'In die Zwischenablage kopieren', - 'copy_success': 'Kopiert!', - 'copy_failure': 'Fehler beim Kopieren', - }, - 'fr' : { - 'copy': 'Copier', - 'copy_to_clipboard': 'Copier dans le presse-papier', - 'copy_success': 'Copié !', - 'copy_failure': 'Échec de la copie', - }, - 'ru': { - 'copy': 'Скопировать', - 'copy_to_clipboard': 'Скопировать в буфер', - 'copy_success': 'Скопировано!', - 'copy_failure': 'Не удалось скопировать', - }, - 'zh-CN': { - 'copy': '复制', - 'copy_to_clipboard': '复制到剪贴板', - 'copy_success': '复制成功!', - 'copy_failure': '复制失败', - }, - 'it' : { - 'copy': 'Copiare', - 'copy_to_clipboard': 'Copiato negli appunti', - 'copy_success': 'Copiato!', - 'copy_failure': 'Errore durante la copia', - } -} - -let locale = 'en' -if( document.documentElement.lang !== undefined - && messages[document.documentElement.lang] !== undefined ) { - locale = document.documentElement.lang -} - -let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; -if (doc_url_root == '#') { - doc_url_root = ''; -} - -/** - * SVG files for our copy buttons - */ -let iconCheck = ` - ${messages[locale]['copy_success']} - - -` - -// If the user specified their own SVG use that, otherwise use the default -let iconCopy = ``; -if (!iconCopy) { - iconCopy = ` - ${messages[locale]['copy_to_clipboard']} - - - -` -} - -/** - * Set up copy/paste for code blocks - */ - -const runWhenDOMLoaded = cb => { - if (document.readyState != 'loading') { - cb() - } else if (document.addEventListener) { - document.addEventListener('DOMContentLoaded', cb) - } else { - document.attachEvent('onreadystatechange', function() { - if (document.readyState == 'complete') cb() - }) - } -} - -const codeCellId = index => `codecell${index}` - -// Clears selected text since ClipboardJS will select the text when copying -const clearSelection = () => { - if (window.getSelection) { - window.getSelection().removeAllRanges() - } else if (document.selection) { - document.selection.empty() - } -} - -// Changes tooltip text for a moment, then changes it back -// We want the timeout of our `success` class to be a bit shorter than the -// tooltip and icon change, so that we can hide the icon before changing back. -var timeoutIcon = 2000; -var timeoutSuccessClass = 1500; - -const temporarilyChangeTooltip = (el, oldText, newText) => { - el.setAttribute('data-tooltip', newText) - el.classList.add('success') - // Remove success a little bit sooner than we change the tooltip - // So that we can use CSS to hide the copybutton first - setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) - setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) -} - -// Changes the copy button icon for two seconds, then changes it back -const temporarilyChangeIcon = (el) => { - el.innerHTML = iconCheck; - setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) -} - -const addCopyButtonToCodeCells = () => { - // If ClipboardJS hasn't loaded, wait a bit and try again. This - // happens because we load ClipboardJS asynchronously. - if (window.ClipboardJS === undefined) { - setTimeout(addCopyButtonToCodeCells, 250) - return - } - - // Add copybuttons to all of our code cells - const COPYBUTTON_SELECTOR = 'div.highlight pre'; - const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) - codeCells.forEach((codeCell, index) => { - const id = codeCellId(index) - codeCell.setAttribute('id', id) - - const clipboardButton = id => - `` - codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) - }) - -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/** - * Removes excluded text from a Node. - * - * @param {Node} target Node to filter. - * @param {string} exclude CSS selector of nodes to exclude. - * @returns {DOMString} Text from `target` with text removed. - */ -function filterText(target, exclude) { - const clone = target.cloneNode(true); // clone as to not modify the live DOM - if (exclude) { - // remove excluded nodes - clone.querySelectorAll(exclude).forEach(node => node.remove()); - } - return clone.innerText; -} - -// Callback when a copy button is clicked. Will be passed the node that was clicked -// should then grab the text and replace pieces of text that shouldn't be used in output -function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { - var regexp; - var match; - - // Do we check for line continuation characters and "HERE-documents"? - var useLineCont = !!lineContinuationChar - var useHereDoc = !!hereDocDelim - - // create regexp to capture prompt and remaining line - if (isRegexp) { - regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') - } else { - regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') - } - - const outputLines = []; - var promptFound = false; - var gotLineCont = false; - var gotHereDoc = false; - const lineGotPrompt = []; - for (const line of textContent.split('\n')) { - match = line.match(regexp) - if (match || gotLineCont || gotHereDoc) { - promptFound = regexp.test(line) - lineGotPrompt.push(promptFound) - if (removePrompts && promptFound) { - outputLines.push(match[2]) - } else { - outputLines.push(line) - } - gotLineCont = line.endsWith(lineContinuationChar) & useLineCont - if (line.includes(hereDocDelim) & useHereDoc) - gotHereDoc = !gotHereDoc - } else if (!onlyCopyPromptLines) { - outputLines.push(line) - } else if (copyEmptyLines && line.trim() === '') { - outputLines.push(line) - } - } - - // If no lines with the prompt were found then just use original lines - if (lineGotPrompt.some(v => v === true)) { - textContent = outputLines.join('\n'); - } - - // Remove a trailing newline to avoid auto-running when pasting - if (textContent.endsWith("\n")) { - textContent = textContent.slice(0, -1) - } - return textContent -} - - -var copyTargetText = (trigger) => { - var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); - - // get filtered text - let exclude = '.linenos'; - - let text = filterText(target, exclude); - return formatCopyText(text, '', false, true, true, true, '', '') -} - - // Initialize with a callback so we can modify the text before copy - const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) - - // Update UI with error/success messages - clipboard.on('success', event => { - clearSelection() - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) - temporarilyChangeIcon(event.trigger) - }) - - clipboard.on('error', event => { - temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) - }) -} - -runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/docs/build/html/_static/copybutton_funcs.js b/docs/build/html/_static/copybutton_funcs.js deleted file mode 100644 index dbe1aaa..0000000 --- a/docs/build/html/_static/copybutton_funcs.js +++ /dev/null @@ -1,73 +0,0 @@ -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/** - * Removes excluded text from a Node. - * - * @param {Node} target Node to filter. - * @param {string} exclude CSS selector of nodes to exclude. - * @returns {DOMString} Text from `target` with text removed. - */ -export function filterText(target, exclude) { - const clone = target.cloneNode(true); // clone as to not modify the live DOM - if (exclude) { - // remove excluded nodes - clone.querySelectorAll(exclude).forEach(node => node.remove()); - } - return clone.innerText; -} - -// Callback when a copy button is clicked. Will be passed the node that was clicked -// should then grab the text and replace pieces of text that shouldn't be used in output -export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { - var regexp; - var match; - - // Do we check for line continuation characters and "HERE-documents"? - var useLineCont = !!lineContinuationChar - var useHereDoc = !!hereDocDelim - - // create regexp to capture prompt and remaining line - if (isRegexp) { - regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') - } else { - regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') - } - - const outputLines = []; - var promptFound = false; - var gotLineCont = false; - var gotHereDoc = false; - const lineGotPrompt = []; - for (const line of textContent.split('\n')) { - match = line.match(regexp) - if (match || gotLineCont || gotHereDoc) { - promptFound = regexp.test(line) - lineGotPrompt.push(promptFound) - if (removePrompts && promptFound) { - outputLines.push(match[2]) - } else { - outputLines.push(line) - } - gotLineCont = line.endsWith(lineContinuationChar) & useLineCont - if (line.includes(hereDocDelim) & useHereDoc) - gotHereDoc = !gotHereDoc - } else if (!onlyCopyPromptLines) { - outputLines.push(line) - } else if (copyEmptyLines && line.trim() === '') { - outputLines.push(line) - } - } - - // If no lines with the prompt were found then just use original lines - if (lineGotPrompt.some(v => v === true)) { - textContent = outputLines.join('\n'); - } - - // Remove a trailing newline to avoid auto-running when pasting - if (textContent.endsWith("\n")) { - textContent = textContent.slice(0, -1) - } - return textContent -} diff --git a/docs/build/html/_static/css/badge_only.css b/docs/build/html/_static/css/badge_only.css deleted file mode 100644 index 88ba55b..0000000 --- a/docs/build/html/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.eot b/docs/build/html/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca..0000000 Binary files a/docs/build/html/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.svg b/docs/build/html/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845..0000000 --- a/docs/build/html/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf b/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2..0000000 Binary files a/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a..0000000 Binary files a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc6..0000000 Binary files a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-bold-italic.woff b/docs/build/html/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 b/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-bold.woff b/docs/build/html/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-bold.woff2 b/docs/build/html/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-normal-italic.woff b/docs/build/html/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 b/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-normal.woff b/docs/build/html/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/build/html/_static/css/fonts/lato-normal.woff2 b/docs/build/html/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/build/html/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/build/html/_static/css/theme.css b/docs/build/html/_static/css/theme.css deleted file mode 100644 index a88467c..0000000 --- a/docs/build/html/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%;float:none;margin-left:0}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/build/html/_static/doctools.js b/docs/build/html/_static/doctools.js deleted file mode 100644 index 807cdb1..0000000 --- a/docs/build/html/_static/doctools.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Base JavaScript utilities for all Sphinx HTML documentation. - */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); - } -}; - -/** - * Small JavaScript module for the documentation. - */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); - }, - - /** - * i18n support - */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } - }, - - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; - }, - - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})`, - ); - Documentation.LOCALE = catalog.locale; - }, - - /** - * helper function to focus on search bar - */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); - }, - - /** - * Initialise the domain index toggle buttons - */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); - } - }; - - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)), - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); - }, - - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS - && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) - return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); - } - break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); - } - break; - } - } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } - }); - }, -}; - -// quick alias for translations -const _ = Documentation.gettext; - -_ready(Documentation.init); diff --git a/docs/build/html/_static/documentation_options.js b/docs/build/html/_static/documentation_options.js deleted file mode 100644 index 13d90ff..0000000 --- a/docs/build/html/_static/documentation_options.js +++ /dev/null @@ -1,13 +0,0 @@ -const DOCUMENTATION_OPTIONS = { - VERSION: '0.1.0', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, -}; \ No newline at end of file diff --git a/docs/build/html/_static/english-stemmer.js b/docs/build/html/_static/english-stemmer.js deleted file mode 100644 index 056760e..0000000 --- a/docs/build/html/_static/english-stemmer.js +++ /dev/null @@ -1,1066 +0,0 @@ -// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/ - -/**@constructor*/ -var EnglishStemmer = function() { - var base = new BaseStemmer(); - - /** @const */ var a_0 = [ - ["arsen", -1, -1], - ["commun", -1, -1], - ["emerg", -1, -1], - ["gener", -1, -1], - ["later", -1, -1], - ["organ", -1, -1], - ["past", -1, -1], - ["univers", -1, -1] - ]; - - /** @const */ var a_1 = [ - ["'", -1, 1], - ["'s'", 0, 1], - ["'s", -1, 1] - ]; - - /** @const */ var a_2 = [ - ["ied", -1, 2], - ["s", -1, 3], - ["ies", 1, 2], - ["sses", 1, 1], - ["ss", 1, -1], - ["us", 1, -1] - ]; - - /** @const */ var a_3 = [ - ["succ", -1, 1], - ["proc", -1, 1], - ["exc", -1, 1] - ]; - - /** @const */ var a_4 = [ - ["even", -1, 2], - ["cann", -1, 2], - ["inn", -1, 2], - ["earr", -1, 2], - ["herr", -1, 2], - ["out", -1, 2], - ["y", -1, 1] - ]; - - /** @const */ var a_5 = [ - ["", -1, -1], - ["ed", 0, 2], - ["eed", 1, 1], - ["ing", 0, 3], - ["edly", 0, 2], - ["eedly", 4, 1], - ["ingly", 0, 2] - ]; - - /** @const */ var a_6 = [ - ["", -1, 3], - ["bb", 0, 2], - ["dd", 0, 2], - ["ff", 0, 2], - ["gg", 0, 2], - ["bl", 0, 1], - ["mm", 0, 2], - ["nn", 0, 2], - ["pp", 0, 2], - ["rr", 0, 2], - ["at", 0, 1], - ["tt", 0, 2], - ["iz", 0, 1] - ]; - - /** @const */ var a_7 = [ - ["anci", -1, 3], - ["enci", -1, 2], - ["ogi", -1, 14], - ["li", -1, 16], - ["bli", 3, 12], - ["abli", 4, 4], - ["alli", 3, 8], - ["fulli", 3, 9], - ["lessli", 3, 15], - ["ousli", 3, 10], - ["entli", 3, 5], - ["aliti", -1, 8], - ["biliti", -1, 12], - ["iviti", -1, 11], - ["tional", -1, 1], - ["ational", 14, 7], - ["alism", -1, 8], - ["ation", -1, 7], - ["ization", 17, 6], - ["izer", -1, 6], - ["ator", -1, 7], - ["iveness", -1, 11], - ["fulness", -1, 9], - ["ousness", -1, 10], - ["ogist", -1, 13] - ]; - - /** @const */ var a_8 = [ - ["icate", -1, 4], - ["ative", -1, 6], - ["alize", -1, 3], - ["iciti", -1, 4], - ["ical", -1, 4], - ["tional", -1, 1], - ["ational", 5, 2], - ["ful", -1, 5], - ["ness", -1, 5] - ]; - - /** @const */ var a_9 = [ - ["ic", -1, 1], - ["ance", -1, 1], - ["ence", -1, 1], - ["able", -1, 1], - ["ible", -1, 1], - ["ate", -1, 1], - ["ive", -1, 1], - ["ize", -1, 1], - ["iti", -1, 1], - ["al", -1, 1], - ["ism", -1, 1], - ["ion", -1, 2], - ["er", -1, 1], - ["ous", -1, 1], - ["ant", -1, 1], - ["ent", -1, 1], - ["ment", 15, 1], - ["ement", 16, 1] - ]; - - /** @const */ var a_10 = [ - ["e", -1, 1], - ["l", -1, 2] - ]; - - /** @const */ var a_11 = [ - ["andes", -1, -1], - ["atlas", -1, -1], - ["bias", -1, -1], - ["cosmos", -1, -1], - ["early", -1, 5], - ["gently", -1, 3], - ["howe", -1, -1], - ["idly", -1, 2], - ["news", -1, -1], - ["only", -1, 6], - ["singly", -1, 7], - ["skies", -1, 1], - ["sky", -1, -1], - ["ugly", -1, 4] - ]; - - /** @const */ var /** Array */ g_aeo = [17, 64]; - - /** @const */ var /** Array */ g_v = [17, 65, 16, 1]; - - /** @const */ var /** Array */ g_v_WXY = [1, 17, 65, 208, 1]; - - /** @const */ var /** Array */ g_valid_LI = [55, 141, 2]; - - var /** boolean */ B_Y_found = false; - var /** number */ I_p2 = 0; - var /** number */ I_p1 = 0; - - - /** @return {boolean} */ - function r_prelude() { - B_Y_found = false; - /** @const */ var /** number */ v_1 = base.cursor; - lab0: { - base.bra = base.cursor; - if (!(base.eq_s("'"))) - { - break lab0; - } - base.ket = base.cursor; - if (!base.slice_del()) - { - return false; - } - } - base.cursor = v_1; - /** @const */ var /** number */ v_2 = base.cursor; - lab1: { - base.bra = base.cursor; - if (!(base.eq_s("y"))) - { - break lab1; - } - base.ket = base.cursor; - if (!base.slice_from("Y")) - { - return false; - } - B_Y_found = true; - } - base.cursor = v_2; - /** @const */ var /** number */ v_3 = base.cursor; - lab2: { - while(true) - { - /** @const */ var /** number */ v_4 = base.cursor; - lab3: { - golab4: while(true) - { - /** @const */ var /** number */ v_5 = base.cursor; - lab5: { - if (!(base.in_grouping(g_v, 97, 121))) - { - break lab5; - } - base.bra = base.cursor; - if (!(base.eq_s("y"))) - { - break lab5; - } - base.ket = base.cursor; - base.cursor = v_5; - break golab4; - } - base.cursor = v_5; - if (base.cursor >= base.limit) - { - break lab3; - } - base.cursor++; - } - if (!base.slice_from("Y")) - { - return false; - } - B_Y_found = true; - continue; - } - base.cursor = v_4; - break; - } - } - base.cursor = v_3; - return true; - }; - - /** @return {boolean} */ - function r_mark_regions() { - I_p1 = base.limit; - I_p2 = base.limit; - /** @const */ var /** number */ v_1 = base.cursor; - lab0: { - lab1: { - /** @const */ var /** number */ v_2 = base.cursor; - lab2: { - if (base.find_among(a_0) == 0) - { - break lab2; - } - break lab1; - } - base.cursor = v_2; - if (!base.go_out_grouping(g_v, 97, 121)) - { - break lab0; - } - base.cursor++; - if (!base.go_in_grouping(g_v, 97, 121)) - { - break lab0; - } - base.cursor++; - } - I_p1 = base.cursor; - if (!base.go_out_grouping(g_v, 97, 121)) - { - break lab0; - } - base.cursor++; - if (!base.go_in_grouping(g_v, 97, 121)) - { - break lab0; - } - base.cursor++; - I_p2 = base.cursor; - } - base.cursor = v_1; - return true; - }; - - /** @return {boolean} */ - function r_shortv() { - lab0: { - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab1: { - if (!(base.out_grouping_b(g_v_WXY, 89, 121))) - { - break lab1; - } - if (!(base.in_grouping_b(g_v, 97, 121))) - { - break lab1; - } - if (!(base.out_grouping_b(g_v, 97, 121))) - { - break lab1; - } - break lab0; - } - base.cursor = base.limit - v_1; - lab2: { - if (!(base.out_grouping_b(g_v, 97, 121))) - { - break lab2; - } - if (!(base.in_grouping_b(g_v, 97, 121))) - { - break lab2; - } - if (base.cursor > base.limit_backward) - { - break lab2; - } - break lab0; - } - base.cursor = base.limit - v_1; - if (!(base.eq_s_b("past"))) - { - return false; - } - } - return true; - }; - - /** @return {boolean} */ - function r_R1() { - return I_p1 <= base.cursor; - }; - - /** @return {boolean} */ - function r_R2() { - return I_p2 <= base.cursor; - }; - - /** @return {boolean} */ - function r_Step_1a() { - var /** number */ among_var; - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab0: { - base.ket = base.cursor; - if (base.find_among_b(a_1) == 0) - { - base.cursor = base.limit - v_1; - break lab0; - } - base.bra = base.cursor; - if (!base.slice_del()) - { - return false; - } - } - base.ket = base.cursor; - among_var = base.find_among_b(a_2); - if (among_var == 0) - { - return false; - } - base.bra = base.cursor; - switch (among_var) { - case 1: - if (!base.slice_from("ss")) - { - return false; - } - break; - case 2: - lab1: { - /** @const */ var /** number */ v_2 = base.limit - base.cursor; - lab2: { - { - /** @const */ var /** number */ c1 = base.cursor - 2; - if (c1 < base.limit_backward) - { - break lab2; - } - base.cursor = c1; - } - if (!base.slice_from("i")) - { - return false; - } - break lab1; - } - base.cursor = base.limit - v_2; - if (!base.slice_from("ie")) - { - return false; - } - } - break; - case 3: - if (base.cursor <= base.limit_backward) - { - return false; - } - base.cursor--; - if (!base.go_out_grouping_b(g_v, 97, 121)) - { - return false; - } - base.cursor--; - if (!base.slice_del()) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_Step_1b() { - var /** number */ among_var; - base.ket = base.cursor; - among_var = base.find_among_b(a_5); - base.bra = base.cursor; - lab0: { - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab1: { - switch (among_var) { - case 1: - /** @const */ var /** number */ v_2 = base.limit - base.cursor; - lab2: { - lab3: { - /** @const */ var /** number */ v_3 = base.limit - base.cursor; - lab4: { - if (base.find_among_b(a_3) == 0) - { - break lab4; - } - if (base.cursor > base.limit_backward) - { - break lab4; - } - break lab3; - } - base.cursor = base.limit - v_3; - if (!r_R1()) - { - break lab2; - } - if (!base.slice_from("ee")) - { - return false; - } - } - } - base.cursor = base.limit - v_2; - break; - case 2: - break lab1; - case 3: - among_var = base.find_among_b(a_4); - if (among_var == 0) - { - break lab1; - } - switch (among_var) { - case 1: - /** @const */ var /** number */ v_4 = base.limit - base.cursor; - if (!(base.out_grouping_b(g_v, 97, 121))) - { - break lab1; - } - if (base.cursor > base.limit_backward) - { - break lab1; - } - base.cursor = base.limit - v_4; - base.bra = base.cursor; - if (!base.slice_from("ie")) - { - return false; - } - break; - case 2: - if (base.cursor > base.limit_backward) - { - break lab1; - } - break; - } - break; - } - break lab0; - } - base.cursor = base.limit - v_1; - /** @const */ var /** number */ v_5 = base.limit - base.cursor; - if (!base.go_out_grouping_b(g_v, 97, 121)) - { - return false; - } - base.cursor--; - base.cursor = base.limit - v_5; - if (!base.slice_del()) - { - return false; - } - base.ket = base.cursor; - base.bra = base.cursor; - /** @const */ var /** number */ v_6 = base.limit - base.cursor; - among_var = base.find_among_b(a_6); - switch (among_var) { - case 1: - if (!base.slice_from("e")) - { - return false; - } - return false; - case 2: - { - /** @const */ var /** number */ v_7 = base.limit - base.cursor; - lab5: { - if (!(base.in_grouping_b(g_aeo, 97, 111))) - { - break lab5; - } - if (base.cursor > base.limit_backward) - { - break lab5; - } - return false; - } - base.cursor = base.limit - v_7; - } - break; - case 3: - if (base.cursor != I_p1) - { - return false; - } - /** @const */ var /** number */ v_8 = base.limit - base.cursor; - if (!r_shortv()) - { - return false; - } - base.cursor = base.limit - v_8; - if (!base.slice_from("e")) - { - return false; - } - return false; - } - base.cursor = base.limit - v_6; - base.ket = base.cursor; - if (base.cursor <= base.limit_backward) - { - return false; - } - base.cursor--; - base.bra = base.cursor; - if (!base.slice_del()) - { - return false; - } - } - return true; - }; - - /** @return {boolean} */ - function r_Step_1c() { - base.ket = base.cursor; - lab0: { - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab1: { - if (!(base.eq_s_b("y"))) - { - break lab1; - } - break lab0; - } - base.cursor = base.limit - v_1; - if (!(base.eq_s_b("Y"))) - { - return false; - } - } - base.bra = base.cursor; - if (!(base.out_grouping_b(g_v, 97, 121))) - { - return false; - } - lab2: { - if (base.cursor > base.limit_backward) - { - break lab2; - } - return false; - } - if (!base.slice_from("i")) - { - return false; - } - return true; - }; - - /** @return {boolean} */ - function r_Step_2() { - var /** number */ among_var; - base.ket = base.cursor; - among_var = base.find_among_b(a_7); - if (among_var == 0) - { - return false; - } - base.bra = base.cursor; - if (!r_R1()) - { - return false; - } - switch (among_var) { - case 1: - if (!base.slice_from("tion")) - { - return false; - } - break; - case 2: - if (!base.slice_from("ence")) - { - return false; - } - break; - case 3: - if (!base.slice_from("ance")) - { - return false; - } - break; - case 4: - if (!base.slice_from("able")) - { - return false; - } - break; - case 5: - if (!base.slice_from("ent")) - { - return false; - } - break; - case 6: - if (!base.slice_from("ize")) - { - return false; - } - break; - case 7: - if (!base.slice_from("ate")) - { - return false; - } - break; - case 8: - if (!base.slice_from("al")) - { - return false; - } - break; - case 9: - if (!base.slice_from("ful")) - { - return false; - } - break; - case 10: - if (!base.slice_from("ous")) - { - return false; - } - break; - case 11: - if (!base.slice_from("ive")) - { - return false; - } - break; - case 12: - if (!base.slice_from("ble")) - { - return false; - } - break; - case 13: - if (!base.slice_from("og")) - { - return false; - } - break; - case 14: - if (!(base.eq_s_b("l"))) - { - return false; - } - if (!base.slice_from("og")) - { - return false; - } - break; - case 15: - if (!base.slice_from("less")) - { - return false; - } - break; - case 16: - if (!(base.in_grouping_b(g_valid_LI, 99, 116))) - { - return false; - } - if (!base.slice_del()) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_Step_3() { - var /** number */ among_var; - base.ket = base.cursor; - among_var = base.find_among_b(a_8); - if (among_var == 0) - { - return false; - } - base.bra = base.cursor; - if (!r_R1()) - { - return false; - } - switch (among_var) { - case 1: - if (!base.slice_from("tion")) - { - return false; - } - break; - case 2: - if (!base.slice_from("ate")) - { - return false; - } - break; - case 3: - if (!base.slice_from("al")) - { - return false; - } - break; - case 4: - if (!base.slice_from("ic")) - { - return false; - } - break; - case 5: - if (!base.slice_del()) - { - return false; - } - break; - case 6: - if (!r_R2()) - { - return false; - } - if (!base.slice_del()) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_Step_4() { - var /** number */ among_var; - base.ket = base.cursor; - among_var = base.find_among_b(a_9); - if (among_var == 0) - { - return false; - } - base.bra = base.cursor; - if (!r_R2()) - { - return false; - } - switch (among_var) { - case 1: - if (!base.slice_del()) - { - return false; - } - break; - case 2: - lab0: { - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab1: { - if (!(base.eq_s_b("s"))) - { - break lab1; - } - break lab0; - } - base.cursor = base.limit - v_1; - if (!(base.eq_s_b("t"))) - { - return false; - } - } - if (!base.slice_del()) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_Step_5() { - var /** number */ among_var; - base.ket = base.cursor; - among_var = base.find_among_b(a_10); - if (among_var == 0) - { - return false; - } - base.bra = base.cursor; - switch (among_var) { - case 1: - lab0: { - lab1: { - if (!r_R2()) - { - break lab1; - } - break lab0; - } - if (!r_R1()) - { - return false; - } - { - /** @const */ var /** number */ v_1 = base.limit - base.cursor; - lab2: { - if (!r_shortv()) - { - break lab2; - } - return false; - } - base.cursor = base.limit - v_1; - } - } - if (!base.slice_del()) - { - return false; - } - break; - case 2: - if (!r_R2()) - { - return false; - } - if (!(base.eq_s_b("l"))) - { - return false; - } - if (!base.slice_del()) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_exception1() { - var /** number */ among_var; - base.bra = base.cursor; - among_var = base.find_among(a_11); - if (among_var == 0) - { - return false; - } - base.ket = base.cursor; - if (base.cursor < base.limit) - { - return false; - } - switch (among_var) { - case 1: - if (!base.slice_from("sky")) - { - return false; - } - break; - case 2: - if (!base.slice_from("idl")) - { - return false; - } - break; - case 3: - if (!base.slice_from("gentl")) - { - return false; - } - break; - case 4: - if (!base.slice_from("ugli")) - { - return false; - } - break; - case 5: - if (!base.slice_from("earli")) - { - return false; - } - break; - case 6: - if (!base.slice_from("onli")) - { - return false; - } - break; - case 7: - if (!base.slice_from("singl")) - { - return false; - } - break; - } - return true; - }; - - /** @return {boolean} */ - function r_postlude() { - if (!B_Y_found) - { - return false; - } - while(true) - { - /** @const */ var /** number */ v_1 = base.cursor; - lab0: { - golab1: while(true) - { - /** @const */ var /** number */ v_2 = base.cursor; - lab2: { - base.bra = base.cursor; - if (!(base.eq_s("Y"))) - { - break lab2; - } - base.ket = base.cursor; - base.cursor = v_2; - break golab1; - } - base.cursor = v_2; - if (base.cursor >= base.limit) - { - break lab0; - } - base.cursor++; - } - if (!base.slice_from("y")) - { - return false; - } - continue; - } - base.cursor = v_1; - break; - } - return true; - }; - - this.stem = /** @return {boolean} */ function() { - lab0: { - /** @const */ var /** number */ v_1 = base.cursor; - lab1: { - if (!r_exception1()) - { - break lab1; - } - break lab0; - } - base.cursor = v_1; - lab2: { - { - /** @const */ var /** number */ v_2 = base.cursor; - lab3: { - { - /** @const */ var /** number */ c1 = base.cursor + 3; - if (c1 > base.limit) - { - break lab3; - } - base.cursor = c1; - } - break lab2; - } - base.cursor = v_2; - } - break lab0; - } - base.cursor = v_1; - r_prelude(); - r_mark_regions(); - base.limit_backward = base.cursor; base.cursor = base.limit; - /** @const */ var /** number */ v_3 = base.limit - base.cursor; - r_Step_1a(); - base.cursor = base.limit - v_3; - /** @const */ var /** number */ v_4 = base.limit - base.cursor; - r_Step_1b(); - base.cursor = base.limit - v_4; - /** @const */ var /** number */ v_5 = base.limit - base.cursor; - r_Step_1c(); - base.cursor = base.limit - v_5; - /** @const */ var /** number */ v_6 = base.limit - base.cursor; - r_Step_2(); - base.cursor = base.limit - v_6; - /** @const */ var /** number */ v_7 = base.limit - base.cursor; - r_Step_3(); - base.cursor = base.limit - v_7; - /** @const */ var /** number */ v_8 = base.limit - base.cursor; - r_Step_4(); - base.cursor = base.limit - v_8; - /** @const */ var /** number */ v_9 = base.limit - base.cursor; - r_Step_5(); - base.cursor = base.limit - v_9; - base.cursor = base.limit_backward; - /** @const */ var /** number */ v_10 = base.cursor; - r_postlude(); - base.cursor = v_10; - } - return true; - }; - - /**@return{string}*/ - this['stemWord'] = function(/**string*/word) { - base.setCurrent(word); - this.stem(); - return base.getCurrent(); - }; -}; diff --git a/docs/build/html/_static/file.png b/docs/build/html/_static/file.png deleted file mode 100644 index a858a41..0000000 Binary files a/docs/build/html/_static/file.png and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.eot b/docs/build/html/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.ttf b/docs/build/html/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.woff b/docs/build/html/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.woff2 b/docs/build/html/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot b/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d41549..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf b/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.eot b/docs/build/html/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f82642..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.ttf b/docs/build/html/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.woff b/docs/build/html/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.woff2 b/docs/build/html/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.eot b/docs/build/html/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.ttf b/docs/build/html/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.woff b/docs/build/html/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.woff2 b/docs/build/html/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/build/html/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8ef..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a79..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/build/html/_static/jquery.js b/docs/build/html/_static/jquery.js deleted file mode 100644 index c4c6022..0000000 --- a/docs/build/html/_static/jquery.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); - - const languagesHTML = ` -
-
Languages
- ${languages - .map( - (translation) => ` -
- ${translation.language.code} -
- `, - ) - .join("\n")} -
- `; - return languagesHTML; - } - - function renderVersions(config) { - if (!config.versions.active.length) { - return ""; - } - const versionsHTML = ` -
-
Versions
- ${config.versions.active - .map( - (version) => ` -
- ${version.slug} -
- `, - ) - .join("\n")} -
- `; - return versionsHTML; - } - - function renderDownloads(config) { - if (!Object.keys(config.versions.current.downloads).length) { - return ""; - } - const downloadsNameDisplay = { - pdf: "PDF", - epub: "Epub", - htmlzip: "HTML", - }; - - const downloadsHTML = ` -
-
Downloads
- ${Object.entries(config.versions.current.downloads) - .map( - ([name, url]) => ` -
- ${downloadsNameDisplay[name]} -
- `, - ) - .join("\n")} -
- `; - return downloadsHTML; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const flyout = ` -
- - Read the Docs - v: ${config.versions.current.slug} - - -
-
- ${renderLanguages(config)} - ${renderVersions(config)} - ${renderDownloads(config)} -
-
On Read the Docs
-
- Project Home -
-
- Builds -
-
- Downloads -
-
-
-
Search
-
-
- -
-
-
-
- - Hosted by Read the Docs - -
-
- `; - - // Inject the generated flyout into the body HTML element. - document.body.insertAdjacentHTML("beforeend", flyout); - - // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. - document - .querySelector("#flyout-search-form") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); - }) -} - -if (themeLanguageSelector || themeVersionSelector) { - function onSelectorSwitch(event) { - const option = event.target.selectedIndex; - const item = event.target.options[option]; - window.location.href = item.dataset.url; - } - - document.addEventListener("readthedocs-addons-data-ready", function (event) { - const config = event.detail.data(); - - const versionSwitch = document.querySelector( - "div.switch-menus > div.version-switch", - ); - if (themeVersionSelector) { - let versions = config.versions.active; - if (config.versions.current.hidden || config.versions.current.type === "external") { - versions.unshift(config.versions.current); - } - const versionSelect = ` - - `; - - versionSwitch.innerHTML = versionSelect; - versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - - const languageSwitch = document.querySelector( - "div.switch-menus > div.language-switch", - ); - - if (themeLanguageSelector) { - if (config.projects.translations.length) { - // Add the current language to the options on the selector - let languages = config.projects.translations.concat( - config.projects.current, - ); - languages = languages.sort((a, b) => - a.language.name.localeCompare(b.language.name), - ); - - const languageSelect = ` - - `; - - languageSwitch.innerHTML = languageSelect; - languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); - } - else { - languageSwitch.remove(); - } - } - }); -} - -document.addEventListener("readthedocs-addons-data-ready", function (event) { - // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. - document - .querySelector("[role='search'] input") - .addEventListener("focusin", () => { - const event = new CustomEvent("readthedocs-search-show"); - document.dispatchEvent(event); - }); -}); \ No newline at end of file diff --git a/docs/build/html/_static/language_data.js b/docs/build/html/_static/language_data.js deleted file mode 100644 index 5776786..0000000 --- a/docs/build/html/_static/language_data.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This script contains the language-specific data used by searchtools.js, - * namely the set of stopwords, stemmer, scorer and splitter. - */ - -const stopwords = new Set(["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]); -window.stopwords = stopwords; // Export to global scope - - -/* Non-minified versions are copied as separate JavaScript files, if available */ -BaseStemmer=function(){this.current="",this.cursor=0,this.limit=0,this.limit_backward=0,this.bra=0,this.ket=0,this.setCurrent=function(t){this.current=t,this.cursor=0,this.limit=this.current.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},this.getCurrent=function(){return this.current},this.copy_from=function(t){this.current=t.current,this.cursor=t.cursor,this.limit=t.limit,this.limit_backward=t.limit_backward,this.bra=t.bra,this.ket=t.ket},this.in_grouping=function(t,r,i){return!(this.cursor>=this.limit||i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i))||(this.cursor++,0))},this.go_in_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.in_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward||i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i))||(this.cursor--,0))},this.go_in_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(i>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.out_grouping=function(t,r,i){return!(this.cursor>=this.limit)&&(i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i)))&&(this.cursor++,!0)},this.go_out_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.out_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward)&&(i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i)))&&(this.cursor--,!0)},this.go_out_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(s<=i&&r<=s&&0!=(t[(s-=r)>>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.eq_s=function(t){return!(this.limit-this.cursor>>1),o=0,a=e=(l=t[r])[0].length){if(this.cursor=s+l[0].length,l.length<4)return l[2];var g=l[3](this);if(this.cursor=s+l[0].length,g)return l[2]}}while(0<=(r=l[1]));return 0},this.find_among_b=function(t){for(var r=0,i=t.length,s=this.cursor,h=this.limit_backward,e=0,n=0,c=!1;;){for(var u,o=r+(i-r>>1),a=0,l=e=(u=t[r])[0].length){if(this.cursor=s-u[0].length,u.length<4)return u[2];var g=u[3](this);if(this.cursor=s-u[0].length,g)return u[2]}}while(0<=(r=u[1]));return 0},this.replace_s=function(t,r,i){var s=i.length-(r-t);return this.current=this.current.slice(0,t)+i+this.current.slice(r),this.limit+=s,this.cursor>=r?this.cursor+=s:this.cursor>t&&(this.cursor=t),s},this.slice_check=function(){return!(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>this.current.length)},this.slice_from=function(t){var r=!1;return this.slice_check()&&(this.replace_s(this.bra,this.ket,t),r=!0),r},this.slice_del=function(){return this.slice_from("")},this.insert=function(t,r,i){r=this.replace_s(t,r,i);t<=this.bra&&(this.bra+=r),t<=this.ket&&(this.ket+=r)},this.slice_to=function(){var t="";return t=this.slice_check()?this.current.slice(this.bra,this.ket):t},this.assign_to=function(){return this.current.slice(0,this.limit)}}; -var EnglishStemmer=function(){var a=new BaseStemmer,c=[["arsen",-1,-1],["commun",-1,-1],["emerg",-1,-1],["gener",-1,-1],["later",-1,-1],["organ",-1,-1],["past",-1,-1],["univers",-1,-1]],o=[["'",-1,1],["'s'",0,1],["'s",-1,1]],u=[["ied",-1,2],["s",-1,3],["ies",1,2],["sses",1,1],["ss",1,-1],["us",1,-1]],t=[["succ",-1,1],["proc",-1,1],["exc",-1,1]],l=[["even",-1,2],["cann",-1,2],["inn",-1,2],["earr",-1,2],["herr",-1,2],["out",-1,2],["y",-1,1]],n=[["",-1,-1],["ed",0,2],["eed",1,1],["ing",0,3],["edly",0,2],["eedly",4,1],["ingly",0,2]],f=[["",-1,3],["bb",0,2],["dd",0,2],["ff",0,2],["gg",0,2],["bl",0,1],["mm",0,2],["nn",0,2],["pp",0,2],["rr",0,2],["at",0,1],["tt",0,2],["iz",0,1]],_=[["anci",-1,3],["enci",-1,2],["ogi",-1,14],["li",-1,16],["bli",3,12],["abli",4,4],["alli",3,8],["fulli",3,9],["lessli",3,15],["ousli",3,10],["entli",3,5],["aliti",-1,8],["biliti",-1,12],["iviti",-1,11],["tional",-1,1],["ational",14,7],["alism",-1,8],["ation",-1,7],["ization",17,6],["izer",-1,6],["ator",-1,7],["iveness",-1,11],["fulness",-1,9],["ousness",-1,10],["ogist",-1,13]],m=[["icate",-1,4],["ative",-1,6],["alize",-1,3],["iciti",-1,4],["ical",-1,4],["tional",-1,1],["ational",5,2],["ful",-1,5],["ness",-1,5]],b=[["ic",-1,1],["ance",-1,1],["ence",-1,1],["able",-1,1],["ible",-1,1],["ate",-1,1],["ive",-1,1],["ize",-1,1],["iti",-1,1],["al",-1,1],["ism",-1,1],["ion",-1,2],["er",-1,1],["ous",-1,1],["ant",-1,1],["ent",-1,1],["ment",15,1],["ement",16,1]],k=[["e",-1,1],["l",-1,2]],g=[["andes",-1,-1],["atlas",-1,-1],["bias",-1,-1],["cosmos",-1,-1],["early",-1,5],["gently",-1,3],["howe",-1,-1],["idly",-1,2],["news",-1,-1],["only",-1,6],["singly",-1,7],["skies",-1,1],["sky",-1,-1],["ugly",-1,4]],d=[17,64],v=[17,65,16,1],i=[1,17,65,208,1],w=[55,141,2],p=!1,y=0,h=0;function q(){var r=a.limit-a.cursor;return!!(a.out_grouping_b(i,89,121)&&a.in_grouping_b(v,97,121)&&a.out_grouping_b(v,97,121)||(a.cursor=a.limit-r,a.out_grouping_b(v,97,121)&&a.in_grouping_b(v,97,121)&&!(a.cursor>a.limit_backward))||(a.cursor=a.limit-r,a.eq_s_b("past")))}function z(){return h<=a.cursor}function Y(){return y<=a.cursor}this.stem=function(){var r=a.cursor;if(!(()=>{var r;if(a.bra=a.cursor,0!=(r=a.find_among(g))&&(a.ket=a.cursor,!(a.cursora.limit)a.cursor=i;else{a.cursor=e,a.cursor=r,(()=>{p=!1;var r=a.cursor;if(a.bra=a.cursor,!a.eq_s("'")||(a.ket=a.cursor,a.slice_del())){a.cursor=r;r=a.cursor;if(a.bra=a.cursor,a.eq_s("y")){if(a.ket=a.cursor,!a.slice_from("Y"))return;p=!0}a.cursor=r;for(r=a.cursor;;){var i=a.cursor;r:{for(;;){var e=a.cursor;if(a.in_grouping(v,97,121)&&(a.bra=a.cursor,a.eq_s("y"))){a.ket=a.cursor,a.cursor=e;break}if(a.cursor=e,a.cursor>=a.limit)break r;a.cursor++}if(!a.slice_from("Y"))return;p=!0;continue}a.cursor=i;break}a.cursor=r}})(),h=a.limit,y=a.limit;i=a.cursor;r:{var s=a.cursor;if(0==a.find_among(c)){if(a.cursor=s,!a.go_out_grouping(v,97,121))break r;if(a.cursor++,!a.go_in_grouping(v,97,121))break r;a.cursor++}h=a.cursor,a.go_out_grouping(v,97,121)&&(a.cursor++,a.go_in_grouping(v,97,121))&&(a.cursor++,y=a.cursor)}a.cursor=i,a.limit_backward=a.cursor,a.cursor=a.limit;var e=a.limit-a.cursor,r=((()=>{var r=a.limit-a.cursor;if(a.ket=a.cursor,0==a.find_among_b(o))a.cursor=a.limit-r;else if(a.bra=a.cursor,!a.slice_del())return;if(a.ket=a.cursor,0!=(r=a.find_among_b(u)))switch(a.bra=a.cursor,r){case 1:if(a.slice_from("ss"))break;return;case 2:r:{var i=a.limit-a.cursor,e=a.cursor-2;if(!(e{a.ket=a.cursor,o=a.find_among_b(n),a.bra=a.cursor;r:{var r=a.limit-a.cursor;i:{switch(o){case 1:var i=a.limit-a.cursor;e:{var e=a.limit-a.cursor;if(0==a.find_among_b(t)||a.cursor>a.limit_backward){if(a.cursor=a.limit-e,!z())break e;if(!a.slice_from("ee"))return}}a.cursor=a.limit-i;break;case 2:break i;case 3:if(0==(o=a.find_among_b(l)))break i;switch(o){case 1:var s=a.limit-a.cursor;if(!a.out_grouping_b(v,97,121))break i;if(a.cursor>a.limit_backward)break i;if(a.cursor=a.limit-s,a.bra=a.cursor,a.slice_from("ie"))break;return;case 2:if(a.cursor>a.limit_backward)break i}}break r}a.cursor=a.limit-r;var c=a.limit-a.cursor;if(!a.go_out_grouping_b(v,97,121))return;if(a.cursor--,a.cursor=a.limit-c,!a.slice_del())return;a.ket=a.cursor,a.bra=a.cursor;var o,c=a.limit-a.cursor;switch(o=a.find_among_b(f)){case 1:return a.slice_from("e");case 2:var u=a.limit-a.cursor;if(a.in_grouping_b(d,97,111)&&!(a.cursor>a.limit_backward))return;a.cursor=a.limit-u;break;case 3:return a.cursor!=h||(u=a.limit-a.cursor,q()&&(a.cursor=a.limit-u,a.slice_from("e")))}if(a.cursor=a.limit-c,a.ket=a.cursor,a.cursor<=a.limit_backward)return;if(a.cursor--,a.bra=a.cursor,!a.slice_del())return}})(),a.cursor=a.limit-r,a.limit-a.cursor),r=(a.ket=a.cursor,e=a.limit-a.cursor,(a.eq_s_b("y")||(a.cursor=a.limit-e,a.eq_s_b("Y")))&&(a.bra=a.cursor,a.out_grouping_b(v,97,121))&&a.cursor>a.limit_backward&&a.slice_from("i"),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(_))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ence"))break;return;case 3:if(a.slice_from("ance"))break;return;case 4:if(a.slice_from("able"))break;return;case 5:if(a.slice_from("ent"))break;return;case 6:if(a.slice_from("ize"))break;return;case 7:if(a.slice_from("ate"))break;return;case 8:if(a.slice_from("al"))break;return;case 9:if(a.slice_from("ful"))break;return;case 10:if(a.slice_from("ous"))break;return;case 11:if(a.slice_from("ive"))break;return;case 12:if(a.slice_from("ble"))break;return;case 13:if(a.slice_from("og"))break;return;case 14:if(!a.eq_s_b("l"))return;if(a.slice_from("og"))break;return;case 15:if(a.slice_from("less"))break;return;case 16:if(!a.in_grouping_b(w,99,116))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.limit-a.cursor),i=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(m))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ate"))break;return;case 3:if(a.slice_from("al"))break;return;case 4:if(a.slice_from("ic"))break;return;case 5:if(a.slice_del())break;return;case 6:if(!Y())return;if(a.slice_del())break}})(),a.cursor=a.limit-e,a.limit-a.cursor),r=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(b))&&(a.bra=a.cursor,Y()))switch(r){case 1:if(a.slice_del())break;return;case 2:var i=a.limit-a.cursor;if(!a.eq_s_b("s")&&(a.cursor=a.limit-i,!a.eq_s_b("t")))return;if(a.slice_del())break}})(),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(k)))switch(a.bra=a.cursor,r){case 1:if(!Y()){if(!z())return;var i=a.limit-a.cursor;if(q())return;a.cursor=a.limit-i}if(a.slice_del())break;return;case 2:if(!Y())return;if(!a.eq_s_b("l"))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.cursor=a.limit_backward,a.cursor);(()=>{if(p)for(;;){var r=a.cursor;r:{for(;;){var i=a.cursor;if(a.bra=a.cursor,a.eq_s("Y")){a.ket=a.cursor,a.cursor=i;break}if(a.cursor=i,a.cursor>=a.limit)break r;a.cursor++}if(a.slice_from("y"))continue;return}a.cursor=r;break}})(),a.cursor=e}}return!0},this.stemWord=function(r){return a.setCurrent(r),this.stem(),a.getCurrent()}}; -window.Stemmer = EnglishStemmer; diff --git a/docs/build/html/_static/minus.png b/docs/build/html/_static/minus.png deleted file mode 100644 index d96755f..0000000 Binary files a/docs/build/html/_static/minus.png and /dev/null differ diff --git a/docs/build/html/_static/plus.png b/docs/build/html/_static/plus.png deleted file mode 100644 index 7107cec..0000000 Binary files a/docs/build/html/_static/plus.png and /dev/null differ diff --git a/docs/build/html/_static/pygments.css b/docs/build/html/_static/pygments.css deleted file mode 100644 index 6f8b210..0000000 --- a/docs/build/html/_static/pygments.css +++ /dev/null @@ -1,75 +0,0 @@ -pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #F00 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #04D } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ -.highlight .no { color: #800 } /* Name.Constant */ -.highlight .nd { color: #A2F } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #00F } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #BBB } /* Text.Whitespace */ -.highlight .mb { color: #666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666 } /* Literal.Number.Float */ -.highlight .mh { color: #666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #00F } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/build/html/_static/searchtools.js b/docs/build/html/_static/searchtools.js deleted file mode 100644 index e29b1c7..0000000 --- a/docs/build/html/_static/searchtools.js +++ /dev/null @@ -1,693 +0,0 @@ -/* - * Sphinx JavaScript utilities for the full-text search. - */ -"use strict"; - -/** - * Simple result scoring code. - */ -if (typeof Scorer === "undefined") { - var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [docname, title, anchor, descr, score, filename] - // and returns the new score. - /* - score: result => { - const [docname, title, anchor, descr, score, filename, kind] = result - return score - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: { - 0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5, // used to be unimportantResults - }, - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - partialTitle: 7, - // query found in terms - term: 5, - partialTerm: 2, - }; -} - -// Global search result kind enum, used by themes to style search results. -// prettier-ignore -class SearchResultKind { - static get index() { return "index"; } - static get object() { return "object"; } - static get text() { return "text"; } - static get title() { return "title"; } -} - -const _removeChildren = (element) => { - while (element && element.lastChild) element.removeChild(element.lastChild); -}; - -/** - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - */ -const _escapeRegExp = (string) => - string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - -const _escapeHTML = (text) => { - return text - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -}; - -const _displayItem = (item, searchTerms, highlightTerms) => { - const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; - const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; - const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; - const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; - const contentRoot = document.documentElement.dataset.content_root; - - const [docName, title, anchor, descr, score, _filename, kind] = item; - - let listItem = document.createElement("li"); - // Add a class representing the item's type: - // can be used by a theme's CSS selector for styling - // See SearchResultKind for the class names. - listItem.classList.add(`kind-${kind}`); - let requestUrl; - let linkUrl; - if (docBuilder === "dirhtml") { - // dirhtml builder - let dirname = docName + "/"; - if (dirname.match(/\/index\/$/)) - dirname = dirname.substring(0, dirname.length - 6); - else if (dirname === "index/") dirname = ""; - requestUrl = contentRoot + dirname; - linkUrl = requestUrl; - } else { - // normal html builders - requestUrl = contentRoot + docName + docFileSuffix; - linkUrl = docName + docLinkSuffix; - } - let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + anchor; - linkEl.dataset.score = score; - linkEl.innerHTML = _escapeHTML(title); - if (descr) { - listItem.appendChild(document.createElement("span")).innerHTML = - ` (${_escapeHTML(descr)})`; - // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) - // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js - highlightTerms.forEach((term) => - _highlightText(listItem, term, "highlighted"), - ); - } else if (showSearchSummary) - fetch(requestUrl) - .then((responseData) => responseData.text()) - .then((data) => { - if (data) - listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor), - ); - // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) - // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js - highlightTerms.forEach((term) => - _highlightText(listItem, term, "highlighted"), - ); - }); - Search.output.appendChild(listItem); -}; -const _finishSearch = (resultCount) => { - Search.stopPulse(); - Search.title.innerText = _("Search Results"); - if (!resultCount) - Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.", - ); - else - Search.status.innerText = Documentation.ngettext( - "Search finished, found one page matching the search query.", - "Search finished, found ${resultCount} pages matching the search query.", - resultCount, - ).replace("${resultCount}", resultCount); -}; -const _displayNextItem = ( - results, - resultCount, - searchTerms, - highlightTerms, -) => { - // results left, load the summary and display it - // this is intended to be dynamic (don't sub resultsCount) - if (results.length) { - _displayItem(results.pop(), searchTerms, highlightTerms); - setTimeout( - () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5, - ); - } - // search finished, update title and status message - else _finishSearch(resultCount); -}; -// Helper function used by query() to order search results. -// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. -// Order the results by score (in opposite order of appearance, since the -// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. -const _orderResultsByScoreThenName = (a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; -}; - -/** - * Default splitQuery function. Can be overridden in ``sphinx.search`` with a - * custom function per language. - * - * The regular expression works by splitting the string on consecutive characters - * that are not Unicode letters, numbers, underscores, or emoji characters. - * This is the same as ``\W+`` in Python, preserving the surrogate pair area. - */ -if (typeof splitQuery === "undefined") { - var splitQuery = (query) => - query - .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter((term) => term); // remove remaining empty strings -} - -/** - * Search Module - */ -const Search = { - _index: null, - _queued_query: null, - _pulse_status: -1, - - htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString( - htmlString, - "text/html", - ); - for (const removalQuery of [".headerlink", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { - el.remove(); - }); - } - if (anchor) { - const anchorContent = htmlElement.querySelector( - `[role="main"] ${anchor}`, - ); - if (anchorContent) return anchorContent.textContent; - - console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`, - ); - } - - // if anchor not specified or not found, fall back to main content - const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent) return docContent.textContent; - - console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.", - ); - return ""; - }, - - init: () => { - const query = new URLSearchParams(window.location.search).get("q"); - document - .querySelectorAll('input[name="q"]') - .forEach((el) => (el.value = query)); - if (query) Search.performSearch(query); - }, - - loadIndex: (url) => - (document.body.appendChild(document.createElement("script")).src = url), - - setIndex: (index) => { - Search._index = index; - if (Search._queued_query !== null) { - const query = Search._queued_query; - Search._queued_query = null; - Search.query(query); - } - }, - - hasIndex: () => Search._index !== null, - - deferQuery: (query) => (Search._queued_query = query), - - stopPulse: () => (Search._pulse_status = -1), - - startPulse: () => { - if (Search._pulse_status >= 0) return; - - const pulse = () => { - Search._pulse_status = (Search._pulse_status + 1) % 4; - Search.dots.innerText = ".".repeat(Search._pulse_status); - if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); - }; - pulse(); - }, - - /** - * perform a search for something (or wait until index is loaded) - */ - performSearch: (query) => { - // create the required interface elements - const searchText = document.createElement("h2"); - searchText.textContent = _("Searching"); - const searchSummary = document.createElement("p"); - searchSummary.classList.add("search-summary"); - searchSummary.innerText = ""; - const searchList = document.createElement("ul"); - searchList.setAttribute("role", "list"); - searchList.classList.add("search"); - - const out = document.getElementById("search-results"); - Search.title = out.appendChild(searchText); - Search.dots = Search.title.appendChild(document.createElement("span")); - Search.status = out.appendChild(searchSummary); - Search.output = out.appendChild(searchList); - - const searchProgress = document.getElementById("search-progress"); - // Some themes don't use the search progress node - if (searchProgress) { - searchProgress.innerText = _("Preparing search..."); - } - Search.startPulse(); - - // index already loaded, the browser was quick! - if (Search.hasIndex()) Search.query(query); - else Search.deferQuery(query); - }, - - _parseQuery: (query) => { - // stem the search terms and add them to the correct list - const stemmer = new Stemmer(); - const searchTerms = new Set(); - const excludedTerms = new Set(); - const highlightTerms = new Set(); - const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); - splitQuery(query.trim()).forEach((queryTerm) => { - const queryTermLower = queryTerm.toLowerCase(); - - // maybe skip this "word" - // stopwords set is from language_data.js - if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return; - - // stem the word - let word = stemmer.stemWord(queryTermLower); - // select the correct list - if (word[0] === "-") excludedTerms.add(word.substr(1)); - else { - searchTerms.add(word); - highlightTerms.add(queryTermLower); - } - }); - - if (SPHINX_HIGHLIGHT_ENABLED) { - // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js - localStorage.setItem( - "sphinx_highlight_terms", - [...highlightTerms].join(" "), - ); - } - - // console.debug("SEARCH: searching for:"); - // console.info("required: ", [...searchTerms]); - // console.info("excluded: ", [...excludedTerms]); - - return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; - }, - - /** - * execute search (requires search index to be loaded) - */ - _performSearch: ( - query, - searchTerms, - excludedTerms, - highlightTerms, - objectTerms, - ) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - const allTitles = Search._index.alltitles; - const indexEntries = Search._index.indexentries; - - // Collect multiple result groups to be sorted separately and then ordered. - // Each is an array of [docname, title, anchor, descr, score, filename, kind]. - const normalResults = []; - const nonMainIndexResults = []; - - _removeChildren(document.getElementById("search-progress")); - - const queryLower = query.toLowerCase().trim(); - for (const [title, foundTitles] of Object.entries(allTitles)) { - if ( - title.toLowerCase().trim().includes(queryLower) - && queryLower.length >= title.length / 2 - ) { - for (const [file, id] of foundTitles) { - const score = Math.round( - (Scorer.title * queryLower.length) / title.length, - ); - const boost = titles[file] === title ? 1 : 0; // add a boost for document titles - normalResults.push([ - docNames[file], - titles[file] !== title ? `${titles[file]} > ${title}` : title, - id !== null ? "#" + id : "", - null, - score + boost, - filenames[file], - SearchResultKind.title, - ]); - } - } - } - - // search for explicit entries in index directives - for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) { - for (const [file, id, isMain] of foundEntries) { - const score = Math.round((100 * queryLower.length) / entry.length); - const result = [ - docNames[file], - titles[file], - id ? "#" + id : "", - null, - score, - filenames[file], - SearchResultKind.index, - ]; - if (isMain) { - normalResults.push(result); - } else { - nonMainIndexResults.push(result); - } - } - } - } - - // lookup as object - objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)), - ); - - // lookup as search terms in fulltext - normalResults.push( - ...Search.performTermsSearch(searchTerms, excludedTerms), - ); - - // let the scorer override scores with a custom scoring function - if (Scorer.score) { - normalResults.forEach((item) => (item[4] = Scorer.score(item))); - nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); - } - - // Sort each group of results by score and then alphabetically by name. - normalResults.sort(_orderResultsByScoreThenName); - nonMainIndexResults.sort(_orderResultsByScoreThenName); - - // Combine the result groups in (reverse) order. - // Non-main index entries are typically arbitrary cross-references, - // so display them after other results. - let results = [...nonMainIndexResults, ...normalResults]; - - // remove duplicate search results - // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept - let seen = new Set(); - results = results.reverse().reduce((acc, result) => { - let resultStr = result - .slice(0, 4) - .concat([result[5]]) - .map((v) => String(v)) - .join(","); - if (!seen.has(resultStr)) { - acc.push(result); - seen.add(resultStr); - } - return acc; - }, []); - - return results.reverse(); - }, - - query: (query) => { - const [ - searchQuery, - searchTerms, - excludedTerms, - highlightTerms, - objectTerms, - ] = Search._parseQuery(query); - const results = Search._performSearch( - searchQuery, - searchTerms, - excludedTerms, - highlightTerms, - objectTerms, - ); - - // for debugging - //Search.lastresults = results.slice(); // a copy - // console.info("search results:", Search.lastresults); - - // print the results - _displayNextItem(results, results.length, searchTerms, highlightTerms); - }, - - /** - * search for object names - */ - performObjectSearch: (object, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const objects = Search._index.objects; - const objNames = Search._index.objnames; - const titles = Search._index.titles; - - const results = []; - - const objectSearchCallback = (prefix, match) => { - const name = match[4]; - const fullname = (prefix ? prefix + "." : "") + name; - const fullnameLower = fullname.toLowerCase(); - if (fullnameLower.indexOf(object) < 0) return; - - let score = 0; - const parts = fullnameLower.split("."); - - // check for different match types: exact matches of full name or - // "last name" (i.e. last dotted part) - if (fullnameLower === object || parts.slice(-1)[0] === object) - score += Scorer.objNameMatch; - else if (parts.slice(-1)[0].indexOf(object) > -1) - score += Scorer.objPartialMatch; // matches in last name - - const objName = objNames[match[1]][2]; - const title = titles[match[0]]; - - // If more than one term searched for, we require other words to be - // found in the name/title/description - const otherTerms = new Set(objectTerms); - otherTerms.delete(object); - if (otherTerms.size > 0) { - const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); - if ( - [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) - ) - return; - } - - let anchor = match[3]; - if (anchor === "") anchor = fullname; - else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; - - const descr = objName + _(", in ") + title; - - // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) - score += Scorer.objPrio[match[2]]; - else score += Scorer.objPrioDefault; - - results.push([ - docNames[match[0]], - fullname, - "#" + anchor, - descr, - score, - filenames[match[0]], - SearchResultKind.object, - ]); - }; - Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => objectSearchCallback(prefix, array)), - ); - return results; - }, - - /** - * search for full-text terms in the index - */ - performTermsSearch: (searchTerms, excludedTerms) => { - // prepare search - const terms = Search._index.terms; - const titleTerms = Search._index.titleterms; - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - - const scoreMap = new Map(); - const fileMap = new Map(); - - // perform the search on the required terms - searchTerms.forEach((word) => { - const files = []; - // find documents, if any, containing the query word in their text/title term indices - // use Object.hasOwnProperty to avoid mismatching against prototype properties - const arr = [ - { - files: terms.hasOwnProperty(word) ? terms[word] : undefined, - score: Scorer.term, - }, - { - files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, - score: Scorer.title, - }, - ]; - // add support for partial matches - if (word.length > 2) { - const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - } - if (!titleTerms.hasOwnProperty(word)) { - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); - }); - } - } - - // no match but word was a required one - if (arr.every((record) => record.files === undefined)) return; - - // found search word in contents - arr.forEach((record) => { - if (record.files === undefined) return; - - let recordFiles = record.files; - if (recordFiles.length === undefined) recordFiles = [recordFiles]; - files.push(...recordFiles); - - // set score for the word in each file - recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, new Map()); - const fileScores = scoreMap.get(file); - fileScores.set(word, record.score); - }); - }); - - // create the mapping - files.forEach((file) => { - if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) - fileMap.get(file).push(word); - }); - }); - - // now check if the files don't contain excluded terms - const results = []; - for (const [file, wordList] of fileMap) { - // check if all requirements are matched - - // as search terms with length < 3 are discarded - const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2, - ).length; - if ( - wordList.length !== searchTerms.size - && wordList.length !== filteredTermCount - ) - continue; - - // ensure that none of the excluded terms is in the search result - if ( - [...excludedTerms].some( - (term) => - terms[term] === file - || titleTerms[term] === file - || (terms[term] || []).includes(file) - || (titleTerms[term] || []).includes(file), - ) - ) - break; - - // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); - // add result to the result list - results.push([ - docNames[file], - titles[file], - "", - null, - score, - filenames[file], - SearchResultKind.text, - ]); - } - return results; - }, - - /** - * helper function to return a node containing the - * search summary for a given text. keywords is a list - * of stemmed words. - */ - makeSearchSummary: (htmlText, keywords, anchor) => { - const text = Search.htmlToText(htmlText, anchor); - if (text === "") return null; - - const textLower = text.toLowerCase(); - const actualStartPosition = [...keywords] - .map((k) => textLower.indexOf(k.toLowerCase())) - .filter((i) => i > -1) - .slice(-1)[0]; - const startWithContext = Math.max(actualStartPosition - 120, 0); - - const top = startWithContext === 0 ? "" : "..."; - const tail = startWithContext + 240 < text.length ? "..." : ""; - - let summary = document.createElement("p"); - summary.classList.add("context"); - summary.textContent = - top + text.substr(startWithContext, 240).trim() + tail; - - return summary; - }, -}; - -_ready(Search.init); diff --git a/docs/build/html/_static/sphinx_highlight.js b/docs/build/html/_static/sphinx_highlight.js deleted file mode 100644 index a74e103..0000000 --- a/docs/build/html/_static/sphinx_highlight.js +++ /dev/null @@ -1,159 +0,0 @@ -/* Highlighting utilities for Sphinx HTML documentation. */ -"use strict"; - -const SPHINX_HIGHLIGHT_ENABLED = true; - -/** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 - && !parent.classList.contains(className) - && !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore(span, parent.insertBefore(rest, node.nextSibling)); - node.nodeValue = val.substr(0, pos); - /* There may be more occurrences of search term in this node. So call this - * function recursively on the remaining fragment. - */ - _highlight(rest, addItems, text, className); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect", - ); - const bbox = parent.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute("class", className); - addItems.push({ parent: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target), - ); -}; - -/** - * Small JavaScript module for the documentation. - */ -const SphinxHighlight = { - /** - * highlight the search words provided in localstorage in the text - */ - highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight - - // get and clear terms from localstorage - const url = new URL(window.location); - const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms"); - // Update history only if '?highlight' is present; otherwise it - // clears text fragments (not set in window.location by the browser) - if (url.searchParams.has("highlight")) { - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - } - - // get individual terms from highlight string - const terms = highlight - .toLowerCase() - .split(/\s+/) - .filter((x) => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '", - ), - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms"); - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) - return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) - return; - if ( - DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - && event.key === "Escape" - ) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); diff --git a/docs/build/html/api.html b/docs/build/html/api.html deleted file mode 100644 index 4e39993..0000000 --- a/docs/build/html/api.html +++ /dev/null @@ -1,3781 +0,0 @@ - - - - - - - - - API Reference — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

API Reference

-

The API reference is generated automatically from the package source.

-
-

Core

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis.core.optimize(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Do various optimizations with all simulations available in -‘easypairspin()’.

-
-
Parameters:
-
    -
  • Sys (object) – Spinsystem object of module ‘epr_setup’.

  • -
  • Exp (object) – Experimental object of module ‘epr_setup’.

  • -
  • SimOpt (object) – SimulationOptions object of module ‘epr_setup’.

  • -
  • FitOpt (object) – FittingOptions object of module ‘epr_setup’.

  • -
  • Var (object) – Variation object of module ‘epr_setup’.

  • -
-
-
Raises:
-

ValueError – Will be raised, if the given simulation routine in FitOpt.routine is an - invalid string.

-
-
Returns:
-

best_Spinsystem – The best Spinsystem found during optimization. Object is of class -Spinsystem of module ‘epr_setup’.

-
-
Return type:
-

object

-
-
-

Examples

-

Basic optimization using genetic_radpair and static_radical_pair:

-
>>> from epr_setup import Spinsystem, Experimental, SimulationOptions,
-... FittingOptions, Variation
->>> Sys = Spinsystem()
->>> Exp = Experimental()
->>> SimOpt = SimulationOptions()
->>> SimOpt.routine = 'static_radpair'
->>> FitOpt = FittingOptions()
->>> Var = Variation()
->>> Var.g1 = np.array([0.001, 0.003, 0.002])
->>> best_Sys = optimize(Sys, Exp, SimOpt, FitOpt, Var)
-
-
-

Plot your result:

-
>>> from plotting import plot_2D
->>> simulate(best_Sys, Exp, SimOpt)
->>> plot_2D(Exp.B_z , Exp.spec_sim)
-
-
-
- -
-
-spinanalysis.core.simulate(Sys, Exp, SimOpt)[source]
-

Do various simulations with (spinpolarized) radical pairs.

-
-
Parameters:
-
    -
  • Sys (object) – Spinsystem object of module ‘epr_setup’.

  • -
  • Exp (object) – Experimental object of module ‘epr_setup’. simulated_spectra will be -saved in Exp.spec_sim.

  • -
  • SimOpt (object) – SimulationOptions object of module ‘epr_setup’.

  • -
-
-
Raises:
-

ValueError – Will be raised, if the given simulation routine in SimOpt.routine is an - invalid string.

-
-
Returns:
-

simulated_spectra – Simulated spectra as a numpy.ndarray.

-
-
Return type:
-

ndarray

-
-
-

Examples

-

Basic simulation using static_radical_pair:

-
>>> from epr_setup import Spinsystem, Experimental, SimulationOptions
->>> Sys = Spinsystem()
->>> Exp = Experimental()
->>> SimOpt = SimulationOptions()
->>> SimOpt.routine = 'static_radpair'
->>> simulate(Sys, Exp, SimOpt)
-
-
-

Plot your result:

-
>>> from plotting import plot_2D
->>> plot_2D(Exp.B_z , Exp.spec_sim)
-
-
-
- -
-
-spinanalysis.core.start_log()[source]
-

Diese Funktion soll mal Log-Dateien während der Nutzung anlegen. -Aktuell nicht nutzbar! Reine Baustelle!

-
-
Return type:
-

None

-
-
-
- -
-
-

EPR

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-class spinanalysis.epr.EPR_Parameters[source]
-

Bases: object

-

A class containing all parameters for various radical pair simulations.

-
-
-g1
-

g-Tensor of electron 1.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g2
-

g-Tensor of electron 2.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g_tri
-

g-Tensor of a triplet radical.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g
-

g-Tensor of a radical.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A1
-

A-Tensor of nuclei 1 in Megahertz.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A2
-

A-Tensor of nuclei 2 in Megahertz.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A3
-

A-Tensor of nuclei 3 in Megahertz.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A4
-

A-Tensor of nuclei 4 in Megahertz.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A5
-

A-Tensor of nuclei 5 in Megahertz.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A_eseem
-

Hyperfine coupling for the nuclei in OOP-ESEEM in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-omega_I
-

Nuclei frequency in OOP-ESEEM in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-D
-

Zero field splitting parameter D in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-D_tri
-

Zero field splitting parameter D of a triplet in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-E
-

Zero field splitting parameter E in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-E_tri
-

Zero field splitting parameter E of a triplet in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-beta
-

Decay rate of the exchange coupling in Å^-1.

-
-
Type:
-

float64

-
-
-
- -
-
-J_0
-

Zero distance exchange coupling constant in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-J_ex
-

Exchange coupling in Megahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-g1_frame
-

Orientation of electron spin 1 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g2_frame
-

Orientation of electron spin 2 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g_tri_frame
-

Orientation of g_tri in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-g_frame
-

Orientation of g in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A1_frame
-

Orientation of nuclei spin 1 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A2_frame
-

Orientation of nuclei spin 2 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A3_frame
-

Orientation of nuclei spin 3 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A4_frame
-

Orientation of nuclei spin 4 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-A5_frame
-

Orientation of nuclei spin 5 in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-D_frame
-

Orientation of dipol coupling in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-D_tri_frame
-

Orientation of triplet dipol coupling in radian.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-n1
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I1
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n2
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I2
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n3
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I3
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n4
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I4
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n5
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I5
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-width_gauss
-

Gaussian linewidth in mT.

-
-
Type:
-

float64

-
-
-
- -
-
-T_relax_1
-

Longitudinal relaxation time in s.

-
-
Type:
-

float64

-
-
-
- -
-
-T_relax_2
-

Transversal relaxation time in s.

-
-
Type:
-

float64

-
-
-
- -
-
-decay
-

Exponential decay time for hilbert space simulations in s.

-
-
Type:
-

float64

-
-
-
- -
-
-T_pm
-

Phase memory time (OOP ESEEM).

-
-
Type:
-

float64

-
-
-
- -
-
-population
-

Populations of the initial density matrix of a triplet precursor in -zero field.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-amplitude
-

Amplitude of the spectra for OOP-ESEEM.

-
-
Type:
-

float64

-
-
-
- -
- -
-
-class spinanalysis.epr.Experimental(magnetic_field=None, real_int=None, imag_int=None, cmplx_int=None, time_axis=None, rescale=True)[source]
-

Bases: object

-

A class containing all experimental parameters and data.

-
-
-B_z
-

External magnetic field points in mT used for simulation. Conversions -allowed.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-freq_mw
-

Frequency of induced microwave radiation in Gigahertz.

-
-
Type:
-

float64

-
-
-
- -
-
-magnetic_field
-

Same as B_z, but will never be changed.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-int
-

Real and imaginary part of the measured intensities. 1d or 2d.

-
-
Type:
-

np.array, np.complex128

-
-
-
- -
-
-time_axis
-

Contains all experimental time points.

-
-
Type:
-

1d-Array, optional

-
-
-
- -
-
-spec_sim
-

Calculated spectrum. At initialisation empty.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-get_linear_time_axis()[source]
-

Get a linear time axis using the given boundaries from self.t_scale -with self.t_points points.

-
- -
-
-get_linear_time_axis(t_min=None, t_max=None, t_points=None)[source]
-

Get a linear timea axis for transient simulations.

-

Get a linear time axis using the given boundaries from self.t_scale -with self.t_points points.

-
-
Parameters:
-
    -
  • t_min (float) – Left boundary of the time axis. If None is given, the current value -of self.t_scale[0] will be used. Else, the value of self.t_scale[0] -will be replaced. The default is None.

  • -
  • t_max (float) – Right boundary of the time axis. If None is given, the current -value of self.t_scale[1] will be used. Else, the value of -self.t_scale[1] will be replaced. The default is None.

  • -
  • t_points (int) – Number of time points. If None is given, the current value of -self.t_points will be used. Else, the value of self.t_points will -be replaced. The default is None.

  • -
-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-class spinanalysis.epr.FittingOptions[source]
-

Bases: object

-

A class containing all optimization options.

-
-
-routine
-

Name of the optimization routine which will be used by -easypairspin_optimize().

-
-
Type:
-

str

-
-
-
- -
-
-method
-

Name of the optimization method used in the scipy.optimize routines.

-
-
Type:
-

str

-
-
-
- -
-
-x0
-

Array containing the initial guess for the optimization routine for -the parameters which will be varied.

-
-
Type:
-

numpy.array, np.float64

-
-
-
- -
-
-cpu_cores
-

Number of cores used for the optimization.

-
-
Type:
-

int

-
-
-
- -
-
-gui
-

Set to True if in GUI mode (PySpin). The default is False.

-
-
Type:
-

bool

-
-
-
- -
-
-window
-

Plot canvas. Only needed in GUI mode.

-
-
Type:
-

object

-
-
-
- -
-
-load(profile_name: str)[source]
-

Load FittingOptions values from a config file (profile_name.ini).

-

Recommended

-
- -
-
-save(profile_name: str)[source]
-

Save the current fitting options as a config file (profile_name.ini).

-

Recommended

-
- -

Examples

-

Initialize an object of class <FittingOptions>:

-
-
>>> FitOpt = FittingOptions()
->>> FitOpt.GAVaPS
-True
-
-
-

Change values:

-
>>> FitOpt.GAVaPS = False
->>> FitOpt.GAVaPS
-False
-
-
-

Save your current values as a new profile:

-
>>> FitOpt.save_simulationoptions('FitOpt_prof_1')
->>> FitOpt_2 = FittingOptions()
->>> FitOpt_2.GAVaPS
-True
->>> FitOpt.load_profile('FitOpt_prof_1')
->>> FitOpt_2.GAVaPS
-False
-
-
-

You can also create a fitting options profile from an empty template:

-
>>> fitopt_prof = profiles.new_optimization_profile()
->>> fitopt_prof['genetic']['GAVaPS'] = False
->>> profiles.add_profile(fitopt_prof, 'simulation', 'FitOpt_prof_2')
->>> FitOpt_3 = FittingOptions()
->>> FitOpt_3.GAVaPS
-True
->>> FitOpt.load_profile('FitOpt_prof_2')
->>> FitOpt_3.GAVaPS
-False
-
-
-
-
-
-load(profile_name)[source]
-

Load fitting options from a profile.

-

Load the settings from [sys.prefix]/easypairspin/profiles/optimization/ -[profile_name].ini into the FittingOptions object. Overwrites previous -settings. Only loads the section given in [‘main’][‘routine’].

-
-
Parameters:
-

profile_name (str) – Name of the optimization profile which will be loaded.

-
-
Return type:
-

None

-
-
-
- -
-
-save(profile_name='')[source]
-

Save the fitting options as a profile.

-

Save the fitting options as a fitting options profile using -profile_management.add_profile(). Load the fitting options using -FittingOptions.load_profile(<profile_name>).

-
-
Parameters:
-

profile_name (str) – Name of the profile. If no profile name is given, a default one -will be generated by get_profile_name(). The default is ‘’.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-class spinanalysis.epr.SimulationOptions[source]
-

Bases: object

-

A class containing all simulation options.

-
-
-routine
-

Name of the simulation routine which will be used by easypairspin() and -easypairspin_optimize().

-
-
Type:
-

str

-
-
-
- -
-
-grid_points
-

Number of points used for spherical grid.

-
-
Type:
-

int

-
-
-
- -
-
-space
-

Name of the mathematical space used for some calculations.

-
-
Type:
-

str

-
-
-
- -
-
-pop_evolution
-

If set to True, the population evolution in calculated using teacups.

-
-
Type:
-

boolean

-
-
-
- -
-
-eigval_mode
-

If set to True, only the eigenvalues of the system are calculated using -teacups.

-
-
Type:
-

boolean

-
-
-
- -
-
-force_cpu
-

If True, the simulation will be executed on the CPU, even if GPU is -available. Default is False.

-
-
Type:
-

boolean

-
-
-
- -
-
-regularization_mode
-

Choose the regularization matrix used for the Tikhonov-Regularization. -0 : Unitary matrix -1 : First order derivative matrix -2 : Second order derivative matrix (default)

-
-
Type:
-

int

-
-
-
- -
-
-load(profile_name: str)[source]
-

Load SimulationOptions values from a config file (profile_name.ini).

-

Recommended

-
- -
-
-save(profile_name: str)[source]
-

Save the current simulation options as a config file -(profile_name.ini).

-
- -

Examples

-

Initialize an object of class <SimulationOptions>:

-
>>> SimOpt = SimulationOptions()
->>> SimOpt.grid_points
-500
-
-
-

Change values:

-
>>> SimOpt.grid_points = 1000
->>> SimOpt.grid_points
-1000
-
-
-

Save your current values as a new profile:

-
>>> SimOpt.save('SimOpt_prof_1')
->>> SimOpt_2 = SimulationOptions()
->>> SimOpt_2.grid_points
-500
->>> SimOpt.load('SimOpt_prof_1')
->>> SimOpt_2.grid_points
-1000
-
-
-

You can also create a simulation options profile from an empty template:

-
>>> simopt_prof = profiles.new_simulation_profile()
->>> simopt_prof['static_radpair']['grid_points'] = 1100
->>> profiles.add_profile(simopt_prof, 'simulation', 'SimOpt_prof_2')
->>> SimOpt_3 = SimulationOptions()
->>> SimOpt_3.grid_points
-500
->>> SimOpt.load('SimOpt_prof_2')
->>> SimOpt_3.grid_points
-1100
-
-
-
-
-load(profile_name)[source]
-

Load simulation options from a profile.

-

Load the settings from [sys.prefix]/easypairspin/profiles/simulation/ -[profile_name].ini into the SimulationOptions object. Overwrites -previous settings.

-
-
Parameters:
-

profile_name (str) – Name of the simulation profile which will be loaded.

-
-
Return type:
-

None

-
-
-
- -
-
-save(profile_name='')[source]
-

Save the simulation optionas as a profile.

-

Save the simulation options as a simulation options profile using -profile_management.add_profile(). Load the simulation options using -SimulationOptions.load_profile(<profile_name>).

-
-
Parameters:
-

profile_name (str) – Name of the profile. If no profile name is given, a default one -will be generated by get_profile_name(). The default is ‘’.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-class spinanalysis.epr.Spinsystem[source]
-

Bases: EPR_Parameters

-

A class containing all parameters for various radical pair simulations.

-
-
-g1_iso
-

Isotropic g value of electron 1.

-
-
Type:
-

float64

-
-
-
- -
-
-g2_iso
-

Isotropic g value of electron 2.

-
-
Type:
-

float64

-
-
-
- -
-
-n1
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I1
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n2
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I2
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n3
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I3
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n4
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I4
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-n5
-

Number of chemically equivalent atoms.

-
-
Type:
-

int

-
-
-
- -
-
-I5
-

Corresponding nuclear spin.

-
-
Type:
-

float64

-
-
-
- -
-
-donor_list
-

Defines which atom groups are donor groups.

-
-
Type:
-

np.array

-
-
-
- -
-
-acceptor_list
-

Defines which atom groups are acceptor groups.

-
-
Type:
-

np.array

-
-
-
- -
-
-frame_group_i
-

Define a frame_group which will be used in optimization mode. Each -frame group contains the names of the angle lists which always will -have same values during optimization. i is a variable and can be -whatever you want. You can define as many frame groups as you want. An -example frame group would be: frame_group_1 = [‘A1’, ‘A2’, ‘D’]. This -list means, that A2_frame and D_frame will always have the same values -as A1_frame, no matter which values were given to them.

-
-
Type:
-

list

-
-
-
- -
-
-spin_system
-

Define the spin system by one out of: “rp” (radical pair), “doub” -(doublet), “trip” (triplet), “tdp” (triplet-doublet pair).

-
-
Type:
-

str

-
-
-
- -
-
-precursor
-

State of the precursor. One out of: “zf”, “eigen”, “singlet”, -“triplet-zf”, “triplet-eigen”, “coupled”, “basis”.

-
-
Type:
-

str

-
-
-
- -
-
-dynamics
-

Matrix with rate constants of relaxation process in 1/s. For further -information see the documentation.

-
-
Type:
-

np.array

-
-
-
- -
-
-distribution_order
-

Number of Gaussians used for Multi-Gauss-Fitting.

-
-
Type:
-

int

-
-
-
- -
-
-distribution
-

Distance distribution of the radical pair.

-
-
Type:
-

np.array

-
-
-
- -
-
-load(profile_name: str)[source]
-

Load Spinsystem values from a config file (profile_name.ini).

-

Recommended

-
- -
-
-save(profile_name: str)[source]
-

Save the current spinsystem values as a config file (profile_name.ini).

-

Recommended

-
- -
-
-_get_g_iso()[source]
-

Determine both g_iso values. Needed in simulation.

-
- -

Examples

-

Initialize a new object of class <Spinsystem>:

-
>>> Sys = Spinsystem()
->>> Sys.g1
-np.array([2.002, 2.002, 2.002])
->>> Sys.g1_iso
-2.002
-
-
-

Change values:

-
>>> Sys.g1 = np.array([2.0024, 2.00381, 2.0027])
->>> Sys.get_g_iso()
->>> Sys.g1
-np.array([2.0024 , 2.00381, 2.0027 ])
->>> Sys.g1_iso
-2.00297
-
-
-

Create a new spinsystem profile from an empty template and load it:

-
>>> Sys_profile = profiles.new_spinsystem_profile()
->>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] #  use list not array!
->>> profiles.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1')
->>> Sys_2 = Spinsystem()
->>> Sys_2.load_profile('Sys_prof_1')
->>> Sys_2.g1
-np.array([2.0034 , 2.00156, 2.00228])
-
-
-

You can also save your current spinsystem as a new profile:

-
>>> Sys_3 = Spinsystem()
->>> Sys_3.g1 = np.array([1, 2, 3]) #  either array or list
->>> Sys_3.save('Sys_prof_2')
->>> Sys_4 = Spinsystem()
->>> Sys_4.load('Sys_prof_2')
->>> Sys_4.g1
-np.array([1., 2., 3.])
-
-
-
-
-load(profile_name, degree=False)[source]
-

Load a spinsystem from a profile.

-

Load the settings from [sys.prefix]/easypairspin/profiles/spinsystem/ -[profile_name].ini into the Spinsystem object. Overwrites previous -settings.

-
-
Parameters:
-
    -
  • profile_name (str) – Name of the Spinsystem profile which will be loaded.

  • -
  • degree (bool) – If True, the angle values in the profile are given in degree not -radian. Thus, they will be converted to radian. If False, the -angles are given in radian and will therefore not be converted. The -default is ‘False’.

  • -
-
-
Return type:
-

None

-
-
-
- -
-
-save(profile_name='')[source]
-

Save the spinsystem as a profile.

-

Save the spinsystem object as a spinsystem profile using -profile_management.add_profile(). Load the spinsystem using -Spinsystem.load_profile(<profile_name>).

-
-
Parameters:
-

profile_name (str) – Name of the profile. If no profile name is given, a default one -will be generated by get_profile_name(). The default is ‘’.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-class spinanalysis.epr.Variation[source]
-

Bases: EPR_Parameters

-

A class containing variation ranges for all possible parameters.

-
-
-needed_digits
-

Number of needed digits for chromosomes.

-
-
Type:
-

int

-
-
-
- -
-
-number_of_genes
-

Number of varied parameters. Used for fp representation.

-
-
Type:
-

int

-
-
-
- -
-
-variation_array
-

Array with all variation ranges greater 0.

-
-
Type:
-

1d-Array, np.float64

-
-
-
- -
-
-boundaries
-

Sequence of tuples containing upper and lower bounds for all varied -parameters. Used for scipy.optimize.

-
-
Type:
-

list

-
-
-
- -
-
-freq_mw
-

Frequency of induced microwave radiation in Hertz.

-
-
Type:
-

float64

-
-
-
- -
-
-bohr_magneton
-

Bohr magneton in Hertz/Tesla.

-
-
Type:
-

float64

-
-
-
- -
-
-load(profile_name: str)[source]
-

Load Spinsystem values from a config file (profile_name.ini).

-

Recommended

-
- -
-
-save(profile_name: str)[source]
-

Save the current variation values as a config file (profile_name.ini).

-

Recommended

-
- -
-
-get_digits_for_one_par(Par, digits_per_True, one_par=True)
-

Get the number of needed digits for one varied parameter.

-
- -
-
-get_needed_digits()[source]
-

Get the total number of needed digits in binary mode.

-
- -
-
-get_number_of_genes()[source]
-

Get the total number of genes. Is equal to the number of varied -parameters.

-
- -
-
-get_variation_array()[source]
-

Create an array with all used variation ranges. Only used in floating -point representation.

-
- -
-
-update_digits()[source]
-

Determine number of needed digits. Just calls get_needed_digits()

-
- -
-
-get_boundaries(Sys)[source]
-

Get a sequence of tuples containing the boundaries for the varied -parameters.

-
- -

Examples

-

Initialize a new object of class <Variation>:

-
>>> Var = Variation()
->>> Var.g1
-np.array([0., 0., 0.])
-
-
-

Change values:

-
>>> Var.g1 = np.array([0.003, 0.004, 0.003])
->>> Var.g1
-np.array([0.003, 0.004, 0.003])
-
-
-

Create a new variation profile from an empty template and load it:

-
>>> Var_profile = profiles.new_variation_profile()
->>> Var_profile['g_1'] = [0.001, 0.007, 0.003] #  use list not array!
->>> profiles.add_profile(Var.profile, 'variation', 'Var_prof_1')
->>> Var_2 = Variation()
->>> Var_2.load_profile('Var_prof_1')
->>> Var_2.g1
-np.array([0.001, 0.007, 0.003])
-
-
-

You can also save your current variation object as a new profile:

-
>>> Var_3 = Variation()
->>> Var_3.g1 = np.array([1, 2, 3]) #  either array or list
->>> Var_3.save('Var_prof_2')
->>> Var_4 = Variation()
->>> Var_4.load('Var_prof_2')
->>> Var_4.g1
-np.array([1., 2., 3.])
-
-
-
-
-get_boundaries(Sys)[source]
-

Create a sequence of pairs with all bounds for the varied parameters.

-

Used for the scipy optimization routines.

-
-
Parameters:
-

Sys (object) – Spinsystem object.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-get_needed_digits()[source]
-

Get sum of needed digits for all varied parameters.

-
-
Return type:
-

None

-
-
-
- -
-
-get_number_of_genes()[source]
-

Determine number of parameters which get varied.

-
-
Return type:
-

None

-
-
-
- -
-
-get_variation_array()[source]
-

Put all variation ranges in one 1d-Array.

-
-
Return type:
-

None

-
-
-
- -
-
-load(profile_name, degree=False)[source]
-

Laod a variation object from a profile.

-

Load the settings from [sys.prefix]/easypairspin/profiles/variation/ -[profile_name].ini into the Variation object. Overwrites previous -settings.

-
-
Parameters:
-
    -
  • profile_name (str) – Name of the Variation profile which will be loaded.

  • -
  • degree (bool) – If True, the angle values in the profile are given in degree not -radian. Thus, they will be converted to radian. If False, the -angles are given in radian and will therefore not be converted. The -default is ‘False’.

  • -
-
-
Return type:
-

None

-
-
-
- -
-
-save(profile_name='')[source]
-

Save a variation object as a profile.

-

Save the variation object as a variation profile using -profile_management.add_profile(). Load the variation object using -Variation.load_profile(<profile_name>).

-
-
Parameters:
-

profile_name (str) – Name of the profile. If no profile name is given, a default one -will be generated by get_profile_name(). The default is ‘’.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-update_digits()[source]
-

Update number of needed_digits.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-

Loading

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis.loading.convert_parameter_type(value)[source]
-

Convert the type of a given string to bool, int or float if possible.

-
-
Parameters:
-

value (str) – Parameter string which should be converted.

-
-
Returns:
-

value – Parameter as bool, int or float. If none is possible, the stripped -string will be returned.

-
-
Return type:
-

Tuple[bool, int, float, str]

-
-
-
- -
-
-spinanalysis.loading.get_DSC_parameters(path_to_folder)[source]
-

Extract all parameters from the DSC File.

-
-
Parameters:
-

path_to_folder (str) – Full path to the folder with .DSC and .DTA file. Files and path must -have the same basename (BRUKER STANDARD).

-
-
Returns:
-

DSC_parameters – Dictionary with all parameters. Key is the same as in .DSC.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.loading.get_byte_mode(DSC_dict, data_key='IRFMT')[source]
-

Get the used byte mode of the BRUKER BES3T-data.

-

For more information about BES3T go to BRUKER website or easyspin @ github.

-
-
Parameters:
-
    -
  • DSC_dict (dict) – Dictionary with all parameters. Key is the same as in .DSC.

  • -
  • data_key (str) – Key for the data array. Either ‘IRFMT’ or ‘IIFMT’. -The default is ‘IRFMT’.

  • -
-
-
Raises:
-

ValueError – Raised if key value is not C, S, I, F or D.

-
-
Returns:
-

byte_mode – Return the used byte mode as one string for -numpy.fromfile(dtype=byte_mode).

-
-
Return type:
-

str

-
-
-
- -
-
-spinanalysis.loading.get_data_dimension(DSC_dict)[source]
-

Get the dimension of the measured spectrum (1d/2d).

-
-
Parameters:
-

DSC_dict (dict) – Dictionary with all parameters. Key is the same as in .DSC.

-
-
Returns:
-

The dimension will be safed in the given dictionary to the key -‘dimensions’.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis.loading.get_full_path(directory_name, start_directory=None)[source]
-

Get the full path of a given directory. Search starts at home directory.

-
-
Parameters:
-
    -
  • directory_name (str) – Name of the directory whose path is to be found.

  • -
  • start_directory (str) –

    Directory at which the search starts. If given, the perfomance -increases very sharply. The default is None.

    -

    Recommended.

    -

  • -
-
-
Returns:
-

full_path – The full path of the directory.

-
-
Return type:
-

str

-
-
-
- -
-
-spinanalysis.loading.get_transient_data(fpath)[source]
-

Get the measured intensities of the whole spectrum from a transient epr -experiment measured by ESP380E.

-
-
Parameters:
-

fpath (str) – Full path to the folder with the experimental data.

-
-
Return type:
-

Tuple[tuple, array]

-
-
Returns:
-

    -
  • axis (tuple) – Return a tuple with all axis vectors as two numpy.arrays -(magnetic_field, time).

  • -
  • cmplx_data_vector (np.array, np.complex128) – Measured real intensities. All imaginary parts are zero.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.get_transient_info(fpath)[source]
-

Get all informations about the time axis and magnetic field vector from the -.info file.

-
-
Parameters:
-

fpath (str) – Full path to the folder with the experimental data.

-
-
Raises:
-

KeyError – Raised if section ‘MAGNETIC FIELD’ or ‘TRANSIENT’ could not be found in - .info file. Take care that the sections must be all upper case.

-
-
Returns:
-

transient_info – Contains the needed informations in the following order (time_length, -time_points, mag_field_start, mag_field_stop, mag_field_step).

-
-
Return type:
-

tuple

-
-
-
- -
-
-spinanalysis.loading.load_axis_vector(axis, DSC_dict)[source]
-

Load the points of a given axis (x, y, z).

-
-
Parameters:
-
    -
  • axis (str) – Name of the axis. Needs to start with x,X,y,Y or z,Z. E. g.: x-axis.

  • -
  • DSC_dict (dict) – Dictionary with all parameters. Key is the same as in .DSC.

  • -
-
-
Raises:
-

ValueError – Will be raised if the given axis doesn’t starts with an allowed - character.

-
-
Returns:
-

axis_vector – Array with all axis points.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.loading.load_data_vector(DSC_dict)[source]
-

Load the binary intensity vector(s) from BRUKER BES3T-File.

-
-
Parameters:
-

DSC_dict (dict) – Dictionary with all parameters. Key is the same as in .DSC.

-
-
Raises:
-
    -
  • ValueError – Will be raised if IKKF isn’t CPLX or REAL or if the dimension isn’t 1 - or 2.

  • -
  • KeyError – Raised if an axis is given but not the corresponding number of points.

  • -
-
-
Returns:
-

cmplx_data_vector – Real and imaginary part of the measured intensities. If no imaginary -part is measured, zeroes will be inserted.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.loading.load_epr_ESP_transient(folder, start_directory=None)[source]
-

Load data from a transient epr experiment measured with ESP380E.

-
-
Parameters:
-
    -
  • folder (str) – Name of the folder with all data files. Data files need to have the -same basename as folder, e. g. ‘/home/user/exp_1/exp_1.001’.

  • -
  • start_directory (str) –

    Give the path starting from your home folder which the search for -the data folder should start at. The default is None. -E. g.: data is at /home/cooluser/nice/data/this_folder -and you know, that all your data is in /home/cooluser/nice/*, then you -can give this start_directory with start_directory=’nice’. So -the function call would be:

    -
    -

    load_epr_ESP_transient(this_folder, ‘nice’)

    -
    -
    -
    Recommended:

    Without this parameter, the search for the right folder will be -much longer.

    -
    -
    -

  • -
-
-
Return type:
-

Tuple[tuple, array]

-
-
Returns:
-

    -
  • axis (tuple) – Return a tuple with all axis vectors as two numpy.arrays -(magnetic_field, time).

  • -
  • data (np.array, np.complex128) – All intensity values as one complex numpy.array. Imaginary part is -always 0.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.load_epr_bruker_bes3t(folder, start_directory=None)[source]
-

Load the whole dataset from BRUKER BES3T data folder into numpy.arrays.

-

Time axis will be rescaled for OOP-ESEEM experiments, if ‘FTAcqModeSlct’ -is ‘Run from Tables’.

-
-
Parameters:
-
    -
  • folder (str) – Name of the data folder with the corresponding data files.

  • -
  • start_directory (str) –

    Give the path starting from your home folder which the search for the -data folder should start at. The default is None. -E. g.: data is at /home/cooluser/nice/data/this_folder -and you know, that all your data is in /home/cooluser/nice/*, then you -can give this start_directory with start_directory=’nice’. So -the function call would be:

    -
    -

    load_epr_bruker_bes3t(this_folder, ‘nice’)

    -
    -
    -
    Recommended:

    Without this parameter, the search for the right folder will be -much longer.

    -
    -
    -

  • -
-
-
Return type:
-

Tuple[tuple, array]

-
-
Returns:
-

    -
  • axis (tuple) – Tuple of all axis vectors as three numpy.arrays (x, y, z).

  • -
  • data (np.array, np.complex128) – All intensity values as one complex numpy.array.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.load_matlab(folder, start_directory=None, field='field', signal='signal')[source]
-

Load EPR data from a matlab data file .mat

-
-
Parameters:
-
    -
  • folder (str) – Name of the data folder with the corresponding data files.

  • -
  • start_directory (str) –

    Give the path starting from your home folder which the search for -the data folder should start at. The default is None. -E. g.: data is at /home/cooluser/nice/data/this_folder -and you know, that all your data is in /home/cooluser/nice/*, then you -can give this start_directory with start_directory=’cooluser/nice’. So -the function call would be:

    -
    -

    load_simulated_data(this_folder, ‘cooluser/nice’)

    -
    -

  • -
  • field (str) – Name of the field array. The default is ‘field’.

  • -
  • signal (str) – Name of the signal array. The default is ‘signal’.

  • -
-
-
Return type:
-

Tuple[array, array, array]

-
-
Returns:
-

    -
  • axis (np.array) – Tuple of np.array containing the x and y axis.

  • -
  • data (np.array) – np.array with the measured intensities.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.load_simulated_data(folder, start_directory=None)[source]
-

Load the simulated data from EasyPairSpin or data saved with -saving.save_simulation(). Using numpy.loadtxt().

-
-
Parameters:
-
    -
  • folder (str) – Name of the data folder with the corresponding data files.

  • -
  • start_directory (str) –

    Give the path starting from your home folder which the search for -the data folder should start at. The default is None. -E. g.: data is at /home/cooluser/nice/data/this_folder -and you know, that all your data is in /home/cooluser/nice/*, then you -can give this start_directory with start_directory=’cooluser/nice’. So -the function call would be:

    -
    -

    load_simulated_data(this_folder, ‘cooluser/nice’)

    -
    -
    -
    Recommended:

    Without this parameter, the search for the right folder -will be much longer.

    -
    -
    -

  • -
-
-
Return type:
-

Tuple[array, array, array]

-
-
Returns:
-

    -
  • x (np.array) – Axis vector for the x-axis.

  • -
  • y (np.array) – Axis vector for the y-axis. Only returned, if the simulated data is 2d.

  • -
  • intensity (np.array, np.complex128) – Simulated intensities. Either 1d or 2d.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.load_txt(folder, start_directory=None)[source]
-

Load EPR data from a .txt file.

-
-
Parameters:
-
    -
  • folder (str) – Name of the data folder with the corresponding data files.

  • -
  • start_directory (str) –

    Give the path starting from your home folder which the search for -the data folder should start at. The default is ‘’. -E. g.: data is at /home/cooluser/nice/data/this_folder -and you know, that all your data is in /home/cooluser/nice/*, then you -can give this start_directory with start_directory=’cooluser/nice’. So -the function call would be:

    -
    -

    load_simulated_data(this_folder, ‘cooluser/nice’)

    -
    -

  • -
-
-
Return type:
-

Tuple[array, array, array]

-
-
Returns:
-

    -
  • axis (np.array) – Tuple of np.array containing the x and y axis.

  • -
  • data (np.array) – np.array with the measured intensities.

  • -
-

-
-
-
- -
-
-spinanalysis.loading.read_single_transient_file(fpath, filenumber, digits, time=False)[source]
-

Get the measured intensities of a single field point from a transient epr -experiment measured by ESP380E.

-
-
Parameters:
-
    -
  • fpath (str) – Full path to the folder with the experimental data.

  • -
  • filenumber (int) – Number of the dataset for the magnetic field point e. g. 003.

  • -
  • digits (int) – Number of digits from the highest filenumber (ESP380E has an increasing -number as fileextension for each magnetic field point e. g. data.001).

  • -
  • time (bool) – If True, the time axis will be returned.

  • -
-
-
Return type:
-

Tuple[float, array]

-
-
Returns:
-

    -
  • field (float) – Magnetic field point.

  • -
  • time_axis (np.array) – Linear time axis.

  • -
  • data_vector (np.array) – Measured intensities.

  • -
-

-
-
-
- -
-
-

Processing

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis.processing.background_corr(x, y, mode='biexp')[source]
-

Perform a background correction of measured data.

-

Available correction modes are: biexp, exp, lin, poly2, poly3 and poly4. -biexp and exp are using exponential models for the background. lin, poly2-4 -are polynominal models of first to fourth order. Default is ‘biexp’.

-
-

Warning

-

poly3 and poly4 can lead to overfitting!

-
-
-
Parameters:
-
    -
  • x (array) – x axis of the dataset.

  • -
  • y (array) – y data which will be background corrected.

  • -
  • mode (str) – Select the type of the background. The default is ‘biexp’.

  • -
-
-
Returns:
-

y_corr – Background corrected y data.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.biexp_fun(x, *coeff)[source]
-

Generalized biexponential function for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the biexponential function which will be fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.exp_fun(x, *coeff)[source]
-

Generalized monoexponential function for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the monoexponential function which will be fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.lin_fun(x, *coeff)[source]
-

Generalized linear function for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the linear function which will be fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.normalization(x, mode=None, dx=None)[source]
-

Normalize the given data.

-
-\[x_{\mathrm{norm}} = \frac{x_i - min(x)}{max(x)-min(x)}\]
-
-
Parameters:
-
    -
  • x (array) – Unnormalized data.

  • -
  • mode (str) – If ‘area’, the total AUC will be 1, if ‘value’, the maximum value will -be 1 or -1, else, the formula mentioned above will be used. Default is -None.

  • -
  • dx (float) – Distance between two points on the x axis. Only used for simpson -integration. Default is None.

  • -
-
-
Returns:
-

x_norm – Normalized data.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.poly2_fun(x, *coeff)[source]
-

Generalized polynominal function of degree 2 for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the polynominal function of degree 2 which will be -fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.poly3_fun(x, *coeff)[source]
-

Generalized polynominal function of degree 3 for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the polynominal function of degree 3 which will be -fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.poly4_fun(x, *coeff)[source]
-

Generalized polynominal function of degree 4 for background correction.

-
-
Parameters:
-
    -
  • x (array) – x values used to calculate corresponding y values.

  • -
  • *coeff (float) – Variables for the polynominal function of degree 4 which will be -fitted.

  • -
-
-
Returns:
-

y – Calculated y values.

-
-
Return type:
-

array

-
-
-
- -
-
-spinanalysis.processing.reconstruct(x, y)[source]
-

Reconstruction of a time signal use the Yule-Walker algorithm.

-
-
Parameters:
-
    -
  • x (array) – x axis.

  • -
  • y (array) – Intensities.

  • -
-
-
Return type:
-

array

-
-
Returns:
-

    -
  • x_new (np.array) – Reconstructed x axis.

  • -
  • y (np.array) – Reconstructed intensities.

  • -
-

-
-
-
- -
-
-spinanalysis.processing.reduce_offset(x)[source]
-

Eliminate the offset of the data by using the mean of the last quarter.

-
-
Parameters:
-

x (array) – Given data, e. g. measured intensities.

-
-
Returns:
-

x_shifted – Shifted data without offset.

-
-
Return type:
-

array

-
-
-
- -
-
-

Plotting

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-class spinanalysis.plotting.HiddenPrints[source]
-

Bases: object

-

Supress Error Messages in a context manager.

-
- -
-
-spinanalysis.plotting.heatmap(x, y, Z, mpl_stylesheet='default_stylesheet', ax=None, **kwargs)[source]
-

Plot 2D Data as a heatmap using matplotlib.pylab.pcolormesh().

-

The plot can be configured via plot profiles.

-
-
Parameters:
-
    -
  • x (array) – Array with values for the x axis.

  • -
  • y (array) – Array with values for the y axis.

  • -
  • Z (array) – 2D-Array with intensities.

  • -
  • mpl_stylesheet (str) – Name of the matplotlib style sheet (see: matplotlib documentation). -If no style sheet is given, the styles defined in the plotting profile -will be used. The default is None.

  • -
  • ax (object) – Axes object, used for the PySpin GUI.

  • -
  • **kwargs (optional) – Keyword arguments passed to the matplotlib plot function. Overrides the -arguments given in the stylesheet.

  • -
-
-
Returns:
-

fig – Figure object of matplotlib.pylab.

-
-
Return type:
-

object

-
-
-
- -
-
-spinanalysis.plotting.plot_2D(x, y, mpl_stylesheet='default_stylesheet', labels='no_label', ax=None, **kwargs)[source]
-

Plot the given y value(s) against the given x array.

-

Using matplotlib.pylab.plot(). The plot can be configured via plot -profiles.

-
-
Parameters:
-
    -
  • x (array) – Array with values for the x axis.

  • -
  • y (array) – 1D-Array or 2D-Array with values for y axis.

  • -
  • mpl_stylesheet (str) – Name of the matplotlib style sheet (see: matplotlib documentation). -If no style sheet is given, the styles defined in the plotting profile -will be used. The default is None.

  • -
  • labels (list[str]) – List of labels for the legend. If only one label is given, all labels -will be the same. The default is ‘no_label’.

  • -
  • ax (object) – Axes object, used for the PySpin GUI.

  • -
  • **kwargs (optional) – Keyword arguments passed to the matplotlib plot function. Overrides the -arguments given in the stylesheet.

  • -
-
-
Returns:
-

fig – Figure object of matplotlib.pylab.

-
-
Return type:
-

object

-
-
-
- -
-
-spinanalysis.plotting.plot_3D(x, y, Z, mpl_stylesheet='default_stylesheet', labels='no_label', ax=None, **kwargs)[source]
-

Plot 2D Data in 3D using matplotlib.pylab.plot_surface().

-

The plot can be configured via plot profiles.

-
-
Parameters:
-
    -
  • x (array) – Array with values for the x axis.

  • -
  • y (array) – Array with values for the y axis.

  • -
  • Z (array) – 2D-Array with intensities.

  • -
  • mpl_stylesheet (str) – Name of the matplotlib style sheet (see: matplotlib documentation). -If no style sheet is given, the styles defined in the plotting profile -will be used. The default is None.

  • -
  • labels (str) – At the moment no function. The default is ‘no_label’. # TODO

  • -
  • ax (object) – Axes object, used for the PySpin GUI.

  • -
  • **kwargs (optional) – Keyword arguments passed to the matplotlib plot function. Overrides the -arguments given in the stylesheet.

  • -
-
-
Returns:
-

fig – Figure object of matplotlib.pylab.

-
-
Return type:
-

object

-
-
-
- -
-
-spinanalysis.plotting.plot_3D_multiple_lines(x, y, Z, mpl_stylesheet='default_stylesheet', ax=None, **kwargs)[source]
-

Plot 2D Data in 3D using matplotlib.pylab.plot().

-

Each y trace as a single line plot. The plot can be configured via plot -profiles.

-
-
Parameters:
-
    -
  • x (array) – Array with values for the x axis.

  • -
  • y (array) – Array with values for the y axis.

  • -
  • Z (array) – 2D-Array with intensities.

  • -
  • mpl_stylesheet (str) – Name of the matplotlib style sheet (see: matplotlib documentation). -If no style sheet is given, the styles defined in the plotting profile -will be used. The default is None.

  • -
  • ax (object) – Axes object, used for the PySpin GUI.

  • -
  • **kwargs (optional) – Keyword arguments passed to the matplotlib plot function. Overrides the -arguments given in the stylesheet.

  • -
-
-
Returns:
-

fig – Figure object of matplotlib.pylab.

-
-
Return type:
-

object

-
-
-
- -
-
-spinanalysis.plotting.shifted_2D(x, Y, mpl_stylesheet='default_stylesheet', labels='no_label', ax=None, **kwargs)[source]
-

Plot multiples lines in 2D, shifted vertically.

-

The plot can be configured via plot profiles.

-
-
Parameters:
-
    -
  • x (array) – Array with values for the x axis.

  • -
  • Y (array) – 2D-Array with values for y axis.

  • -
  • mpl_stylesheet (str) – Name of the matplotlib style sheet (see: matplotlib documentation). -If no style sheet is given, the styles defined in the plotting profile -will be used. The default is None.

  • -
  • labels (list[str]) – List of labels for the legend. If only one label is given, all labels -will be the same. The default is ‘no_label’.

  • -
  • ax (object) – Axes object, used for the PySpin GUI.

  • -
  • **kwargs (optional) – Keyword arguments passed to the matplotlib plot function. Overrides the -arguments given in the stylesheet.

  • -
-
-
Returns:
-

fig – Figure object of matplotlib.pylab.

-
-
Return type:
-

object

-
-
-
- -
-
-

Saving

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis.saving.save_plot(fname, *figures, path=None, **kwargs)[source]
-

Save the figures plotted with matplotlib.

-
-
Parameters:
-
    -
  • fname (str) – Filename for the figure(s). If multiple figures are given, ‘_[number]’ -will be append to the filename.

  • -
  • *figures (object) – Matplotlib figure object(s).

  • -
  • path (str) – Absolute path where the figures will be stored. The default is -‘~/Results/Plots/’.

  • -
  • **kwargs (dict) – Other keyword arguments. Will be passed to plt.savefig(). See -matplotlib documentation for further informations.

  • -
-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis.saving.save_simulation(name, *data, path=None)[source]
-

Save the simulated data at ‘[path]/[name]/[files]’ using np.savetxt.

-
-
Parameters:
-
    -
  • name (str) – Foldername for the dataset.

  • -
  • *data (array) – Arrays with the simulated data. Must be 2d or 3d. If 2d: x_axis, int; -if 3d: x_axis, y_axis, int.

  • -
  • path (str) – Full path where the data will be stored. The default is -‘~/Results/Simulated Data/[name]’.

  • -
-
-
Raises:
-

ValueError – Will be raised, if dimension of the data isn’t 2d or 3d.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis.saving.write_out_file(Sys, Exp, SimOpt, *FitOpt, current_best=False)[source]
-

Write an output file with all datas from Sys, Exp, SimOpt and, if running -in optimization mode, FitOpt.

-
-
Parameters:
-
    -
  • Sys (object) – Spinsystem object of module ‘epr_setup’.

  • -
  • Exp (object) – Experimental object of module ‘epr_setup’.

  • -
  • SimOpt (object) – SimulationOptions object of module ‘epr_setup’.

  • -
  • *FitOpt (object) – FittingOptions object of module ‘epr_setup’.

  • -
  • current_best (bool) – True if the given Sys and Exp are the current best while running in -optimization mode. False if Sys and Exp are the final result / are the -given Sys and Exp in normal simulation mode. The default is False.

  • -
-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-

Profiles

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis.profiles.add_profile(profile, pkind, pname='')[source]
-

Add a new profile for EasyPairSpin.

-
-
Parameters:
-
    -
  • profile (dict) – Dictionary with all profile settings.

  • -
  • pkind (str) – Give the kind of the kind of the profile. Not case sensitive. pkind can -be ‘plot’, ‘save’, ‘simulation’, ‘optimization’, ‘spinsystem’ or -‘variation’.

  • -
  • pname (str) – Name of the profile. If no profile name is given, a default one will -be generated by _get_profile_name(). The default is ‘’.

  • -
-
-
Raises:
-

ValueError – Raised if pkind isn’t ‘plot’, ‘save’, ‘simulation’, ‘optimization’, - ‘spinsystem’ or ‘variation’.

-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-

Examples

-

Creating and adding a new profile:

-
>>> Sys = epr_setup.Spinsystem()
->>> Sys_profile = new_spinsystem_profile()
->>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228]
->>> prom.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1')
-
-
-
- -
-
-spinanalysis.profiles.export(path=None, pkind='all', pname='all')[source]
-

Export the choosen profile(s) as a zip archive.

-
-
Parameters:
-
    -
  • path (str) – Path where the zip file will be stored. If no path given, the zip file -will be stored in the current working directory. The default is None.

  • -
  • pkind ([<class ‘str’>, <class ‘list’>]) – Define which kind(s) of profiles should be exported. Multiple kinds of -profiles possible. Options are ‘plot’, ‘spinsystem’, ‘optimization’, -‘save’, ‘variation’, ‘simulation’ and ‘all’. The default is ‘all’.

  • -
  • pname (str) – Give the basename of the profile. The default is ‘all’.

  • -
-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis.profiles.import_profiles(zipfile, override=False)[source]
-

Import profiles from a zip archive.

-
-
Parameters:
-
    -
  • zipfile (str) – Path to the zip archive.

  • -
  • override (bool) – If True, existing profile with the same name will be overriden. The -default is False.

  • -
-
-
Returns:
-

Nothing will be returned.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis.profiles.load_plot_profile(pname)[source]
-

Load a plotting profile from a mplstylesheet.

-
-
Parameters:
-

pname (str) – Name of the profile. Case sensitive.

-
-
Returns:
-

Contains the settings for the plotting functions.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.load_profile(pname, pkind)[source]
-

Load a given plotting profile.

-
-
Parameters:
-
    -
  • pname (str) – Name of the profile. Case sensitive. Either with .ini or not. -E. g.: load_profile(‘test’) or load_profile(‘test.ini’).

  • -
  • pkind (str) – Give the kind of the profile. Not case sensitive. pkind can be ‘save’, -‘simulation’, ‘optimization’, ‘spinsystem’ or ‘variation’.

  • -
-
-
Returns:
-

profile – Loaded profile as a dictionary.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_optimization_profile()[source]
-

Get a default optimization profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for optimization routines.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_plot_profile()[source]
-

Get an empty plotting profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for plottings.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_save_profile()[source]
-

Get a default save profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for saving.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_simulation_profile()[source]
-

Get an empty simulation profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for simulation profiles.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_spinsystem_profile()[source]
-

Get a default spinsystem profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for a spinsystem.

-
-
Return type:
-

dict

-
-
-
- -
-
-spinanalysis.profiles.new_variation_profile()[source]
-

Get a default variation profile.

-
-
Returns:
-

default_profile – Dictionary with default settings for variation.

-
-
Return type:
-

dict

-
-
-
- -
-
-

Internal Modules

-

The following modules support internal execution and integration workflows.

-
-

Wrappers

-

© M. Sc. Florian Quintes, 2021-2022

-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-spinanalysis._wrappers.function_benchmark(func, niter=100)[source]
-

This decorateur will run the given function niter times and print the best, -the worst and the average runtime.

-
-
Parameters:
-
    -
  • func (callable) – Function which will be benchmarked.

  • -
  • niter (int) – Number of function calls.

  • -
-
-
Returns:
-

Function with automatic benchmark.

-
-
Return type:
-

callable

-
-
-
- -
-
-spinanalysis._wrappers.multicore(simulation)[source]
-

Using multiprocessing.Pool() with starmap() for parallel computing of -various simulation routines using the easypairspin function interface -simulation(Sys, Exp, SimOpt).

-
-
Parameters:
-

simulation (callable) – Simulation function which uses the easypairspin interface (Sys, Exp, -SimOpt).

-
-
Returns:
-

multicore_wrapper – The origin simulation callable as multicore version.

-
-
Return type:
-

callable

-
-
-
- -
-
-spinanalysis._wrappers.timer(func)[source]
-

Decorator function to measure time for one function call.

-
-
Parameters:
-

func (callable) – Function whose runtime will be measured.

-
-
Returns:
-

res – Result(s) of the function.

-
-
Return type:
-

callable

-
-
-
- -
-
-

Interface Handler

-
    -
    1. -
    2. Sc. Florian Quintes, 2021-2026

    3. -
    -
  1. -
-

@contact: florian.quintes@pc.uni.freiburg.de

-

@author: Florian Quintes

-
-
-class spinanalysis._interface_handler.BasinhoppingBounds(Var)[source]
-

Bases: object

-

Acceptance test for the scipy.optimize.basinhopping algorithm.

-
-
-xmin
-

Lower bounds for the varied parameters.

-
-
Type:
-

np.array

-
-
-
- -
-
-xmax
-

Upper bounds for the varied parameters.

-
-
Type:
-

np.array

-
-
-
- -
-
-Var
-

Object of class Variation from the epr_setup module.

-
-
Type:
-

object

-
-
-
- -
-
-__call__(**kwargs)[source]
-

Check if the current guess is within the bounds.

-
- -
- -
-
-class spinanalysis._interface_handler.BasinhoppingStatus(Sys, Var, verbose=False)[source]
-

Bases: object

-

Status callback for the scipy.optimize.basinhopping algorithm.

-
-
-best
-

Best objective-function value found so far.

-
-
Type:
-

float

-
-
-
- -
-
-def_Sys
-

Reference spin-system object.

-
-
Type:
-

object

-
-
-
- -
-
-Var
-

Variation object containing the parameter ranges.

-
-
Type:
-

object

-
-
-
- -
-
-verbose
-

Controls whether status information is printed.

-
-
Type:
-

bool, optional

-
-
-
- -
-
-xmin
-

Lower bounds for the varied parameters.

-
-
Type:
-

np.array

-
-
-
- -
-
-xmax
-

Upper bounds for the varied parameters.

-
-
Type:
-

np.array

-
-
-
- -
-
-__call__(x, value, accepted)[source]
-

Print status information.

-
- -
-
-check_bounds(x)[source]
-

Check if the current guess is within the bounds.

-
-
Parameters:
-

x (GenericAlias[float64]) – Current guess.

-
-
Returns:
-

True if the guess is within the bounds, False if not.

-
-
Return type:
-

bool

-
-
-
- -
-
-save_best(x)[source]
-

Create a spin-system object from the best parameter vector and save it.

-
-
Parameters:
-

x (GenericAlias[float64]) – Current guess.

-
-
Returns:
-

No return value.

-
-
Return type:
-

None

-
-
-
- -
- -
-
-class spinanalysis._interface_handler.BasinhoppingStep(Var, stepsize=0.75)[source]
-

Bases: object

-

Step generator for the scipy.optimize.basinhopping algorithm.

-
-
-stepsize
-

Relative size of the random step with respect to the variation range.

-
-
Type:
-

float, optional

-
-
-
- -
-
-rng
-

NumPy random number generator.

-
-
Type:
-

object

-
-
-
- -
-
-Var
-

Object of class Variation from the epr_setup module.

-
-
Type:
-

object

-
-
-
- -
-
-bounds
-

Parameter boundaries as a two-dimensional array.

-
-
Type:
-

np.array

-
-
-
- -
-
-lb
-

Lower bounds.

-
-
Type:
-

np.array

-
-
-
- -
-
-ub
-

Upper bounds.

-
-
Type:
-

np.array

-
-
-
- -
-
-var_range
-

Half the difference between lower and upper bounds.

-
-
Type:
-

np.array

-
-
-
- -
-
-dim_var
-

Number of variables.

-
-
Type:
-

int

-
-
-
- -
-
-__call__(x)[source]
-

Generate the next random step.

-
- -
-
-check_guess(x)[source]
-

Check whether the current parameter vector is within the bounds.

-

Out-of-bound values are replaced by randomly generated values inside the bounds.

-
-
Parameters:
-

x (GenericAlias[float64]) – Current guess.

-
-
Returns:
-

x – Current guess.

-
-
Return type:
-

GenericAlias[float64]

-
-
-
- -
- -
-
-spinanalysis._interface_handler.basinhopping(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.basinhopping for global optimization.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Returns:
-

best_Sys – Best spin-system object found by the optimizer.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.differential_evolution(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.differential_evolution for global optimization.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Returns:
-

best_Sys – Best spin-system object found by the optimizer.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.dualannealing(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.dual_annealing for global optimization.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Returns:
-

best_Sys – Best spin-system object found by the optimizer.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.get_random_x0(boundaries)[source]
-

Generate a random initial guess within the variation boundaries.

-
-
Parameters:
-

boundaries (Sequence[tuple[float, float]]) – Lower and upper bounds of the fitted parameters.

-
-
Returns:
-

x0 – Randomly generated initial parameter vector.

-
-
Return type:
-

GenericAlias[float64]

-
-
-
- -
-
-spinanalysis._interface_handler.guess2Sys(x, Sys, Var, SimOpt)[source]
-

Create a spin-system object from the current optimizer vector.

-
-
Parameters:
-
    -
  • x (GenericAlias[float64]) – Current parameter vector of the optimizer.

  • -
  • Sys (Any) – Reference spin-system object.

  • -
  • Var (Any) – Variation object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
-
-
Returns:
-

Sys_mod – Spin-system object corresponding to the current optimizer vector.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.least_squares(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.least_squares for nonlinear optimization.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Returns:
-

best_Sys – Best spin-system object found by the optimizer.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.minimize(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.minimize for local optimization.

-

Minimize provides multiple local optimization routines such as Nelder-Mead, -COBYLA, Powell, CG and so on.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Return type:
-

Any

-
-
Returns:
-

    -
  • best_Sys (object) – Best spin-system object found by the optimizer.

  • -
  • results (str, optional) – Results of the scipy optimization. Only for the GUI.

  • -
-

-
-
-
- -
-
-spinanalysis._interface_handler.plot_callback(xk, *_, Sys=None, Exp=None, SimOpt=None, FitOpt=None, Var=None, **_kwargs)[source]
-

Plot the current optimization state in the graphical user interface.

-
-
Parameters:
-
    -
  • xk (GenericAlias[float64]) – Current best guess vector.

  • -
  • *_ (arbitrary) – Unused arguments passed by some optimization routines to the callback -function.

  • -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
  • *_kwargs (arbitrary) – Unused keyword arguments passed by some optimization routines to the -callback function.

  • -
-
-
Raises:
-

ValueError – Raised if the selected simulation routine is unknown.

-
-
Return type:
-

None

-
-
-
- -
-
-spinanalysis._interface_handler.shgo(Sys, Exp, SimOpt, FitOpt, Var)[source]
-

Run scipy.optimize.shgo for global optimization.

-
-
Parameters:
-
    -
  • Sys (Any) – Reference spin-system object.

  • -
  • Exp (Any) – Experimental data object.

  • -
  • SimOpt (Any) – Simulation options object.

  • -
  • Var (Any) – Variation object describing the fitted parameters.

  • -
  • FitOpt (Any) – Fitting options object.

  • -
-
-
Returns:
-

best_Sys – Best spin-system object found by the optimizer.

-
-
Return type:
-

Any

-
-
-
- -
-
-spinanalysis._interface_handler.spinanalysis2scipy(x, *objects)[source]
-

Objective function for the scipy.optimize interface.

-

Used by scipy.optimize routines.

-
-
Parameters:
-
    -
  • x (GenericAlias[float64]) – Current parameter vector of the optimizer.

  • -
  • *objects (Any) – Additional objects required for the simulation and fitting interface.

  • -
-
-
Raises:
-

ValueError – Raised if the selected simulation routine is unknown.

-
-
Returns:
-

error – Sum of squared residuals between experimental and simulated data.

-
-
Return type:
-

float

-
-
-
- -
-
-spinanalysis._interface_handler.spinanalysis2scipy_res(x, *objects)[source]
-

Residual function for scipy.optimize least-squares algorithms.

-

Returns the absolute residuals between simulation and experiment.

-

Used by scipy.optimize routines.

-
-
Parameters:
-
    -
  • x (GenericAlias[float64]) – Current parameter vector of the optimizer.

  • -
  • *objects (Any) – Additional objects required for the simulation and fitting interface.

  • -
-
-
Raises:
-

ValueError – Raised if the selected simulation routine is unknown.

-
-
Returns:
-

error – One-dimensional array containing the absolute residuals.

-
-
Return type:
-

GenericAlias[float64]

-
-
-
- -
-
-spinanalysis._interface_handler.spinanalysis2scipy_singlecore(x, *objects)[source]
-

Objective function for the scipy.optimize interface.

-

Used by scipy.optimize.differential_evolution.

-
-
Parameters:
-
    -
  • x (GenericAlias[float64]) – Current parameter vector of the optimizer.

  • -
  • *objects (Any) – Additional objects required for the simulation and fitting interface.

  • -
-
-
Raises:
-

ValueError – Raised if the selected simulation routine is unknown.

-
-
Returns:
-

error – Sum of squared residuals between experimental and simulated data.

-
-
Return type:
-

float

-
-
-
- -
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/contributing.html b/docs/build/html/contributing.html deleted file mode 100644 index cbaa06d..0000000 --- a/docs/build/html/contributing.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - Contributing — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Contributing

-

Thank you for contributing to spinanalysis. The project is a Python -framework for loading, processing, plotting, simulating, optimizing, and -saving electron paramagnetic resonance (EPR) spectra.

-
-

Project Structure

-

The main package modules are organized by workflow:

-
    -
  • core.py orchestrates simulation and optimization.

  • -
  • epr.py defines EPR parameter and workflow objects.

  • -
  • loading.py reads EPR data files.

  • -
  • processing.py transforms and reconstructs spectra.

  • -
  • plotting.py renders spectra and figures.

  • -
  • saving.py writes figures, simulated data, and output files.

  • -
  • profiles.py manages configuration and profile files.

  • -
  • _wrappers.py provides internal timing and multiprocessing wrappers.

  • -
  • _interface_handler.py provides internal simulation and optimization -interfaces.

  • -
-

Profile templates and plotting styles are package data under -src/spinanalysis/data/profiles. Changes to a profile schema should update -the corresponding configspec.ini and relevant fixtures under -tests/data.

-
-
-

Development Setup

-

The project requires Python 3.13 or newer. The repository uses uv for -dependency and environment management.

-

Clone the repository and change into its directory:

-
git clone https://github.com/florianquintes/spinanalysis.git
-cd spinanalysis
-
-
-

Install the locked development environment from the repository root:

-
uv sync --dev
-
-
-

The package uses a src layout. Application code belongs under -src/spinanalysis and tests belong under tests.

-
-
-

Branches

-

Create a focused branch for each change and keep unrelated changes separate. -The repository does not currently document a required branch naming scheme.

-
-
-

Pull Requests

-

Pull requests should explain the change and include the relevant validation -results. Before opening a pull request, run the checks that apply to the -change:

-
uv run pytest
-uv run ruff check .
-uv build
-
-
-

If documentation is changed, also build the documentation as described in the -Documentation section. Mention known baseline failures or warnings rather -than presenting them as regressions.

-
-
-

Code Style

-

Follow the existing Python and reStructuredText style in the surrounding -files. Keep importable Python modules under src/spinanalysis and add tests -under tests. The repository has no configured formatter or type checker.

-

Ruff is the configured code-quality tool, using its default rules:

-
uv run ruff check .
-
-
-
-
-

Documentation

-

Documentation source files are under docs/source. The Sphinx configuration -adds src to the module search path and reads the package version from -pyproject.toml.

-

Build the documentation from the docs directory:

-
uv run sphinx-build -M html source build
-
-
-

The generated HTML is written to docs/build/html. Add a title to every -document included by the root index.rst so Sphinx can include it in the -table of contents.

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/development.html b/docs/build/html/development.html deleted file mode 100644 index 73bbbd4..0000000 --- a/docs/build/html/development.html +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - Development — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Development

-
-

Development Setup

-

The project requires Python 3.13 or newer and uses uv for dependency and -environment management. From the repository root, install the development -environment with:

-
uv sync --dev
-
-
-
-
-

Running Tests

-

Run the test suite from the repository root:

-
uv run pytest
-
-
-
-
-

Code Formatting and Linting

-

Check the code with Ruff:

-
uv run ruff check .
-
-
-

The repository does not currently configure a separate formatter.

-
-
-

Build the Documentation

-

Build the HTML documentation from the docs directory:

-
uv run sphinx-build -M html source build
-
-
-

The generated documentation is written to docs/build/html.

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/examples.html b/docs/build/html/examples.html deleted file mode 100644 index 8dcf39f..0000000 --- a/docs/build/html/examples.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - <no title> — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- - - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html deleted file mode 100644 index af6a913..0000000 --- a/docs/build/html/genindex.html +++ /dev/null @@ -1,851 +0,0 @@ - - - - - - - - Index — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Index

- -
- _ - | A - | B - | C - | D - | E - | F - | G - | H - | I - | J - | L - | M - | N - | O - | P - | R - | S - | T - | U - | V - | W - | X - -
-

_

- - - -
- -

A

- - - -
- -

B

- - - -
- -

C

- - - -
- -

D

- - - -
- -

E

- - - -
- -

F

- - - -
- -

G

- - - -
- -

H

- - - -
- -

I

- - - -
- -

J

- - - -
- -

L

- - - -
- -

M

- - - -
- -

N

- - - -
- -

O

- - - -
- -

P

- - - -
- -

R

- - - -
- -

S

- - - -
- -

T

- - - -
- -

U

- - - -
- -

V

- - - -
- -

W

- - - -
- -

X

- - - -
- - - -
-
-
- -
- -
-

© Copyright 2026, Florian Quintes.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/index.html b/docs/build/html/index.html deleted file mode 100644 index 5fa07b6..0000000 --- a/docs/build/html/index.html +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - spinanalysis — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

spinanalysis

-

spinanalysis is a high-level Python framework for the analysis of -electron paramagnetic resonance (EPR) spectra.

-

The framework provides modules for loading, processing, plotting, simulating, -optimizing, and saving EPR spectra. It is designed to support complete -analysis workflows from experimental data to simulated and fitted spectra.

-
-

Important

-

The simulation routines for spin-correlated, singlet-born radical pairs, -out-of-phase ESEEM signals, and transiently coupled systems are currently -provided through external dependencies.

-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/installation.html b/docs/build/html/installation.html deleted file mode 100644 index 2c0a682..0000000 --- a/docs/build/html/installation.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - Installation — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Installation

-
-

Requirements

-

spinanalysis requires Python 3.13 or newer. uv or pip is required -to install the package.

-
-
-

Installation Commands

-

The following commands cover the common installation use cases:

- ----- - - - - - - - - - - - - - - - - -

uv

pip

User

uv add spinanalysis

pip install spinanalysis

Developer

uv sync --dev

pip install -e .

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/introduction.html b/docs/build/html/introduction.html deleted file mode 100644 index c75240f..0000000 --- a/docs/build/html/introduction.html +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - Introduction — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Introduction

-

spinanalysis is a Python framework for the analysis of electron -paramagnetic resonance (EPR) spectra. It is designed to support a complete -workflow, from loading experimental data and applying common processing steps -to visualizing, simulating, optimizing, and saving results.

-

The package supports common EPR data formats, including Bruker BES3T, -ESP380E, MATLAB, simulated, and plain-text data. Processing tools cover tasks -such as normalization, offset reduction, background correction, and -reconstruction of out-of-phase ESEEM spectra. Matplotlib-based plotting -functions provide consistent two-dimensional and three-dimensional figures -using configurable plot styles.

-

Simulation and optimization workflows are organized around EPR parameter -objects and profile-based configuration. The profile system stores reusable -settings for spin systems, simulation and optimization routines, plotting, -and saving. Some specialized simulation routines are provided through -external dependencies and may require additional installation steps.

-
-

Module overview

-

The package is organized into modules that support different parts of an EPR -analysis workflow:

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Module

Description

core

Orchestrates simulation and optimization workflows.

epr

Defines EPR parameter, experimental, simulation, and optimization -objects.

loading

Loads EPR data from Bruker, ESP380E, MATLAB, simulated, and text files.

processing

Normalizes, corrects, and reconstructs spectra.

plotting

Creates two-dimensional and three-dimensional spectrum plots.

saving

Saves figures, simulated data, and simulation output files.

profiles

Creates, loads, saves, imports, and exports configuration profiles.

_wrappers

Provides internal timing and multiprocessing wrappers.

_interface_handler

Provides internal interfaces for simulation and optimization routines.

-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/license.html b/docs/build/html/license.html deleted file mode 100644 index e8cdbb2..0000000 --- a/docs/build/html/license.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - License — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

License

-

spinanalysis is licensed under the GNU General Public License v3.0 (GPLv3).

-
-

Important

-

The GPLv3 allows commercial use. It does not prohibit others from selling -software based on spinanalysis.

-
-

In summary, the GPLv3:

-
    -
  • allows use, modification, and redistribution;

  • -
  • requires modified or derived distributed software to remain licensed under -the GPLv3;

  • -
  • requires the corresponding source code to be made available when distributing -binaries;

  • -
  • permits commercial use, including selling GPLv3-licensed software;

  • -
  • provides no warranty.

  • -
-

The complete license text is available in the -LICENSE file.

-

For more information, see the -official GPLv3 license text.

-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/objects.inv b/docs/build/html/objects.inv deleted file mode 100644 index 814c2b0..0000000 Binary files a/docs/build/html/objects.inv and /dev/null differ diff --git a/docs/build/html/py-modindex.html b/docs/build/html/py-modindex.html deleted file mode 100644 index 49505ea..0000000 --- a/docs/build/html/py-modindex.html +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - Python Module Index — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Python Module Index

- -
- s -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
- s
- spinanalysis -
    - spinanalysis._interface_handler -
    - spinanalysis._wrappers -
    - spinanalysis.core -
    - spinanalysis.epr -
    - spinanalysis.loading -
    - spinanalysis.plotting -
    - spinanalysis.processing -
    - spinanalysis.profiles -
    - spinanalysis.saving -
- - -
-
-
- -
- -
-

© Copyright 2026, Florian Quintes.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/build/html/search.html b/docs/build/html/search.html deleted file mode 100644 index a1f8c73..0000000 --- a/docs/build/html/search.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - Search — spinanalysis 0.1.0 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - - - -
- -
- -
-
-
- -
- -
-

© Copyright 2026, Florian Quintes.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - - - - \ No newline at end of file diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js deleted file mode 100644 index fbe2427..0000000 --- a/docs/build/html/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles":{"API Reference":[[0,null]],"Branches":[[1,"branches"]],"Build the Documentation":[[2,"build-the-documentation"]],"Code Formatting and Linting":[[2,"code-formatting-and-linting"]],"Code Style":[[1,"code-style"]],"Contents":[[4,null]],"Contributing":[[1,null]],"Core":[[0,"module-spinanalysis.core"]],"Development":[[2,null]],"Development Setup":[[1,"development-setup"],[2,"development-setup"]],"Documentation":[[1,"documentation"]],"EPR":[[0,"module-spinanalysis.epr"]],"Installation":[[5,null]],"Installation Commands":[[5,"installation-commands"]],"Interface Handler":[[0,"module-spinanalysis._interface_handler"]],"Internal Modules":[[0,"internal-modules"]],"Introduction":[[6,null]],"License":[[7,null]],"Loading":[[0,"module-spinanalysis.loading"]],"Module overview":[[6,"module-overview"]],"Plotting":[[0,"module-spinanalysis.plotting"]],"Processing":[[0,"module-spinanalysis.processing"]],"Profiles":[[0,"module-spinanalysis.profiles"]],"Project Structure":[[1,"project-structure"]],"Pull Requests":[[1,"pull-requests"]],"Requirements":[[5,"requirements"]],"Running Tests":[[2,"running-tests"]],"Saving":[[0,"module-spinanalysis.saving"]],"Wrappers":[[0,"module-spinanalysis._wrappers"]],"spinanalysis":[[4,null]]},"docnames":["api","contributing","development","examples","index","installation","introduction","license"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","contributing.rst","development.rst","examples.rst","index.rst","installation.rst","introduction.rst","license.rst"],"indexentries":{},"objects":{"spinanalysis":[[0,0,0,"-","_interface_handler"],[0,0,0,"-","_wrappers"],[0,0,0,"-","core"],[0,0,0,"-","epr"],[0,0,0,"-","loading"],[0,0,0,"-","plotting"],[0,0,0,"-","processing"],[0,0,0,"-","profiles"],[0,0,0,"-","saving"]],"spinanalysis._interface_handler":[[0,1,1,"","BasinhoppingBounds"],[0,1,1,"","BasinhoppingStatus"],[0,1,1,"","BasinhoppingStep"],[0,4,1,"","basinhopping"],[0,4,1,"","differential_evolution"],[0,4,1,"","dualannealing"],[0,4,1,"","get_random_x0"],[0,4,1,"","guess2Sys"],[0,4,1,"","least_squares"],[0,4,1,"","minimize"],[0,4,1,"","plot_callback"],[0,4,1,"","shgo"],[0,4,1,"","spinanalysis2scipy"],[0,4,1,"","spinanalysis2scipy_res"],[0,4,1,"","spinanalysis2scipy_singlecore"]],"spinanalysis._interface_handler.BasinhoppingBounds":[[0,2,1,"","Var"],[0,3,1,"","__call__"],[0,2,1,"","xmax"],[0,2,1,"","xmin"]],"spinanalysis._interface_handler.BasinhoppingStatus":[[0,2,1,"","Var"],[0,3,1,"","__call__"],[0,2,1,"","best"],[0,3,1,"","check_bounds"],[0,2,1,"","def_Sys"],[0,3,1,"","save_best"],[0,2,1,"","verbose"],[0,2,1,"","xmax"],[0,2,1,"","xmin"]],"spinanalysis._interface_handler.BasinhoppingStep":[[0,2,1,"","Var"],[0,3,1,"","__call__"],[0,2,1,"","bounds"],[0,3,1,"","check_guess"],[0,2,1,"","dim_var"],[0,2,1,"","lb"],[0,2,1,"","rng"],[0,2,1,"","stepsize"],[0,2,1,"","ub"],[0,2,1,"","var_range"]],"spinanalysis._wrappers":[[0,4,1,"","function_benchmark"],[0,4,1,"","multicore"],[0,4,1,"","timer"]],"spinanalysis.core":[[0,4,1,"","optimize"],[0,4,1,"","simulate"],[0,4,1,"","start_log"]],"spinanalysis.epr":[[0,1,1,"","EPR_Parameters"],[0,1,1,"","Experimental"],[0,1,1,"","FittingOptions"],[0,1,1,"","SimulationOptions"],[0,1,1,"","Spinsystem"],[0,1,1,"","Variation"]],"spinanalysis.epr.EPR_Parameters":[[0,2,1,"","A1"],[0,2,1,"","A1_frame"],[0,2,1,"","A2"],[0,2,1,"","A2_frame"],[0,2,1,"","A3"],[0,2,1,"","A3_frame"],[0,2,1,"","A4"],[0,2,1,"","A4_frame"],[0,2,1,"","A5"],[0,2,1,"","A5_frame"],[0,2,1,"","A_eseem"],[0,2,1,"","D"],[0,2,1,"","D_frame"],[0,2,1,"","D_tri"],[0,2,1,"","D_tri_frame"],[0,2,1,"","E"],[0,2,1,"","E_tri"],[0,2,1,"","I1"],[0,2,1,"","I2"],[0,2,1,"","I3"],[0,2,1,"","I4"],[0,2,1,"","I5"],[0,2,1,"","J_0"],[0,2,1,"","J_ex"],[0,2,1,"","T_pm"],[0,2,1,"","T_relax_1"],[0,2,1,"","T_relax_2"],[0,2,1,"","amplitude"],[0,2,1,"","beta"],[0,2,1,"","decay"],[0,2,1,"","g"],[0,2,1,"","g1"],[0,2,1,"","g1_frame"],[0,2,1,"","g2"],[0,2,1,"","g2_frame"],[0,2,1,"","g_frame"],[0,2,1,"","g_tri"],[0,2,1,"","g_tri_frame"],[0,2,1,"","n1"],[0,2,1,"","n2"],[0,2,1,"","n3"],[0,2,1,"","n4"],[0,2,1,"","n5"],[0,2,1,"","omega_I"],[0,2,1,"","population"],[0,2,1,"","width_gauss"]],"spinanalysis.epr.Experimental":[[0,2,1,"","B_z"],[0,2,1,"","freq_mw"],[0,3,1,"id0","get_linear_time_axis"],[0,2,1,"","int"],[0,2,1,"","magnetic_field"],[0,2,1,"","spec_sim"],[0,2,1,"","time_axis"]],"spinanalysis.epr.FittingOptions":[[0,2,1,"","cpu_cores"],[0,2,1,"","gui"],[0,3,1,"id1","load"],[0,2,1,"","method"],[0,2,1,"","routine"],[0,3,1,"id2","save"],[0,2,1,"","window"],[0,2,1,"","x0"]],"spinanalysis.epr.SimulationOptions":[[0,2,1,"","eigval_mode"],[0,2,1,"","force_cpu"],[0,2,1,"","grid_points"],[0,3,1,"id3","load"],[0,2,1,"","pop_evolution"],[0,2,1,"","regularization_mode"],[0,2,1,"","routine"],[0,3,1,"id4","save"],[0,2,1,"","space"]],"spinanalysis.epr.Spinsystem":[[0,2,1,"","I1"],[0,2,1,"","I2"],[0,2,1,"","I3"],[0,2,1,"","I4"],[0,2,1,"","I5"],[0,3,1,"","_get_g_iso"],[0,2,1,"","acceptor_list"],[0,2,1,"","distribution"],[0,2,1,"","distribution_order"],[0,2,1,"","donor_list"],[0,2,1,"","dynamics"],[0,2,1,"","frame_group_i"],[0,2,1,"","g1_iso"],[0,2,1,"","g2_iso"],[0,3,1,"id5","load"],[0,2,1,"","n1"],[0,2,1,"","n2"],[0,2,1,"","n3"],[0,2,1,"","n4"],[0,2,1,"","n5"],[0,2,1,"","precursor"],[0,3,1,"id6","save"],[0,2,1,"","spin_system"]],"spinanalysis.epr.Variation":[[0,2,1,"","bohr_magneton"],[0,2,1,"","boundaries"],[0,2,1,"","freq_mw"],[0,3,1,"id7","get_boundaries"],[0,3,1,"","get_digits_for_one_par"],[0,3,1,"id8","get_needed_digits"],[0,3,1,"id9","get_number_of_genes"],[0,3,1,"id10","get_variation_array"],[0,3,1,"id11","load"],[0,2,1,"","needed_digits"],[0,2,1,"","number_of_genes"],[0,3,1,"id12","save"],[0,3,1,"id13","update_digits"],[0,2,1,"","variation_array"]],"spinanalysis.loading":[[0,4,1,"","convert_parameter_type"],[0,4,1,"","get_DSC_parameters"],[0,4,1,"","get_byte_mode"],[0,4,1,"","get_data_dimension"],[0,4,1,"","get_full_path"],[0,4,1,"","get_transient_data"],[0,4,1,"","get_transient_info"],[0,4,1,"","load_axis_vector"],[0,4,1,"","load_data_vector"],[0,4,1,"","load_epr_ESP_transient"],[0,4,1,"","load_epr_bruker_bes3t"],[0,4,1,"","load_matlab"],[0,4,1,"","load_simulated_data"],[0,4,1,"","load_txt"],[0,4,1,"","read_single_transient_file"]],"spinanalysis.plotting":[[0,1,1,"","HiddenPrints"],[0,4,1,"","heatmap"],[0,4,1,"","plot_2D"],[0,4,1,"","plot_3D"],[0,4,1,"","plot_3D_multiple_lines"],[0,4,1,"","shifted_2D"]],"spinanalysis.processing":[[0,4,1,"","background_corr"],[0,4,1,"","biexp_fun"],[0,4,1,"","exp_fun"],[0,4,1,"","lin_fun"],[0,4,1,"","normalization"],[0,4,1,"","poly2_fun"],[0,4,1,"","poly3_fun"],[0,4,1,"","poly4_fun"],[0,4,1,"","reconstruct"],[0,4,1,"","reduce_offset"]],"spinanalysis.profiles":[[0,4,1,"","add_profile"],[0,4,1,"","export"],[0,4,1,"","import_profiles"],[0,4,1,"","load_plot_profile"],[0,4,1,"","load_profile"],[0,4,1,"","new_optimization_profile"],[0,4,1,"","new_plot_profile"],[0,4,1,"","new_save_profile"],[0,4,1,"","new_simulation_profile"],[0,4,1,"","new_spinsystem_profile"],[0,4,1,"","new_variation_profile"]],"spinanalysis.saving":[[0,4,1,"","save_plot"],[0,4,1,"","save_simulation"],[0,4,1,"","write_out_file"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"],"3":["py","method","Python method"],"4":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute","3":"py:method","4":"py:function"},"terms":{"1d":0,"2d":0,"3d":0,"A":0,"All":0,"An":0,"At":0,"Do":0,"Each":0,"For":[0,7],"From":2,"I":0,"If":[0,1],"In":7,"Is":0,"It":[4,6,7],"No":0,"Not":0,"Other":0,"Out":0,"Same":0,"So":0,"Some":6,"The":[0,1,2,4,5,6,7],"This":0,"When":[],"You":0,"_":0,"__call__":0,"_get_g_iso":0,"_get_profile_nam":0,"_interface_handl":[0,1,6],"_kwarg":0,"_wrapper":[0,1,6],"a1":0,"a1_fram":0,"a2":0,"a2_fram":0,"a3":0,"a3_fram":0,"a4":0,"a4_fram":0,"a5":0,"a5_fram":0,"a_eseem":0,"abov":0,"absolut":0,"accept":0,"acceptor":0,"acceptor_list":0,"action":[],"add":[0,1,5],"add_profil":0,"addit":[0,6],"aktuel":0,"algorithm":0,"allow":[0,7],"also":[0,1],"alway":0,"amplitud":0,"analysi":[4,6],"angl":0,"ani":0,"anlegen":0,"anyth":[],"api":4,"append":0,"appli":[1,6],"applic":1,"arbitrari":0,"archiv":0,"area":0,"argument":0,"around":6,"array":0,"artifact":[],"atom":0,"auc":0,"author":0,"autom":[],"automat":0,"avail":[0,7],"averag":0,"ax":0,"axe":0,"axi":0,"axis_vector":0,"b_z":0,"background":[0,6],"background_corr":0,"base":[0,6,7],"baselin":1,"basenam":0,"basi":0,"basic":0,"basinhop":0,"basinhoppingbound":0,"basinhoppingstatus":0,"basinhoppingstep":0,"baustell":0,"befor":1,"behavior":[],"belong":1,"benchmark":0,"bes3t":[0,6],"best":0,"best_si":0,"best_spinsystem":0,"beta":0,"biexp":0,"biexp_fun":0,"biexponenti":0,"binari":[0,7],"bohr":0,"bohr_magneton":0,"bool":0,"boolean":0,"born":4,"bound":0,"boundari":0,"branch":4,"bruker":[0,6],"build":[1,4],"byte":0,"byte_mod":0,"c":0,"calcul":0,"call":0,"callabl":0,"callback":0,"can":[0,1],"canva":0,"care":0,"case":[0,5],"cd":1,"cg":0,"chang":[0,1],"charact":0,"check":[0,1,2],"check_bound":0,"check_guess":0,"checker":1,"chemic":0,"choos":0,"choosen":0,"chromosom":0,"class":0,"clone":1,"cmplx_data_vector":0,"cmplx_int":0,"cobyla":0,"code":[4,7],"coeff":0,"collect":[],"com":1,"command":4,"commerci":7,"common":[5,6],"complet":[4,6,7],"complex":0,"complex128":0,"comput":0,"config":0,"configspec":1,"configur":[0,1,2,6],"consist":6,"constant":0,"contact":0,"contain":0,"content":1,"context":0,"contribut":4,"control":0,"convers":0,"convert":0,"convert_parameter_typ":0,"coolus":0,"core":[1,4,6],"correct":[0,6],"correl":4,"correspond":[0,1,7],"coupl":[0,4],"cover":[5,6],"cplx":0,"cpu":0,"cpu_cor":0,"creat":[0,1,6],"current":[0,1,2,4],"current_best":0,"custom":[],"d":0,"d_frame":0,"d_tri":0,"d_tri_fram":0,"data":[0,1,4,6],"data_key":0,"data_vector":0,"dataset":0,"dateien":0,"de":0,"decay":0,"decor":0,"decorateur":0,"def_si":0,"default":[0,1],"default_profil":0,"default_stylesheet":0,"defin":[0,1,6],"degre":0,"densiti":0,"depend":[1,2,4,6],"der":0,"deriv":[0,7],"describ":[0,1],"descript":6,"design":[4,6],"determin":0,"dev":[1,2,5],"develop":[4,5],"dict":0,"dictionari":0,"dies":0,"differ":[0,6],"differential_evolut":0,"digit":0,"digits_per_tru":0,"dim_var":0,"dimens":0,"dimension":[0,6],"dipol":0,"directori":[0,1,2],"directory_nam":0,"distanc":0,"distribut":[0,7],"distribution_ord":0,"doc":[1,2],"document":[0,4],"doe":[1,2,7],"doesn":0,"donor":0,"donor_list":0,"doub":0,"doublet":0,"dsc":0,"dsc_dict":0,"dsc_paramet":0,"dta":0,"dtype":0,"dual_ann":0,"dualann":0,"dure":0,"dx":0,"dynam":0,"e":[0,5],"e_tri":0,"easypairspin":0,"easypairspin_optim":0,"easyspin":0,"eigen":0,"eigenvalu":0,"eigval_mod":0,"either":0,"electron":[0,1,4,6],"elimin":0,"els":0,"empti":0,"environ":[1,2],"epr":[1,4,6],"epr_paramet":0,"epr_setup":0,"equal":0,"equival":0,"error":0,"eseem":[0,4,6],"esp380":[0,6],"even":0,"everi":1,"evolut":0,"exampl":0,"exchang":0,"execut":0,"exist":[0,1],"exit":[],"exp":0,"exp_1":0,"exp_fun":0,"experi":0,"experiment":[0,4,6],"explain":1,"exponenti":0,"export":[0,6],"extern":[0,4,6],"extract":0,"f":0,"failur":1,"fals":0,"far":0,"field":0,"fig":0,"figur":[0,1,6],"file":[0,1,6,7],"fileextens":0,"filenam":0,"filenumb":0,"final":0,"first":0,"fit":[0,4],"fitopt":0,"fitopt_2":0,"fitopt_3":0,"fitopt_prof":0,"fitopt_prof_1":0,"fitopt_prof_2":0,"fittingopt":0,"fixtur":1,"float":0,"float64":0,"florian":0,"florianquint":1,"fname":0,"focus":1,"folder":0,"foldernam":0,"follow":[0,1,5],"force_cpu":0,"format":[4,6],"formatt":[1,2],"formula":0,"found":0,"fourth":0,"fp":0,"fpath":0,"frac":0,"frame":0,"frame_group":0,"frame_group_1":0,"frame_group_i":0,"framework":[1,4,6],"freiburg":0,"freq_mw":0,"frequenc":0,"fromfil":0,"ftacqmodeslct":0,"full":0,"full_path":0,"func":0,"function":[0,6],"function_benchmark":0,"funktion":0,"g":0,"g1":0,"g1_frame":0,"g1_iso":0,"g2":0,"g2_frame":0,"g2_iso":0,"g_1":0,"g_frame":0,"g_iso":0,"g_tri":0,"g_tri_fram":0,"gauss":0,"gaussian":0,"gavap":0,"gene":0,"general":[0,7],"generat":[0,1,2],"genericalia":0,"genet":0,"genetic_radpair":0,"get":0,"get_boundari":0,"get_byte_mod":0,"get_data_dimens":0,"get_digits_for_one_par":0,"get_dsc_paramet":0,"get_full_path":0,"get_g_iso":0,"get_linear_time_axi":0,"get_needed_digit":0,"get_number_of_gen":0,"get_profile_nam":0,"get_random_x0":0,"get_transient_data":0,"get_transient_info":0,"get_variation_array":0,"gigahertz":0,"git":1,"github":[0,1],"give":0,"given":0,"global":0,"gnu":7,"go":0,"gplv3":7,"gpu":0,"graphic":0,"greater":0,"grid":0,"grid_point":0,"group":0,"guess":0,"guess2si":0,"gui":0,"half":0,"heatmap":0,"hertz":0,"hiddenprint":0,"high":4,"highest":0,"hilbert":0,"home":0,"html":[1,2],"https":1,"hyperfin":0,"i1":0,"i2":0,"i3":0,"i4":0,"i5":0,"iifmt":0,"ikkf":0,"imag_int":0,"imaginari":0,"import":[0,1,6],"import_profil":0,"includ":[1,6,7],"increas":0,"index":1,"induc":0,"info":0,"inform":[0,7],"ini":[0,1],"initi":0,"initialis":0,"insert":0,"insid":0,"instal":[1,2,4,6],"int":0,"integr":0,"intens":0,"interfac":[1,6],"internal":[1,4,6],"introduct":4,"invalid":0,"irfmt":0,"isn":0,"isotrop":0,"j_0":0,"j_ex":0,"just":0,"keep":1,"key":0,"keyerror":0,"keyword":0,"kind":0,"know":0,"known":1,"kwarg":0,"label":0,"laod":0,"last":0,"layout":1,"lb":0,"lead":0,"least":0,"least_squar":0,"left":0,"legend":0,"level":4,"licens":4,"lin":0,"lin_fun":0,"line":0,"linear":0,"linewidth":0,"lint":4,"linter":[],"list":0,"load":[1,4,6],"load_axis_vector":0,"load_data_vector":0,"load_epr_bruker_bes3t":0,"load_epr_esp_transi":0,"load_matlab":0,"load_plot_profil":0,"load_profil":0,"load_simulated_data":0,"load_txt":0,"loadtxt":0,"local":0,"lock":1,"log":0,"longer":0,"longitudin":0,"lower":0,"m":[0,1,2],"made":7,"mag_field_start":0,"mag_field_step":0,"mag_field_stop":0,"magnet":0,"magnetic_field":0,"magneton":0,"main":[0,1],"mal":0,"manag":[0,1,2],"mani":0,"mat":0,"match":[],"mathemat":0,"mathrm":0,"matlab":[0,6],"matplotlib":[0,6],"matrix":0,"matter":0,"max":0,"maximum":0,"may":6,"mead":0,"mean":0,"measur":0,"megahertz":0,"memori":0,"mention":[0,1],"messag":0,"method":0,"microwav":0,"min":0,"minim":0,"mode":0,"model":0,"modif":7,"modifi":7,"modul":[1,4],"moment":0,"monoexponenti":0,"mpl_stylesheet":0,"mplstylesheet":0,"mt":0,"much":0,"multi":0,"multicor":0,"multicore_wrapp":0,"multipl":0,"multiprocess":[0,1,6],"must":0,"n":[],"n1":0,"n2":0,"n3":0,"n4":0,"n5":0,"name":[0,1],"ndarray":0,"need":0,"needed_digit":0,"nelder":0,"never":0,"new":0,"new_optimization_profil":0,"new_plot_profil":0,"new_save_profil":0,"new_simulation_profil":0,"new_spinsystem_profil":0,"new_variation_profil":0,"newer":[1,2,5],"next":0,"nice":0,"nicht":0,"niter":0,"no_label":0,"none":0,"nonlinear":0,"norm":0,"normal":[0,6],"noth":0,"np":0,"nuclear":0,"nuclei":0,"number":0,"number_of_gen":0,"numpi":0,"nutzbar":0,"nutzung":0,"object":[0,1,6],"offici":7,"offset":[0,6],"omega_i":0,"one":0,"one_par":0,"onli":0,"oop":0,"open":1,"optim":[0,1,4,6],"option":0,"optiona":0,"orchestr":[1,6],"order":0,"organiz":[1,6],"orient":0,"origin":0,"others":7,"output":[0,1,6],"overfit":0,"overrid":0,"overriden":0,"overview":4,"overwrit":0,"packag":[0,1,5,6],"pair":[0,4],"par":0,"parallel":0,"paramagnet":[1,4,6],"paramet":[0,1,6],"part":[0,6],"pass":0,"path":[0,1],"path_to_fold":0,"pc":0,"pcolormesh":0,"perfom":0,"perform":0,"permit":7,"phase":[0,4,6],"pip":5,"pkind":0,"placehold":[],"plain":6,"plot":[1,4,6],"plot_2d":0,"plot_3d":0,"plot_3d_multiple_lin":0,"plot_callback":0,"plot_surfac":0,"plt":0,"pname":0,"point":0,"poly2":0,"poly2_fun":0,"poly3":0,"poly3_fun":0,"poly4":0,"poly4_fun":0,"polynomin":0,"pool":0,"pop_evolut":0,"popul":0,"possibl":0,"powel":0,"precursor":0,"prefix":0,"present":1,"previous":0,"print":0,"process":[1,4,6],"profil":[1,4,6],"profile_manag":0,"profile_nam":0,"prohibit":7,"project":[2,4],"prom":0,"provid":[0,1,4,6,7],"public":7,"publish":[],"pull":4,"put":0,"py":1,"pylab":0,"pypi":[],"pyproject":1,"pyspin":0,"pytest":[1,2],"python":[1,2,4,5,6],"qualiti":1,"quarter":0,"quint":0,"radian":0,"radiat":0,"radic":[0,4],"rais":0,"random":0,"rang":0,"rate":0,"rather":1,"read":1,"read_single_transient_fil":0,"real":0,"real_int":0,"recommend":0,"reconstruct":[0,1,6],"redistribut":7,"reduce_offset":0,"reduct":6,"refer":4,"regress":1,"regular":0,"regularization_mod":0,"rein":0,"relat":0,"relax":0,"releas":[],"relev":1,"reli":[],"remain":7,"render":1,"replac":0,"repositori":[1,2],"represent":0,"request":4,"requir":[0,1,2,4,6,7],"res":0,"rescal":0,"residu":0,"reson":[1,4,6],"respect":0,"restructuredtext":1,"result":[0,1,6],"return":0,"reusabl":6,"right":0,"rng":0,"root":[1,2],"routin":[0,4,6],"rp":0,"rst":1,"ruff":[1,2],"rule":1,"run":[0,1,4],"runtim":0,"s":0,"safe":0,"save":[1,4,6],"save_best":0,"save_plot":0,"save_simul":0,"save_simulationopt":0,"savefig":0,"savetxt":0,"sc":0,"schema":1,"scheme":1,"scipi":0,"search":[0,1],"second":0,"section":[0,1],"see":[0,7],"select":0,"self":0,"sell":7,"sensit":0,"separ":[1,2],"sequenc":0,"set":[0,6],"setup":4,"sharpli":0,"sheet":0,"shgo":0,"shift":0,"shifted_2d":0,"signal":[0,4],"simopt":0,"simopt_2":0,"simopt_3":0,"simopt_prof":0,"simopt_prof_1":0,"simopt_prof_2":0,"simpson":0,"simul":[0,1,4,6],"simulated_spectra":0,"simulationopt":0,"singl":0,"singlet":[0,4],"size":0,"softwar":7,"soll":0,"sourc":[0,1,2,7],"space":0,"spec_sim":0,"special":6,"spectra":[0,1,4,6],"spectrum":[0,6],"spheric":0,"sphinx":[1,2],"spin":[0,4,6],"spin_system":0,"spinanalysi":[0,1,5,6,7],"spinanalysis2scipi":0,"spinanalysis2scipy_r":0,"spinanalysis2scipy_singlecor":0,"spinpolar":0,"spinsystem":0,"split":0,"squar":0,"src":1,"standard":0,"starmap":0,"start":0,"start_directori":0,"start_log":0,"state":0,"static_radical_pair":0,"static_radpair":0,"status":0,"step":[0,6],"stepsiz":0,"store":[0,6],"str":0,"string":0,"strip":0,"strtobool":[],"structur":4,"style":[0,4,6],"stylesheet":0,"success":[],"suit":2,"sum":0,"summari":7,"support":[0,4,6],"supress":0,"surround":1,"sync":[1,2,5],"sys":0,"sys_2":0,"sys_3":0,"sys_4":0,"sys_mod":0,"sys_prof_1":0,"sys_prof_2":0,"sys_profil":0,"system":[0,4,6],"t":0,"t_max":0,"t_min":0,"t_pm":0,"t_point":0,"t_relax_1":0,"t_relax_2":0,"t_scale":0,"tabl":[0,1],"tag":[],"take":0,"task":6,"tdp":0,"teacup":0,"templat":[0,1],"tensor":0,"tesla":0,"test":[0,1,4],"text":[6,7],"thank":1,"therefor":0,"this_fold":0,"three":[0,6],"thus":0,"tikhonov":0,"time":[0,1,6],"time_axi":0,"time_length":0,"time_point":0,"timea":0,"timer":0,"titl":1,"todo":0,"toml":1,"tool":[1,6],"total":0,"trace":0,"transform":1,"transient":[0,4],"transient_info":0,"transvers":0,"trip":0,"triplet":0,"true":0,"truth":[],"tupl":0,"two":[0,6],"txt":0,"type":[0,1],"ub":0,"uni":0,"unitari":0,"unknown":0,"unnorm":0,"unrel":1,"unus":0,"updat":[0,1],"update_digit":0,"upper":0,"use":[0,1,2,5,6,7],"user":[0,5],"uv":[1,2,5],"v":[],"v3":7,"val":[],"valid":1,"valu":0,"valueerror":0,"var":0,"var_2":0,"var_3":0,"var_4":0,"var_prof_1":0,"var_prof_2":0,"var_profil":0,"var_rang":0,"vari":0,"variabl":0,"variat":0,"variation_array":0,"various":0,"vector":0,"verbos":0,"veri":0,"version":[0,1],"vertic":0,"via":0,"visual":6,"walker":0,"want":0,"warn":1,"warranti":7,"websit":0,"whatev":0,"whether":0,"whole":0,"whose":0,"width_gauss":0,"will":0,"window":0,"within":0,"without":0,"work":0,"workflow":[0,1,4,6],"worst":0,"wrapper":[1,6],"write":[0,1],"write_out_fil":0,"written":[1,2],"w\u00e4hrend":0,"x":0,"x0":0,"x_":0,"x_axi":0,"x_i":0,"x_new":0,"x_norm":0,"x_shift":0,"xk":0,"xmax":0,"xmin":0,"y":0,"y_axi":0,"y_corr":0,"yes":[],"yule":0,"z":0,"zero":0,"zf":0,"zip":0,"zipfil":0,"\u00e5":0},"titles":["API Reference","Contributing","Development","","spinanalysis","Installation","Introduction","License"],"titleterms":{"api":0,"branch":1,"build":2,"code":[1,2],"command":5,"content":4,"contribut":1,"core":0,"develop":[1,2],"document":[1,2],"epr":0,"format":2,"handler":0,"instal":5,"interfac":0,"internal":0,"introduct":6,"licens":7,"lint":2,"load":0,"modul":[0,6],"overview":6,"packag":[],"plot":0,"process":0,"profil":0,"project":1,"pull":1,"refer":0,"releas":[],"request":1,"requir":5,"run":2,"save":0,"setup":[1,2],"spinanalysi":4,"structur":1,"style":1,"test":2,"valid":[],"wrapper":0}}) \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index 98cbca3..bd58e84 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -13,9 +13,29 @@ Core EPR --- -.. automodule:: spinanalysis.epr +.. autoclass:: spinanalysis.epr.Spinsystem :members: - :show-inheritance: + :no-index: + +.. autoclass:: spinanalysis.epr.Experimental + :members: + :no-index: + +.. autoclass:: spinanalysis.epr.Variation + :members: + :no-index: + +.. autoclass:: spinanalysis.epr.SimulationOptions + :members: + :no-index: + +.. autoclass:: spinanalysis.epr.FittingOptions + :members: + :no-index: + +.. autoclass:: spinanalysis.epr.ExperimentalInput + :members: + :no-index: Loading ------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 246d885..8e98e42 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,6 +38,8 @@ ] autodoc_mock_imports = [ + "mkl", + "scipy2eps", "teacups", "oop_eseem", "genetic_radpair", diff --git a/docs/source/examples.rst b/docs/source/examples.rst index e69de29..1257c97 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -0,0 +1,6 @@ +Examples +======== + +.. note:: + + Worked examples will be added in a future release. diff --git a/pyproject.toml b/pyproject.toml index ba5dc1f..daabd0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "A high-level Python framework for the analysis of electron parama readme = "README.md" license = { file = "LICENSE" } authors = [ - { name = "florianquintes", email = "florian.quintes@pc.uni-freiburg.de" } + { name = "Florian Quintes", email = "florian.quintes@pc.uni-freiburg.de" } ] requires-python = ">=3.13" dependencies = [ @@ -14,6 +14,7 @@ dependencies = [ "mkl>=2026.1.0", "mkl-service>=2.7.2", "numpy>=2.5.1", + "pydantic>=2.13.4", "scipy>=1.18.0", "statsmodels>=0.14.6", ] @@ -22,6 +23,9 @@ dependencies = [ source_dir = "docs/source" build_dir = "docs/build" +[tool.pytest.ini_options] +python_files = ["test_*.py", "tests_*.py"] + [build-system] requires = ["uv_build>=0.11.28,<0.12.0"] build-backend = "uv_build" diff --git a/src/spinanalysis/__pycache__/__init__.cpython-313.pyc b/src/spinanalysis/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index a6eb113..0000000 Binary files a/src/spinanalysis/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/_interface_handler.cpython-313.pyc b/src/spinanalysis/__pycache__/_interface_handler.cpython-313.pyc deleted file mode 100644 index 211d5a0..0000000 Binary files a/src/spinanalysis/__pycache__/_interface_handler.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/_utils.cpython-313.pyc b/src/spinanalysis/__pycache__/_utils.cpython-313.pyc deleted file mode 100644 index 8619ed2..0000000 Binary files a/src/spinanalysis/__pycache__/_utils.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/_wrappers.cpython-313.pyc b/src/spinanalysis/__pycache__/_wrappers.cpython-313.pyc deleted file mode 100644 index 3ea8259..0000000 Binary files a/src/spinanalysis/__pycache__/_wrappers.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/core.cpython-313.pyc b/src/spinanalysis/__pycache__/core.cpython-313.pyc deleted file mode 100644 index f24bef3..0000000 Binary files a/src/spinanalysis/__pycache__/core.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/epr.cpython-313.pyc b/src/spinanalysis/__pycache__/epr.cpython-313.pyc deleted file mode 100644 index 267ee3f..0000000 Binary files a/src/spinanalysis/__pycache__/epr.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/loading.cpython-313.pyc b/src/spinanalysis/__pycache__/loading.cpython-313.pyc deleted file mode 100644 index 8d03fb5..0000000 Binary files a/src/spinanalysis/__pycache__/loading.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/plotting.cpython-313.pyc b/src/spinanalysis/__pycache__/plotting.cpython-313.pyc deleted file mode 100644 index f2390dc..0000000 Binary files a/src/spinanalysis/__pycache__/plotting.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/processing.cpython-313.pyc b/src/spinanalysis/__pycache__/processing.cpython-313.pyc deleted file mode 100644 index e089395..0000000 Binary files a/src/spinanalysis/__pycache__/processing.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/profiles.cpython-313.pyc b/src/spinanalysis/__pycache__/profiles.cpython-313.pyc deleted file mode 100644 index 5d9d3c4..0000000 Binary files a/src/spinanalysis/__pycache__/profiles.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/__pycache__/saving.cpython-313.pyc b/src/spinanalysis/__pycache__/saving.cpython-313.pyc deleted file mode 100644 index 6c7a3c6..0000000 Binary files a/src/spinanalysis/__pycache__/saving.cpython-313.pyc and /dev/null differ diff --git a/src/spinanalysis/_interface_handler.py b/src/spinanalysis/_interface_handler.py index 32ce7f2..d9f5026 100644 --- a/src/spinanalysis/_interface_handler.py +++ b/src/spinanalysis/_interface_handler.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -(c) M. Sc. Florian Quintes, 2021-2026 +"""Bridge between spinanalysis objects and scipy.optimize routines. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @@ -37,16 +38,15 @@ def plot_callback( Var: Any = None, **_kwargs, ) -> None: - """ - Plot the current optimization state in the graphical user interface. + """Plot the current optimization state in the graphical user interface. Parameters ---------- - xk : np.array + xk : np.ndarray Current best guess vector. - *_ : arbitrary - Unused arguments passed by some optimization routines to the callback - function. + *_ + Unused positional arguments passed by some optimization routines to the + callback function. Sys : object Reference spin-system object. Exp : object @@ -57,19 +57,14 @@ def plot_callback( Variation object describing the fitted parameters. FitOpt : object Fitting options object. - *_kwargs : arbitrary + **_kwargs Unused keyword arguments passed by some optimization routines to the callback function. Raises ------ ValueError - Raised if the selected simulation routine is unknown. - - Returns - ------- - None. - + If the selected simulation routine is unknown. """ current_Sys = guess2Sys(xk, Sys, Var, SimOpt) SimOpt.mode = "simulation" @@ -110,14 +105,13 @@ def plot_callback( def spinanalysis2scipy(x: NDArray[np.float64], *objects: Any) -> float: - """ - Objective function for the scipy.optimize interface. + """Objective function for the scipy.optimize interface. Used by scipy.optimize routines. Parameters ---------- - x : np.array + x : np.ndarray Current parameter vector of the optimizer. *objects : object Additional objects required for the simulation and fitting interface. @@ -125,13 +119,12 @@ def spinanalysis2scipy(x: NDArray[np.float64], *objects: Any) -> float: Raises ------ ValueError - Raised if the selected simulation routine is unknown. + If the selected simulation routine is unknown. Returns ------- error : float Sum of squared residuals between experimental and simulated data. - """ Sys_def, Exp, SimOpt, Var = objects @@ -159,14 +152,13 @@ def spinanalysis2scipy(x: NDArray[np.float64], *objects: Any) -> float: def spinanalysis2scipy_singlecore(x: NDArray[np.float64], *objects: Any) -> float: - """ - Objective function for the scipy.optimize interface. + """Objective function for the scipy.optimize interface. Used by scipy.optimize.differential_evolution. Parameters ---------- - x : np.array + x : np.ndarray Current parameter vector of the optimizer. *objects : object Additional objects required for the simulation and fitting interface. @@ -174,13 +166,12 @@ def spinanalysis2scipy_singlecore(x: NDArray[np.float64], *objects: Any) -> floa Raises ------ ValueError - Raised if the selected simulation routine is unknown. + If the selected simulation routine is unknown. Returns ------- error : float Sum of squared residuals between experimental and simulated data. - """ Sys_def, Exp, SimOpt, Var = objects @@ -210,8 +201,7 @@ def spinanalysis2scipy_singlecore(x: NDArray[np.float64], *objects: Any) -> floa def spinanalysis2scipy_res( x: NDArray[np.float64], *objects: Any ) -> NDArray[np.float64]: - """ - Residual function for scipy.optimize least-squares algorithms. + """Residual function for scipy.optimize least-squares algorithms. Returns the absolute residuals between simulation and experiment. @@ -219,7 +209,7 @@ def spinanalysis2scipy_res( Parameters ---------- - x : np.array + x : np.ndarray Current parameter vector of the optimizer. *objects : object Additional objects required for the simulation and fitting interface. @@ -227,13 +217,12 @@ def spinanalysis2scipy_res( Raises ------ ValueError - Raised if the selected simulation routine is unknown. + If the selected simulation routine is unknown. Returns ------- - error : np.array + error : np.ndarray One-dimensional array containing the absolute residuals. - """ Sys_def, Exp, SimOpt, Var = objects @@ -261,12 +250,11 @@ def spinanalysis2scipy_res( def guess2Sys(x: NDArray[np.float64], Sys: Any, Var: Any, SimOpt: Any) -> Any: - """ - Create a spin-system object from the current optimizer vector. + """Create a spin-system object from the current optimizer vector. Parameters ---------- - x : np.array + x : np.ndarray Current parameter vector of the optimizer. Sys : object Reference spin-system object. @@ -279,38 +267,40 @@ def guess2Sys(x: NDArray[np.float64], Sys: Any, Var: Any, SimOpt: Any) -> Any: ------- Sys_mod : object Spin-system object corresponding to the current optimizer vector. - """ Sys_mod = deepcopy(Sys) n = 0 - for key in vars(Var): + for key in Var.__dict__: if key in Var.non_vars: pass elif key in Var.single_vars: - if vars(Var)[key] > 0: - vars(Sys_mod)[key] = x[n] + if getattr(Var, key) > 0: + setattr(Sys_mod, key, x[n]) n += 1 else: - for i, parameter in enumerate(vars(Var)[key]): + var_values = getattr(Var, key) + sys_values = getattr(Sys_mod, key) + for i, parameter in enumerate(var_values): if parameter > 0: - vars(Sys_mod)[key][i] = x[n] + sys_values[i] = x[n] n += 1 if hasattr(Var, "isotropic"): for par in Var.isotropic: - vars(Sys_mod)[par][1] = vars(Sys_mod)[par][0] - vars(Sys_mod)[par][2] = vars(Sys_mod)[par][0] + arr = getattr(Sys_mod, par) + arr[1] = arr[0] + arr[2] = arr[0] - for key in vars(Sys): + for key in Sys.__dict__: if key.startswith("frame_group"): - for i, frame in enumerate(vars(Sys)[key]): + for i, frame in enumerate(getattr(Sys, key)): if not frame.endswith("_frame"): frame = frame + "_frame" if i == 0: - reference_frame = vars(Sys_mod)[frame] + reference_frame = getattr(Sys_mod, frame) else: - vars(Sys_mod)[frame] = reference_frame + setattr(Sys_mod, frame, reference_frame) if Var.fit_distribution: pos = np.zeros(Sys.distribution_order) @@ -333,19 +323,17 @@ def guess2Sys(x: NDArray[np.float64], Sys: Any, Var: Any, SimOpt: Any) -> Any: def get_random_x0( boundaries: Sequence[tuple[float, float]], ) -> NDArray[np.float64]: - """ - Generate a random initial guess within the variation boundaries. + """Generate a random initial guess within the variation boundaries. Parameters ---------- - boundaries : list + boundaries : Sequence[tuple[float, float]] Lower and upper bounds of the fitted parameters. Returns ------- - x0 : np.array + x0 : np.ndarray Randomly generated initial parameter vector. - """ x0 = np.empty(len(boundaries)) @@ -356,8 +344,7 @@ def get_random_x0( def dualannealing(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.dual_annealing for global optimization. + """Run scipy.optimize.dual_annealing for global optimization. Parameters ---------- @@ -376,7 +363,6 @@ def dualannealing(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any ------- best_Sys : object Best spin-system object found by the optimizer. - """ Var.get_boundaries(Sys) @@ -435,8 +421,7 @@ def dualannealing(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any def shgo(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.shgo for global optimization. + """Run scipy.optimize.shgo for global optimization. Parameters ---------- @@ -455,7 +440,6 @@ def shgo(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: ------- best_Sys : object Best spin-system object found by the optimizer. - """ Var.get_boundaries(Sys) @@ -524,8 +508,7 @@ def shgo(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: def differential_evolution( Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any ) -> Any: - """ - Run scipy.optimize.differential_evolution for global optimization. + """Run scipy.optimize.differential_evolution for global optimization. Parameters ---------- @@ -544,7 +527,6 @@ def differential_evolution( ------- best_Sys : object Best spin-system object found by the optimizer. - """ Var.get_boundaries(Sys) @@ -612,8 +594,7 @@ def differential_evolution( def basinhopping(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.basinhopping for global optimization. + """Run scipy.optimize.basinhopping for global optimization. Parameters ---------- @@ -632,7 +613,6 @@ def basinhopping(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: ------- best_Sys : object Best spin-system object found by the optimizer. - """ Var.get_boundaries(Sys) @@ -707,8 +687,7 @@ def basinhopping(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: def least_squares(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.least_squares for nonlinear optimization. + """Run scipy.optimize.least_squares for nonlinear optimization. Parameters ---------- @@ -727,7 +706,6 @@ def least_squares(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any ------- best_Sys : object Best spin-system object found by the optimizer. - """ Var.get_boundaries(Sys) @@ -788,8 +766,7 @@ def least_squares(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any def minimize(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: - """ - Run scipy.optimize.minimize for local optimization. + """Run scipy.optimize.minimize for local optimization. Minimize provides multiple local optimization routines such as Nelder-Mead, COBYLA, Powell, CG and so on. @@ -813,7 +790,6 @@ def minimize(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: Best spin-system object found by the optimizer. results : str, optional Results of the scipy optimization. Only for the GUI. - """ Var.get_boundaries(Sys) @@ -851,14 +827,13 @@ def minimize(Sys: Any, Exp: Any, SimOpt: Any, FitOpt: Any, Var: Any) -> Any: class BasinhoppingBounds: - """ - Acceptance test for the scipy.optimize.basinhopping algorithm. + """Acceptance test for the scipy.optimize.basinhopping algorithm. Attributes ---------- - xmin : np.array + xmin : np.ndarray Lower bounds for the varied parameters. - xmax : np.array + xmax : np.ndarray Upper bounds for the varied parameters. Var : object Object of class Variation from the epr_setup module. @@ -867,7 +842,6 @@ class BasinhoppingBounds: ------- __call__(**kwargs) Check if the current guess is within the bounds. - """ def __init__(self, Var: Any) -> None: @@ -876,19 +850,17 @@ def __init__(self, Var: Any) -> None: return None def __call__(self, **kwargs) -> bool: - """ - Check if the current guess is within the bounds. + """Check if the current guess is within the bounds. Parameters ---------- - **kwargs : list + **kwargs Varied parameters. Returns ------- bool True if the guess is within the bounds, False if not. - """ x = kwargs["x_new"] test_min = bool(np.all(x >= self.xmin)) @@ -898,8 +870,7 @@ def __call__(self, **kwargs) -> bool: class BasinhoppingStep: - """ - Step generator for the scipy.optimize.basinhopping algorithm. + """Step generator for the scipy.optimize.basinhopping algorithm. Attributes ---------- @@ -909,13 +880,13 @@ class BasinhoppingStep: NumPy random number generator. Var : object Object of class Variation from the epr_setup module. - bounds : np.array + bounds : np.ndarray Parameter boundaries as a two-dimensional array. - lb : np.array + lb : np.ndarray Lower bounds. - ub : np.array + ub : np.ndarray Upper bounds. - var_range : np.array + var_range : np.ndarray Half the difference between lower and upper bounds. dim_var : int Number of variables. @@ -924,7 +895,6 @@ class BasinhoppingStep: ------- __call__(x) Generate the next random step. - """ def __init__(self, Var: Any, stepsize: float = 0.75) -> None: @@ -939,19 +909,17 @@ def __init__(self, Var: Any, stepsize: float = 0.75) -> None: return None def __call__(self, x: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Generate the next random step. + """Generate the next random step. Parameters ---------- - x : np.array + x : np.ndarray Current guess. Returns ------- - x : np.array + x : np.ndarray Current parameter vector after applying a random step. - """ x = self.check_guess(x) @@ -974,21 +942,19 @@ def __call__(self, x: NDArray[np.float64]) -> NDArray[np.float64]: return x def check_guess(self, x: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Check whether the current parameter vector is within the bounds. + """Check whether the current parameter vector is within the bounds. Out-of-bound values are replaced by randomly generated values inside the bounds. Parameters ---------- - x : np.array + x : np.ndarray Current guess. Returns ------- - x : np.array + x : np.ndarray Current guess. - """ choose_outliers = ~((x >= self.lb) & (x <= self.ub)) @@ -1000,8 +966,7 @@ def check_guess(self, x: NDArray[np.float64]) -> NDArray[np.float64]: class BasinhoppingStatus: - """ - Status callback for the scipy.optimize.basinhopping algorithm. + """Status callback for the scipy.optimize.basinhopping algorithm. Attributes ---------- @@ -1013,16 +978,15 @@ class BasinhoppingStatus: Variation object containing the parameter ranges. verbose : bool, optional Controls whether status information is printed. - xmin : np.array + xmin : np.ndarray Lower bounds for the varied parameters. - xmax : np.array + xmax : np.ndarray Upper bounds for the varied parameters. Methods ------- __call__(x, value, accepted) Print status information. - """ def __init__(self, Sys: Any, Var: Any, verbose: bool = False) -> None: @@ -1039,23 +1003,16 @@ def __init__(self, Sys: Any, Var: Any, verbose: bool = False) -> None: return None def __call__(self, x: NDArray[np.float64], value: float, accepted: int) -> None: - """ - Print status information about the basinhopping progress. + """Print status information about the basinhopping progress. Parameters ---------- - x : np.array + x : np.ndarray Current guess. value : float Objective-function value of the current parameter vector. accepted : int Acceptance status returned by the basinhopping algorithm. - - Returns - ------- - None - No return value. - """ mes_1 = "The current minimum with an error of {:.4f} ".format(value) if self.check_bounds(x): @@ -1077,19 +1034,17 @@ def __call__(self, x: NDArray[np.float64], value: float, accepted: int) -> None: return None def check_bounds(self, x: NDArray[np.float64]) -> bool: - """ - Check if the current guess is within the bounds. + """Check if the current guess is within the bounds. Parameters ---------- - x : np.array + x : np.ndarray Current guess. Returns ------- bool True if the guess is within the bounds, False if not. - """ test_min = bool(np.all(x >= self.xmin)) test_max = bool(np.all(x <= self.xmax)) @@ -1097,19 +1052,12 @@ def check_bounds(self, x: NDArray[np.float64]) -> bool: return test_min and test_max def save_best(self, x: NDArray[np.float64]) -> None: - """ - Create a spin-system object from the best parameter vector and save it. + """Create a spin-system object from the best parameter vector and save it. Parameters ---------- - x : np.array + x : np.ndarray Current guess. - - Returns - ------- - None - No return value. - """ Sys = guess2Sys(x, self.def_Sys, self.Var, self.SimOpt) Sys.save_spinsystem("basinhopping_current_best") diff --git a/src/spinanalysis/_utils.py b/src/spinanalysis/_utils.py index ecd7ecb..b7a743e 100644 --- a/src/spinanalysis/_utils.py +++ b/src/spinanalysis/_utils.py @@ -1,9 +1,34 @@ -def strtobool(val): +"""Internal utility functions shared across the spinanalysis package. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + + +def strtobool(val: str) -> int: """Convert a string representation of truth to true (1) or false (0). - True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values - are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if - 'val' is anything else. + True values are ``'y'``, ``'yes'``, ``'t'``, ``'true'``, ``'on'``, and + ``'1'``; false values are ``'n'``, ``'no'``, ``'f'``, ``'false'``, + ``'off'``, and ``'0'``. + + Parameters + ---------- + val : str + String to convert. + + Raises + ------ + ValueError + If *val* is not a recognised truth value. + + Returns + ------- + int + ``1`` for true, ``0`` for false. """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): @@ -11,4 +36,4 @@ def strtobool(val): elif val in ("n", "no", "f", "false", "off", "0"): return 0 else: - raise ValueError(f"invalid truth value {val!r}") + raise ValueError("invalid truth value {0!r}".format(val)) diff --git a/src/spinanalysis/_wrappers.py b/src/spinanalysis/_wrappers.py index cb2ff32..64a9867 100644 --- a/src/spinanalysis/_wrappers.py +++ b/src/spinanalysis/_wrappers.py @@ -1,23 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Provide decorators and multicore helpers for simulation routines. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @author: Florian Quintes """ -from time import time +from collections.abc import Callable from copy import deepcopy -from multiprocessing import cpu_count, Pool from itertools import repeat +from multiprocessing import Pool, cpu_count +from time import time +from typing import Any + import numpy as np -def timer(func: callable) -> callable: - """ - Decorator function to measure time for one function call. +def timer(func: Callable) -> Callable: + """Measure the wall-clock runtime of a single function call. Parameters ---------- @@ -26,12 +29,11 @@ def timer(func: callable) -> callable: Returns ------- - res : any - Result(s) of the function. - + callable + Wrapper that prints the runtime and returns the original result. """ - def time_wrap(*args, **kwargs): + def time_wrap(*args: Any, **kwargs: Any) -> Any: start = time() res = func(*args, **kwargs) runtime = time() - start @@ -41,26 +43,23 @@ def time_wrap(*args, **kwargs): return time_wrap -def function_benchmark(func: callable, niter: int = 100) -> callable: - """ - This decorateur will run the given function niter times and print the best, - the worst and the average runtime. +def function_benchmark(func: Callable, niter: int = 100) -> Callable: + """Run *func* *niter* times and print best, worst, and average runtime. Parameters ---------- func : callable Function which will be benchmarked. - niter : int - Number of function calls. + niter : int, optional + Number of function calls. The default is 100. Returns ------- callable - Function with automatic benchmark. - + Wrapper that runs the benchmark and prints timing statistics. """ - def benchmarked_function(*args, **kwargs): + def benchmarked_function(*args: Any, **kwargs: Any) -> None: times = np.empty(niter) for i in range(times.shape[0]): start = time() @@ -85,39 +84,26 @@ def benchmarked_function(*args, **kwargs): return benchmarked_function -def multicore(simulation: callable) -> callable: - """ - Using multiprocessing.Pool() with starmap() for parallel computing of - various simulation routines using the easypairspin function interface - simulation(Sys, Exp, SimOpt). +def multicore(simulation: Callable) -> Callable: + """Parallelise a simulation routine using :class:`multiprocessing.Pool`. + + The decorated function must accept ``(Sys, Exp, SimOpt)`` and is + executed on ``SimOpt.cpu_cores`` processes, each handling a slice of + the magnetic-field axis. Parameters ---------- simulation : callable - Simulation function which uses the easypairspin interface (Sys, Exp, - SimOpt). + Simulation function with the signature ``(Sys, Exp, SimOpt)``. Returns ------- - multicore_wrapper : callable - The origin simulation callable as multicore version. - + callable + Wrapper with signature ``(Sys, Exp, SimOpt)`` that distributes + the work across CPU cores and returns the concatenated spectrum. """ - def simulation_with_queue( - simulation: callable, - Sys: object, - Exp: object, - SimOpt: object, - queue: object, - num: int, - ) -> None: - intensity = simulation(Sys, Exp, SimOpt) - queue.put((num, intensity)) - - return None - - def multicore_wrapper(Sys: object, Exp: object, SimOpt: object) -> np.array: + def multicore_wrapper(Sys: Any, Exp: Any, SimOpt: Any) -> np.ndarray: if SimOpt.cpu_cores == 0: SimOpt.cpu_cores = cpu_count() @@ -140,7 +126,6 @@ def multicore_wrapper(Sys: object, Exp: object, SimOpt: object) -> np.array: Experimental.B_z = whole_B_z[start:] Exp_list[core] = Experimental - # [Multi-Core Calculation] pool = Pool(processes=SimOpt.cpu_cores) single_intensities = pool.starmap( simulation, zip(repeat(Sys), Exp_list, repeat(SimOpt)) @@ -148,27 +133,6 @@ def multicore_wrapper(Sys: object, Exp: object, SimOpt: object) -> np.array: pool.close() pool.join() - # queue = Queue() - - # processes = [] - # for i, Exp_i in enumerate(Exp_list): - # processes.append(Process(target=simulation_with_queue, - # args=(simulation, Sys, Exp_i, SimOpt, - # queue, i) - # ) - # ) - - # for p in processes: - # p.start() - - # for p in processes: - # p.join() - - # intensities = [queue.get() for p in processes] - # intensities.sort() - - # single_intensities = [intensity[1] for intensity in intensities] - intens_arr_tuple = tuple(single_intensities) intensity = np.hstack(intens_arr_tuple) diff --git a/src/spinanalysis/core.py b/src/spinanalysis/core.py index 12a097f..330d5c5 100644 --- a/src/spinanalysis/core.py +++ b/src/spinanalysis/core.py @@ -1,219 +1,186 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Provide spectrum simulation and spin-system optimization workflows. + +The module exposes :func:`simulate` for generating spectra with the supported +simulation routines and :func:`optimize` for fitting spin-system parameters. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @author: Florian Quintes """ -import mkl -import spinanalysis._interface_handler as spo -from genetic_radpair.genetic_classes import Genetic_Radpair -from oop_eseem.opossum import ( - oop_eseem, - oop_eseem_distance_distribution, - oop_eseem_distribution, -) -from teacups.simulations import teacups -from static_radical_pair.radpair import do_simulation_multicore -import sys -import os -import datetime -import logging import numpy as np -version = "v0.1.0" - - -mkl.set_num_threads(1) - - -def start_log() -> None: - # TODO - """ - Diese Funktion soll mal Log-Dateien während der Nutzung anlegen. - Aktuell nicht nutzbar! Reine Baustelle! - """ - date = datetime.datetime.now() +from spinanalysis.epr import ( + Experimental, + FittingOptions, + SimulationOptions, + Spinsystem, + Variation, +) - date = date.strftime("%d_%m_%Y__%H:%M:%S") - session_name = "EasyPairSpin_Session_at_" + date + ".log" - log_path = os.path.join(sys.prefix, "easypairspin", "logs") - if not os.path.exists(log_path): - os.makedirs(log_path) +def _set_mkl_threads(n: int = 1) -> None: + """Restrict MKL to *n* threads to avoid oversubscription in parallel runs.""" + try: + import mkl - logfile_name = os.path.join(log_path, session_name) + mkl.set_num_threads(n) + except ImportError: + pass - fmt = "{asctime} - [{levelname:8}] - {module} {funcName} - {message}" - dfmt = "%d/%m/%Y %H:%M:%S" - logging.basicConfig( - filename=logfile_name, - style="{", - format=fmt, - datefmt=dfmt, - level=logging.DEBUG, - ) - logging.info("Log startet") - return None +_set_mkl_threads() -def simulate(Sys: object, Exp: object, SimOpt: object) -> np.ndarray: - # TODO Hinschreiben, welche Simulationen möglich sind + Paper. - """ - Do various simulations with (spinpolarized) radical pairs. +def simulate( + Sys: Spinsystem, + Exp: Experimental, + SimOpt: SimulationOptions, +) -> np.ndarray: + """Simulate a spectrum using the selected simulation routine. Parameters ---------- - Sys : object - Spinsystem object of module 'epr_setup'. - Exp : object - Experimental object of module 'epr_setup'. simulated_spectra will be - saved in Exp.spec_sim. - SimOpt : object - SimulationOptions object of module 'epr_setup'. + Sys : Spinsystem + Spin-system parameters from :mod:`spinanalysis.epr`. + Exp : Experimental + Experimental parameters from :mod:`spinanalysis.epr`. The simulated + spectrum is stored in ``Exp.spec_sim``. + SimOpt : SimulationOptions + Simulation options from :mod:`spinanalysis.epr`. + + Notes + ----- + ``SimOpt.mode`` is set to ``"simulation"`` before dispatch. For the + ``teacups`` routine with ``eigval_mode`` *False* the mode is overridden to + ``"fitting"`` because teacups internally switches between the two. + ``Exp.spec_sim`` is always set to the returned (normalized) spectrum. Raises ------ ValueError - Will be raised, if the given simulation routine in SimOpt.routine is an - invalid string. + If ``SimOpt.routine`` is not supported. Returns ------- - simulated_spectra : np.ndarray - Simulated spectra as a numpy.ndarray. - - Examples - -------- - - Basic simulation using static_radical_pair: - - >>> from epr_setup import Spinsystem, Experimental, SimulationOptions - >>> Sys = Spinsystem() - >>> Exp = Experimental() - >>> SimOpt = SimulationOptions() - >>> SimOpt.routine = 'static_radpair' - >>> simulate(Sys, Exp, SimOpt) - - Plot your result: - - >>> from plotting import plot_2D - >>> plot_2D(Exp.B_z , Exp.spec_sim) - + numpy.ndarray + The normalized simulated spectrum. """ SimOpt.mode = "simulation" - - # TODO an match case anpassen PYTHON 3.10 - if SimOpt.routine.lower() == "static_radpair": - simulated_spectra = do_simulation_multicore(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "teacups": - if SimOpt.eigval_mode is True: - teacups(Sys, Exp, SimOpt) - simulated_spectra = np.ones((Exp.t_points, len(Exp.B_z))) - else: - SimOpt.mode = "fitting" - simulated_spectra = teacups(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "opossum": - simulated_spectra = oop_eseem(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis": - simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) - elif SimOpt.routine.lower() == "didelphis_tikhonov": - simulated_spectra = oop_eseem_distance_distribution(Sys, Exp, SimOpt) - else: - raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) - - simulated_spectra /= (abs(simulated_spectra)).max() + routine = SimOpt.routine.lower() + + match routine: + case "static_radpair": + from static_radical_pair.radpair import do_simulation_multicore + + simulated_spectra = do_simulation_multicore(Sys, Exp, SimOpt) + case "teacups": + from teacups.simulations import teacups + + if SimOpt.eigval_mode: + teacups(Sys, Exp, SimOpt) + simulated_spectra = np.ones((Exp.t_points, len(Exp.B_z))) + else: + SimOpt.mode = "fitting" + simulated_spectra = teacups(Sys, Exp, SimOpt) + case "opossum": + from oop_eseem.opossum import oop_eseem + + simulated_spectra = oop_eseem(Sys, Exp, SimOpt) + case "didelphis": + from oop_eseem.opossum import oop_eseem_distribution + + simulated_spectra = oop_eseem_distribution(Sys, Exp, SimOpt) + case "didelphis_tikhonov": + from oop_eseem.opossum import oop_eseem_distance_distribution + + simulated_spectra = oop_eseem_distance_distribution(Sys, Exp, SimOpt) + case _: + raise ValueError("Can't find a routine named '{0}'!".format(SimOpt.routine)) + + max_val = abs(simulated_spectra).max() + if max_val > 0: + simulated_spectra = simulated_spectra / max_val Exp.spec_sim = simulated_spectra return simulated_spectra def optimize( - Sys: object, - Exp: object, - SimOpt: object, - FitOpt: object, - Var: object, -) -> object: - # Hinschreiben welche Optimierungen möglich sind + Paper. - """ - Do various optimizations with all simulations available in - 'easypairspin()'. + Sys: Spinsystem, + Exp: Experimental, + SimOpt: SimulationOptions, + FitOpt: FittingOptions, + Var: Variation, +) -> Spinsystem: + """Optimize spin-system parameters using the selected routine. Parameters ---------- - Sys : object - Spinsystem object of module 'epr_setup'. - Exp : object - Experimental object of module 'epr_setup'. - SimOpt : object - SimulationOptions object of module 'epr_setup'. - FitOpt : object - FittingOptions object of module 'epr_setup'. - Var : object - Variation object of module 'epr_setup'. + Sys : Spinsystem + Spin-system parameters from :mod:`spinanalysis.epr`. + Exp : Experimental + Experimental parameters from :mod:`spinanalysis.epr`. + SimOpt : SimulationOptions + Simulation options from :mod:`spinanalysis.epr`. + FitOpt : FittingOptions + Fitting options from :mod:`spinanalysis.epr`. + Var : Variation + Variation parameters from :mod:`spinanalysis.epr`. + + Notes + ----- + ``SimOpt.mode`` is set to ``"fitting"`` before dispatch. Raises ------ ValueError - Will be raised, if the given simulation routine in FitOpt.routine is an - invalid string. + If ``FitOpt.routine`` is not supported. Returns ------- - best_Spinsystem: object - The best Spinsystem found during optimization. Object is of class - Spinsystem of module 'epr_setup'. - - Examples - -------- - - Basic optimization using genetic_radpair and static_radical_pair: - - >>> from epr_setup import Spinsystem, Experimental, SimulationOptions, - ... FittingOptions, Variation - >>> Sys = Spinsystem() - >>> Exp = Experimental() - >>> SimOpt = SimulationOptions() - >>> SimOpt.routine = 'static_radpair' - >>> FitOpt = FittingOptions() - >>> Var = Variation() - >>> Var.g1 = np.array([0.001, 0.003, 0.002]) - >>> best_Sys = optimize(Sys, Exp, SimOpt, FitOpt, Var) - - Plot your result: - - >>> from plotting import plot_2D - >>> simulate(best_Sys, Exp, SimOpt) - >>> plot_2D(Exp.B_z , Exp.spec_sim) - - + Spinsystem + The best spin-system parameters found during optimization. """ - # TODO an match case anpassen PYTHON 3.10 - SimOpt.mode = "fitting" - - if FitOpt.routine.lower() == "genetic": - Gen_Rad = Genetic_Radpair(Sys, Exp, SimOpt, FitOpt, Var) - best_spinsystem = Gen_Rad.best_spinsystem - elif FitOpt.routine.lower() == "dual_annealing": - best_spinsystem = spo.dualannealing(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "shgo": - best_spinsystem = spo.shgo(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "differential_evolution": - best_spinsystem = spo.differential_evolution(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "basinhopping": - best_spinsystem = spo.basinhopping(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "least_squares": - best_spinsystem = spo.least_squares(Sys, Exp, SimOpt, FitOpt, Var) - elif FitOpt.routine.lower() == "minimize": - best_spinsystem = spo.minimize(Sys, Exp, SimOpt, FitOpt, Var) - else: - raise ValueError( - "Can't find an optimization routine named '{0}'".format(FitOpt.routine) - ) + routine = FitOpt.routine.lower() + + match routine: + case "genetic": + from genetic_radpair.genetic_classes import Genetic_Radpair + + genetic_radpair = Genetic_Radpair(Sys, Exp, SimOpt, FitOpt, Var) + best_spinsystem = genetic_radpair.best_spinsystem + case "dual_annealing": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.dualannealing(Sys, Exp, SimOpt, FitOpt, Var) + case "shgo": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.shgo(Sys, Exp, SimOpt, FitOpt, Var) + case "differential_evolution": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.differential_evolution(Sys, Exp, SimOpt, FitOpt, Var) + case "basinhopping": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.basinhopping(Sys, Exp, SimOpt, FitOpt, Var) + case "least_squares": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.least_squares(Sys, Exp, SimOpt, FitOpt, Var) + case "minimize": + import spinanalysis._interface_handler as spo + + best_spinsystem = spo.minimize(Sys, Exp, SimOpt, FitOpt, Var) + case _: + raise ValueError( + "Can't find an optimization routine named '{0}'!".format(FitOpt.routine) + ) return best_spinsystem diff --git a/src/spinanalysis/data/profiles/plot/default_stylesheet b/src/spinanalysis/data/profiles/plot/default_stylesheet index c23c105..4b71509 100644 --- a/src/spinanalysis/data/profiles/plot/default_stylesheet +++ b/src/spinanalysis/data/profiles/plot/default_stylesheet @@ -41,7 +41,7 @@ ##### CONFIGURATION BEGINS HERE ## *************************************************************************** -## * EASYPAIRSPIN SPECIAL SETTINGS * +## * SPINANALYSIS SPECIAL SETTINGS * ## *************************************************************************** percentage_mode: True diff --git a/src/spinanalysis/data/profiles/save/configspec.ini b/src/spinanalysis/data/profiles/save/configspec.ini deleted file mode 100644 index 15685c3..0000000 --- a/src/spinanalysis/data/profiles/save/configspec.ini +++ /dev/null @@ -1 +0,0 @@ -[main] diff --git a/src/spinanalysis/data/profiles/simulation/configspec.ini b/src/spinanalysis/data/profiles/simulation/configspec.ini index 9ab536f..b8aac29 100644 --- a/src/spinanalysis/data/profiles/simulation/configspec.ini +++ b/src/spinanalysis/data/profiles/simulation/configspec.ini @@ -3,10 +3,10 @@ routine = option('static_radpair', 'teacups', 'opossum', 'didelphis', 'didelphis cpu_cores = integer(min=0) [static_radpair] -grid_points = integer(min=1) +knots = integer(min=1) [teacups] -grid_points = integer(min=1) +knots = integer(min=1) space = option('hilbert', 'liouville') [opossum] diff --git a/src/spinanalysis/epr.py b/src/spinanalysis/epr.py index 432b3ae..bb8abfb 100644 --- a/src/spinanalysis/epr.py +++ b/src/spinanalysis/epr.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Data models for EPR systems, experiments, and optimization settings. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @@ -10,120 +11,261 @@ import numpy as np import scipy.constants as constant +import warnings +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from spinanalysis import profiles - - -class EPR_Parameters: - """ - A class containing all parameters for various radical pair simulations. +from typing import Any, ClassVar + + +class ExperimentalInput(BaseModel): + """Validated input data for :class:`Experimental`.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + magnetic_field: np.ndarray | None = None + real_int: np.ndarray | None = None + imag_int: np.ndarray | None = None + cmplx_int: np.ndarray | None = None + time_axis: np.ndarray | None = None + rescale: bool = True + + @field_validator( + "magnetic_field", + "real_int", + "imag_int", + "cmplx_int", + "time_axis", + mode="before", + ) + @classmethod + def convert_arrays(cls, value: object) -> np.ndarray | None: + """Convert array-like input to an independent NumPy array.""" + if value is None: + return None + return np.asarray(value).copy() + + @field_validator("real_int", "imag_int", "time_axis", mode="after") + @classmethod + def require_real_arrays(cls, value: np.ndarray | None) -> np.ndarray | None: + """Reject complex-valued real intensity and time-axis inputs.""" + if value is not None and np.iscomplexobj(value): + raise ValueError("real_int, imag_int, and time_axis must be real-valued") + return value + + @field_validator("cmplx_int", mode="after") + @classmethod + def require_complex_intensity(cls, value: np.ndarray | None) -> np.ndarray | None: + """Require complex-valued input for ``cmplx_int``.""" + if value is not None and not np.iscomplexobj(value): + raise ValueError("cmplx_int must contain complex-valued data") + return value + + @field_validator("magnetic_field", mode="after") + @classmethod + def require_real_field(cls, value: np.ndarray | None) -> np.ndarray | None: + """Reject complex-valued magnetic-field input.""" + if value is not None and np.iscomplexobj(value): + raise ValueError("magnetic_field must be real-valued") + return value + + @model_validator(mode="after") + def validate_intensity_sources(self) -> "ExperimentalInput": + """Ensure complex and component intensity inputs are not combined.""" + if self.cmplx_int is not None and ( + self.real_int is not None or self.imag_int is not None + ): + raise ValueError("cmplx_int cannot be combined with real_int or imag_int") + if self.real_int is not None and self.imag_int is not None: + if self.real_int.shape != self.imag_int.shape: + raise ValueError("real_int and imag_int must have matching shapes") + return self + + +class _MutableModel(BaseModel): + """Compatibility base for mutable runtime models.""" + + model_config = ConfigDict( + extra="allow", arbitrary_types_allowed=True, validate_assignment=True + ) + + @field_validator( + "g1", + "g2", + "g_tri", + "g", + "A1", + "A2", + "A3", + "A4", + "A5", + "population", + "g1_frame", + "g2_frame", + "g_tri_frame", + "g_frame", + "A1_frame", + "A2_frame", + "A3_frame", + "A4_frame", + "A5_frame", + "D_frame", + "D_tri_frame", + mode="before", + check_fields=False, + ) + @classmethod + def arrays(cls, value: object) -> np.ndarray: + return np.asarray(value, dtype=float) + + def __setattr__(self, name: str, value: object) -> None: + super().__setattr__(name, value) + + +class EPR_Parameters(_MutableModel): + """A class containing all parameters for various radical pair simulations. Attributes ---------- - g1 : 1d-Array, np.float64 + g1 : np.ndarray g-Tensor of electron 1. - g2 : 1d-Array, np.float64 + g2 : np.ndarray g-Tensor of electron 2. - g_tri : 1d-Array, np.float64 + g_tri : np.ndarray g-Tensor of a triplet radical. - g : 1d-Array, np.float64 + g : np.ndarray g-Tensor of a radical. - A1 : 1d-Array, np.float64 + A1 : np.ndarray A-Tensor of nuclei 1 in Megahertz. - A2 : 1d-Array, np.float64 + A2 : np.ndarray A-Tensor of nuclei 2 in Megahertz. - A3 : 1d-Array, np.float64 + A3 : np.ndarray A-Tensor of nuclei 3 in Megahertz. - A4 : 1d-Array, np.float64 + A4 : np.ndarray A-Tensor of nuclei 4 in Megahertz. - A5 : 1d-Array, np.float64 + A5 : np.ndarray A-Tensor of nuclei 5 in Megahertz. - A_eseem : float64 + A_eseem : float Hyperfine coupling for the nuclei in OOP-ESEEM in Megahertz. - omega_I : float64 + omega_I : float Nuclei frequency in OOP-ESEEM in Megahertz. - D : float64 + D : float Zero field splitting parameter D in Megahertz. - D_tri : float64 + D_tri : float Zero field splitting parameter D of a triplet in Megahertz. - E : float64 + E : float Zero field splitting parameter E in Megahertz. - E_tri : float64 + E_tri : float Zero field splitting parameter E of a triplet in Megahertz. - beta : float64 + beta : float Decay rate of the exchange coupling in Å^-1. - J_0 : float64 + J_0 : float Zero distance exchange coupling constant in Megahertz. - J_ex : float64 + J_ex : float Exchange coupling in Megahertz. - g1_frame : 1d-Array, np.float64 + g1_frame : np.ndarray Orientation of electron spin 1 in radian. - g2_frame : 1d-Array, np.float64 + g2_frame : np.ndarray Orientation of electron spin 2 in radian. - g_tri_frame : 1d-Array, np.float64 + g_tri_frame : np.ndarray Orientation of g_tri in radian. - g_frame : 1d-Array, np.float64 + g_frame : np.ndarray Orientation of g in radian. - A1_frame : 1d-Array, np.float64 + A1_frame : np.ndarray Orientation of nuclei spin 1 in radian. - A2_frame : 1d-Array, np.float64 + A2_frame : np.ndarray Orientation of nuclei spin 2 in radian. - A3_frame : 1d-Array, np.float64 + A3_frame : np.ndarray Orientation of nuclei spin 3 in radian. - A4_frame : 1d-Array, np.float64 + A4_frame : np.ndarray Orientation of nuclei spin 4 in radian. - A5_frame : 1d-Array, np.float64 + A5_frame : np.ndarray Orientation of nuclei spin 5 in radian. - D_frame : 1d-Array, np.float64 + D_frame : np.ndarray Orientation of dipol coupling in radian. - D_tri_frame : 1d-Array, np.float64 + D_tri_frame : np.ndarray Orientation of triplet dipol coupling in radian. n1 : int Number of chemically equivalent atoms. - I1 : float64 + I1 : float Corresponding nuclear spin. n2 : int Number of chemically equivalent atoms. - I2 : float64 + I2 : float Corresponding nuclear spin. n3 : int Number of chemically equivalent atoms. - I3 : float64 + I3 : float Corresponding nuclear spin. n4 : int Number of chemically equivalent atoms. - I4 : float64 + I4 : float Corresponding nuclear spin. n5 : int Number of chemically equivalent atoms. - I5 : float64 + I5 : float Corresponding nuclear spin. - width_gauss : float64 + width_gauss : float Gaussian linewidth in mT. - T_relax_1 : float64 + T_relax_1 : float Longitudinal relaxation time in s. - T_relax_2 : float64 + T_relax_2 : float Transversal relaxation time in s. - decay : float64 + decay : float Exponential decay time for hilbert space simulations in s. - T_pm : float64 + T_pm : float Phase memory time (OOP ESEEM). - population : 1d-Array, np.float64 + population : np.ndarray Populations of the initial density matrix of a triplet precursor in zero field. - amplitude : float64 + amplitude : float Amplitude of the spectra for OOP-ESEEM. """ - def __init__(self) -> None: - """ - Initialize object of class 'Spinsystem' for radical pair simulation. - - Returns - ------- - None. + g1: np.ndarray = Field(default_factory=lambda: np.array([2.002, 2.002, 2.002])) + g2: np.ndarray = Field(default_factory=lambda: np.array([2.004, 2.004, 2.004])) + g_tri: np.ndarray = Field(default_factory=lambda: np.array([2.002, 2.002, 2.002])) + g: np.ndarray = Field(default_factory=lambda: np.array([2.002, 2.002, 2.002])) + A1: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A2: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A3: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A4: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A5: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A_eseem: float = 0.0 + omega_I: float = 0.0 + D: float = 1.0 + D_tri: float = 0.0 + E: float = 0.0 + E_tri: float = 0.0 + J_ex: float = 0.0 + J_0: float = 0.0 + beta: float = 1.4 + g1_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + g2_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + g_tri_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + g_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A1_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A2_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A3_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A4_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + A5_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + D_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + D_tri_frame: np.ndarray = Field(default_factory=lambda: np.zeros(3)) + width_gauss: float = 0.5 + T_relax_1: float = 0.0 + T_relax_2: float = 0.0 + T_pm: float = 0.0 + decay: float = 0.0 + population: np.ndarray = Field(default_factory=lambda: np.array([1.0, 0.0, 0.0])) + amplitude: float = 0.0 + + def __init__(self, **data: Any) -> None: + """Initialize an EPR_Parameters object for radical pair simulation.""" + if data: + super().__init__(**data) + return + super().__init__() - """ # [SPINSYSTEM] self.g1 = np.array([2.002, 2.002, 2.002]) self.g2 = np.array([2.004, 2.004, 2.004]) @@ -175,124 +317,242 @@ def __init__(self) -> None: class Spinsystem(EPR_Parameters): - """ - A class containing all parameters for various radical pair simulations. + """A class containing all parameters for various radical pair simulations. Attributes ---------- - g1_iso : float64 + g1_iso : float Isotropic g value of electron 1. - g2_iso : float64 + g2_iso : float Isotropic g value of electron 2. n1 : int Number of chemically equivalent atoms. - I1 : float64 + I1 : float Corresponding nuclear spin. n2 : int Number of chemically equivalent atoms. - I2 : float64 + I2 : float Corresponding nuclear spin. n3 : int Number of chemically equivalent atoms. - I3 : float64 + I3 : float Corresponding nuclear spin. n4 : int Number of chemically equivalent atoms. - I4 : float64 + I4 : float Corresponding nuclear spin. n5 : int Number of chemically equivalent atoms. - I5 : float64 + I5 : float Corresponding nuclear spin. - donor_list : np.array + donor_list : np.ndarray Defines which atom groups are donor groups. - acceptor_list : np.array + acceptor_list : np.ndarray Defines which atom groups are acceptor groups. frame_group_i : list Define a frame_group which will be used in optimization mode. Each frame group contains the names of the angle lists which always will - have same values during optimization. i is a variable and can be + have same values during optimization. ``i`` is a variable and can be whatever you want. You can define as many frame groups as you want. An - example frame group would be: frame_group_1 = ['A1', 'A2', 'D']. This - list means, that A2_frame and D_frame will always have the same values - as A1_frame, no matter which values were given to them. + example frame group would be: ``frame_group_1 = ['A1', 'A2', 'D']``. + This list means that A2_frame and D_frame will always have the same + values as A1_frame, no matter which values were given to them. spin_system : str Define the spin system by one out of: "rp" (radical pair), "doub" (doublet), "trip" (triplet), "tdp" (triplet-doublet pair). precursor : str State of the precursor. One out of: "zf", "eigen", "singlet", "triplet-zf", "triplet-eigen", "coupled", "basis". - dynamics : np.array + dynamics : np.ndarray Matrix with rate constants of relaxation process in 1/s. For further information see the documentation. distribution_order : int Number of Gaussians used for Multi-Gauss-Fitting. - distribution : np.array + distribution : np.ndarray Distance distribution of the radical pair. Methods ------- - load(profile_name: str) + load(profile_name) Load Spinsystem values from a config file (profile_name.ini). - - Recommended - save(profile_name: str) + save(profile_name) Save the current spinsystem values as a config file (profile_name.ini). - - Recommended _get_g_iso() Determine both g_iso values. Needed in simulation. Examples -------- - Initialize a new object of class : - - >>> Sys = Spinsystem() - >>> Sys.g1 - np.array([2.002, 2.002, 2.002]) - >>> Sys.g1_iso - 2.002 - - Change values: - - >>> Sys.g1 = np.array([2.0024, 2.00381, 2.0027]) - >>> Sys.get_g_iso() - >>> Sys.g1 - np.array([2.0024 , 2.00381, 2.0027 ]) - >>> Sys.g1_iso - 2.00297 - - Create a new spinsystem profile from an empty template and load it: - - >>> Sys_profile = profiles.new_spinsystem_profile() - >>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] # use list not array! - >>> profiles.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1') - >>> Sys_2 = Spinsystem() - >>> Sys_2.load_profile('Sys_prof_1') - >>> Sys_2.g1 - np.array([2.0034 , 2.00156, 2.00228]) - - You can also save your current spinsystem as a new profile: - - >>> Sys_3 = Spinsystem() - >>> Sys_3.g1 = np.array([1, 2, 3]) # either array or list - >>> Sys_3.save('Sys_prof_2') - >>> Sys_4 = Spinsystem() - >>> Sys_4.load('Sys_prof_2') - >>> Sys_4.g1 - np.array([1., 2., 3.]) + Initialize a new object of class ``Spinsystem``:: + + >>> Sys = Spinsystem() + >>> Sys.g1 + np.array([2.002, 2.002, 2.002]) + >>> Sys.g1_iso + 2.002 + + Change values:: + + >>> Sys.g1 = np.array([2.0024, 2.00381, 2.0027]) + >>> Sys.get_g_iso() + >>> Sys.g1 + np.array([2.0024 , 2.00381, 2.0027 ]) + >>> Sys.g1_iso + 2.00297 + + Create a new spinsystem profile from an empty template and load it:: + + >>> Sys_profile = profiles.new_spinsystem_profile() + >>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] + >>> profiles.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1') + >>> Sys_2 = Spinsystem() + >>> Sys_2.load_profile('Sys_prof_1') + >>> Sys_2.g1 + np.array([2.0034 , 2.00156, 2.00228]) + + You can also save your current spinsystem as a new profile:: + + >>> Sys_3 = Spinsystem() + >>> Sys_3.g1 = np.array([1, 2, 3]) + >>> Sys_3.save('Sys_prof_2') + >>> Sys_4 = Spinsystem() + >>> Sys_4.load('Sys_prof_2') + >>> Sys_4.g1 + np.array([1., 2., 3.]) """ - def __init__(self) -> None: - """ - Initialize object of class 'Spinsystem' for radical pair simulation. - - Returns - ------- - None. - - """ + g1_iso: float = Field(default=0.0, exclude=True) + g2_iso: float = Field(default=0.0, exclude=True) + spin_system: str = "rp" + precursor: str = "singlet" + acceptor_list: np.ndarray = Field(default_factory=lambda: np.array([1, 2, 3])) + donor_list: np.ndarray = Field(default_factory=lambda: np.array([4, 5])) + n1: int = 0 + I1: float = 0.0 + n2: int = 0 + I2: float = 0.0 + n3: int = 0 + I3: float = 0.0 + n4: int = 0 + I4: float = 0.0 + n5: int = 0 + I5: float = 0.0 + dynamics: np.ndarray | None = None + distribution_order: int = 3 + distribution: np.ndarray | None = None + + @field_validator("acceptor_list", "donor_list", mode="before") + @classmethod + def convert_nucleus_lists(cls, value: object) -> np.ndarray: + return np.asarray(value, dtype=int) + + @field_validator("n1", "n2", "n3", "n4", "n5", mode="before") + @classmethod + def validate_nuclear_counts(cls, value: object) -> int: + """Require non-negative integer counts for equivalent nuclei.""" + if isinstance(value, bool) or not isinstance(value, (int, np.integer)): + raise ValueError("nuclear counts must be non-negative integers") + if value < 0: + raise ValueError("nuclear counts must be non-negative integers") + return int(value) + + @field_validator("I1", "I2", "I3", "I4", "I5", mode="before") + @classmethod + def validate_nuclear_spins(cls, value: object) -> float: + """Require non-negative nuclear spins in half-integer steps.""" + if isinstance(value, bool): + raise ValueError("nuclear spins must be non-negative half-integers") + try: + spin = float(value) + except (TypeError, ValueError) as error: + raise ValueError( + "nuclear spins must be non-negative half-integers" + ) from error + if ( + not np.isfinite(spin) + or spin < 0 + or not np.isclose(spin * 2, round(spin * 2)) + ): + raise ValueError("nuclear spins must be non-negative half-integers") + return spin + + PROFILE_FIELDS: ClassVar[tuple[str, ...]] = ( + "spin_system", + "precursor", + "acceptor_list", + "donor_list", + "n1", + "I1", + "n2", + "I2", + "n3", + "I3", + "n4", + "I4", + "n5", + "I5", + "dynamics", + "distribution_order", + "distribution", + "g1", + "g2", + "g_tri", + "g", + "A1", + "A2", + "A3", + "A4", + "A5", + "A_eseem", + "omega_I", + "D", + "D_tri", + "E", + "E_tri", + "J_ex", + "J_0", + "beta", + "g1_frame", + "g2_frame", + "g_tri_frame", + "g_frame", + "A1_frame", + "A2_frame", + "A3_frame", + "A4_frame", + "A5_frame", + "D_frame", + "D_tri_frame", + "width_gauss", + "T_relax_1", + "T_relax_2", + "T_pm", + "decay", + "population", + "amplitude", + ) + FRAME_FIELDS: ClassVar[tuple[str, ...]] = ( + "g1_frame", + "g2_frame", + "g_tri_frame", + "g_frame", + "A1_frame", + "A2_frame", + "A3_frame", + "A4_frame", + "A5_frame", + "D_frame", + "D_tri_frame", + ) + + def __init__(self, degree: bool = False, **data: Any) -> None: + """Initialize a Spinsystem object for radical pair simulation.""" + if data: + super().__init__(**data) + self._get_g_iso() + if degree: + self._convert_frames_to_radians() + return super().__init__() # [SPIN SYSTEM] @@ -321,17 +581,31 @@ def __init__(self) -> None: # [DISTRIBUTIONS] self.distribution_order = 3 self.distribution = None + if degree: + self._convert_frames_to_radians() + + def _convert_frames_to_radians(self) -> None: + """Convert all orientation fields from degrees to radians.""" + for key in self.FRAME_FIELDS: + setattr( + self, key, np.asarray(getattr(self, key), dtype=float) * np.pi / 180 + ) def _get_g_iso(self) -> None: """Get g1_iso and g2_iso.""" - self.g1_iso = self.g1.sum() / 3 - self.g2_iso = self.g2.sum() / 3 + self.g1_iso = np.mean(self.g1) + self.g2_iso = np.mean(self.g2) + + def __setattr__(self, name: str, value: object) -> None: + """Keep derived isotropic g values in sync with tensor assignments.""" + super().__setattr__(name, value) + if name in ("g1", "g2") and hasattr(self, "g1") and hasattr(self, "g2"): + self._get_g_iso() def load(self, profile_name: str, degree: bool = False) -> None: - """ - Load a spinsystem from a profile. + """Load a spinsystem from a profile. - Load the settings from [sys.prefix]/easypairspin/profiles/spinsystem/ + Load the settings from ~/.config/spinanalysis/profiles/spinsystem/ [profile_name].ini into the Spinsystem object. Overwrites previous settings. @@ -342,49 +616,39 @@ def load(self, profile_name: str, degree: bool = False) -> None: degree : bool, optional If True, the angle values in the profile are given in degree not radian. Thus, they will be converted to radian. If False, the - angles are given in radian and will therefore not be converted. The - default is 'False'. - - Returns - ------- - None. + angles are given in radian and will therefore not be converted, + default is ``False``. """ spinsystem_profile = profiles.load_profile(profile_name, "spinsystem") - for key in vars(self): - if key not in ("g1_iso", "g2_iso"): - try: - vars(self)[key] = spinsystem_profile["main"][key] - except KeyError: - pass - - try: - if len(vars(self)[key]) == 3: - vars(self)[key] = np.array(vars(self)[key]) - except TypeError: - pass - - if key in ("acceptor_list", "donor_list"): - vars(self)[key] = np.array(vars(self)[key]) - - if key == "distribution": - if vars(self)[key] in ("None", None): - vars(self)[key] = None - else: - vars(self)[key] = np.array(vars(self)[key]) - size = vars(self)[key].size - vars(self)[key] = vars(self)[key].reshape((2, size // 2)) - - if degree: - if key.endswith("_frame"): - vars(self)[key] *= np.pi / 180 + values = spinsystem_profile["main"] + for key in self.PROFILE_FIELDS: + if key not in values: + continue + value = values[key] + if value is None or value == "None": + value = None + elif key == "distribution": + if value is None: + value = None + else: + distribution = np.asarray(value, dtype=float) + if distribution.size % 2: + raise ValueError( + "The distribution profile must contain two rows." + ) + value = distribution.reshape(2, -1) + elif isinstance(value, (list, tuple)): + value = np.asarray(value) + if degree and key in self.FRAME_FIELDS: + value = np.asarray(value, dtype=float) * np.pi / 180 + setattr(self, key, value) self._get_g_iso() - def save(self, profile_name: str = "") -> None: - """ - Save the spinsystem as a profile. + def save(self, profile_name: str = "", degree: bool = False) -> None: + """Save the spinsystem as a profile. Save the spinsystem object as a spinsystem profile using profile_management.add_profile(). Load the spinsystem using @@ -394,43 +658,39 @@ def save(self, profile_name: str = "") -> None: ---------- profile_name : str, optional Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. + will be generated by get_profile_name(), default is ``''``. """ spinsys_profile = profiles.new_spinsystem_profile() for key in spinsys_profile["main"]: - if isinstance(vars(self)[key], np.ndarray): - spinsys_profile["main"][key] = list(vars(self)[key].flatten()) - else: - spinsys_profile["main"][key] = vars(self)[key] + value = getattr(self, key) + if degree and key in self.FRAME_FIELDS: + value = np.asarray(value, dtype=float) * 180 / np.pi + spinsys_profile["main"][key] = ( + value.flatten().tolist() if isinstance(value, np.ndarray) else value + ) profiles.add_profile(spinsys_profile, "spinsystem", pname=profile_name) -class Experimental: - """ - A class containing all experimental parameters and data. +class Experimental(_MutableModel): + """A class containing all experimental parameters and data. Attributes ---------- - B_z : 1d-Array, np.float64 + B_z : np.ndarray External magnetic field points in mT used for simulation. Conversions allowed. - freq_mw : float64 + freq_mw : float Frequency of induced microwave radiation in Gigahertz. - magnetic_field : 1d-Array, np.float64 + magnetic_field : np.ndarray Same as B_z, but will never be changed. - int : np.array, np.complex128 + int : np.ndarray Real and imaginary part of the measured intensities. 1d or 2d. - time_axis : 1d-Array, optional + time_axis : np.ndarray, optional Contains all experimental time points. - spec_sim : 1d-Array, np.float64 + spec_sim : np.ndarray Calculated spectrum. At initialisation empty. Methods @@ -441,81 +701,124 @@ class Experimental: """ + B_z: np.ndarray = Field( + default_factory=lambda: np.linspace(240, 260, 100), exclude=True + ) + magnetic_field: np.ndarray = Field( + default_factory=lambda: np.linspace(240, 260, 100), exclude=True + ) + freq_mw: float = Field(default=9.7e9) + B_mw: float = Field(default=1e-3) + t_scale: list[float] = Field(default_factory=lambda: [0.0, 2e-6], exclude=True) + t_points: int = Field(default=2, exclude=True) + time_axis: np.ndarray | None = Field(default=None, exclude=True) + int: np.ndarray | None = Field(default=None, exclude=True) + spec_sim: np.ndarray = Field(default_factory=lambda: np.zeros(100), exclude=True) + def __init__( self, - magnetic_field: np.array = None, - real_int: np.array = None, - imag_int: np.array = None, - cmplx_int: np.array = None, - time_axis: np.array = None, + magnetic_field: np.ndarray | None = None, + real_int: np.ndarray | None = None, + imag_int: np.ndarray | None = None, + cmplx_int: np.ndarray | None = None, + time_axis: np.ndarray | None = None, rescale: bool = True, + **data: Any, ): """ Initialize object of class 'Experimental' for radical pair simulation. Parameters ---------- - magnetic_field : 1d-Array, np.float64, optional + magnetic_field : np.ndarray, optional Contains all experimental external magnetic field points. - real_int : np.array, np.float64, optional + real_int : np.ndarray, optional Real part of the measured intensities. 1d or 2d. - imag_int : np.array, np.float64, optional + imag_int : np.ndarray, optional Imaginary part of the measured intensities. 1d or 2d. - cmplx_int : np.array, np.complex128, optional + cmplx_int : np.ndarray, optional Real and imaginary part of the measured intensities. 1d or 2d. If this parameter is given, real_int and imag_int will be ignored. - time_axis : 1d-Array, optional + time_axis : np.ndarray, optional Contains all experimental time points in s. - rescale : bool, optional + rescale : bool, optional, default is ``True`` If True, the experimental intensities will be scaled to a maximum - of 1. Default is 'True'. - - Returns - ------- - None. + of 1. """ - self.magnetic_field = magnetic_field - if self.magnetic_field is None: + super().__init__() + input_data = ExperimentalInput( + magnetic_field=magnetic_field, + real_int=real_int, + imag_int=imag_int, + cmplx_int=cmplx_int, + time_axis=time_axis, + rescale=rescale, + ) + + if input_data.magnetic_field is None: self.B_z = np.linspace(240, 260, 100) else: - self.B_z = 1 * magnetic_field # external magnetic field / mT - self.freq_mw = 9.7e9 # Microwave radiation / Hz - self.B_mw = 1e-3 + self.B_z = np.asarray(input_data.magnetic_field, dtype=float) + self.magnetic_field = self.B_z.copy() + self.freq_mw = data.pop("freq_mw", 9.7e9) # Microwave radiation / Hz + self.B_mw = data.pop("B_mw", 1e-3) self.t_scale = [0, 2e-6] self.t_points = 2 - self.time_axis = time_axis - if time_axis is not None: - self.t_scale[0] = time_axis.min() - self.t_scale[1] = time_axis.max() - self.t_points = time_axis.shape[0] + self.time_axis = None + if input_data.time_axis is not None: + self.time_axis = np.asarray(input_data.time_axis, dtype=float) + if self.time_axis.ndim != 1 or self.time_axis.size < 2: + raise ValueError( + "time_axis must be a one-dimensional array with at least two points." + ) + self.t_scale[0] = self.time_axis.min() + self.t_scale[1] = self.time_axis.max() + self.t_points = self.time_axis.size self.int = None - if cmplx_int is not None: - self.int = cmplx_int + if input_data.cmplx_int is not None: + self.int = np.asarray(input_data.cmplx_int, dtype=np.complex128) else: - if real_int is not None: + if input_data.real_int is not None: + real_int = np.asarray(input_data.real_int, dtype=float) self.int = np.zeros(real_int.shape, dtype="complex128") self.int.real = real_int - if imag_int is not None: + if input_data.imag_int is not None: + imag_int = np.asarray(input_data.imag_int, dtype=float) if self.int is not None: + if self.int.shape != imag_int.shape: + raise ValueError( + "real_int and imag_int must have matching shapes." + ) self.int.imag = imag_int else: self.int = np.zeros(imag_int.shape, dtype="complex128") - self.int.imag = real_int + self.int.imag = imag_int - if self.int is not None and rescale: - self.int /= np.abs(self.int).max() - self.spec_sim = np.zeros( - self.int.shape, dtype="complex128" - ) # simulated spectra + if self.int is not None: + if self.int.ndim == 1 and input_data.magnetic_field is not None: + if self.int.shape[0] != self.B_z.size: + raise ValueError( + "The intensity data and magnetic-field axis must have compatible lengths." + ) + if input_data.rescale: + maximum = np.abs(self.int).max() + if maximum: + self.int /= maximum + self.spec_sim = np.zeros(self.int.shape, dtype="complex128") + else: + self.spec_sim = np.zeros(self.B_z.shape, dtype="complex128") def get_linear_time_axis( - self, t_min: float = None, t_max: float = None, t_points: int = None + self, + t_min: float | None = None, + t_max: float | None = None, + t_points: "int | None" = None, ) -> None: """ - Get a linear timea axis for transient simulations. + Get a linear time axis for transient simulations. Get a linear time axis using the given boundaries from self.t_scale with self.t_points points. @@ -525,22 +828,22 @@ def get_linear_time_axis( t_min : float, optional Left boundary of the time axis. If None is given, the current value of self.t_scale[0] will be used. Else, the value of self.t_scale[0] - will be replaced. The default is None. + will be replaced. t_max : float, optional Right boundary of the time axis. If None is given, the current value of self.t_scale[1] will be used. Else, the value of - self.t_scale[1] will be replaced. The default is None. + self.t_scale[1] will be replaced. t_points : int, optional Number of time points. If None is given, the current value of self.t_points will be used. Else, the value of self.t_points will - be replaced. The default is None. - - Returns - ------- - None - Nothing will be returned. + be replaced. """ + if self.time_axis is None and (t_min is None or t_max is None): + raise ValueError( + "t_min and t_max are required when no time_axis is available." + ) + if t_min is not None: self.t_scale[0] = t_min if t_max is not None: @@ -548,6 +851,11 @@ def get_linear_time_axis( if t_points is not None: self.t_points = t_points + if self.t_points < 2: + raise ValueError("t_points must be at least 2.") + if self.t_scale[0] >= self.t_scale[1]: + raise ValueError("t_min must be smaller than t_max.") + self.time_axis = np.linspace(self.t_scale[0], self.t_scale[1], self.t_points) @@ -561,26 +869,22 @@ class Variation(EPR_Parameters): Number of needed digits for chromosomes. number_of_genes : int Number of varied parameters. Used for fp representation. - variation_array : 1d-Array, np.float64 + variation_array : np.ndarray Array with all variation ranges greater 0. boundaries : list Sequence of tuples containing upper and lower bounds for all varied parameters. Used for scipy.optimize. - freq_mw : float64 + freq_mw : float Frequency of induced microwave radiation in Hertz. - bohr_magneton : float64 + bohr_magneton : float Bohr magneton in Hertz/Tesla. Methods ------- load(profile_name: str) Load Spinsystem values from a config file (profile_name.ini). - - Recommended save(profile_name: str) Save the current variation values as a config file (profile_name.ini). - - Recommended get_digits_for_one_par(Par, digits_per_True, one_par=True) Get the number of needed digits for one varied parameter. get_needed_digits() @@ -633,24 +937,85 @@ class Variation(EPR_Parameters): """ - def __init__(self): + bohr_magneton: float = Field(default=0.0, exclude=True) + fit_distribution: bool = False + freq_mw: float = 0.0 + non_vars: tuple[str, ...] = Field(default=(), exclude=True) + single_vars: tuple[str, ...] = Field(default=(), exclude=True) + needed_digits: int = Field(default=0, exclude=True) + number_of_genes: int = Field(default=0, exclude=True) + variation_array: np.ndarray = Field( + default_factory=lambda: np.array([]), exclude=True + ) + boundaries: list[tuple[float, float]] = Field(default_factory=list, exclude=True) + + PROFILE_FIELDS: ClassVar[tuple[str, ...]] = ( + "g1", + "g2", + "g_tri", + "g", + "A1", + "A2", + "A3", + "A4", + "A5", + "A_eseem", + "omega_I", + "D", + "D_tri", + "E", + "E_tri", + "beta", + "J_0", + "J_ex", + "g1_frame", + "g2_frame", + "g_tri_frame", + "g_frame", + "A1_frame", + "A2_frame", + "A3_frame", + "A4_frame", + "A5_frame", + "D_frame", + "D_tri_frame", + "width_gauss", + "T_relax_1", + "T_relax_2", + "T_pm", + "decay", + "population", + "freq_mw", + "amplitude", + ) + + def __init__(self, **data: Any) -> None: """ Initialize object of class 'Variation' for radical pairs in EPR. - Returns - ------- - None. - """ + if data: + super().__init__(**data) + for key in self.PROFILE_FIELDS: + if key not in self.model_fields_set: + value = getattr(self, key) + if isinstance(value, np.ndarray): + setattr(self, key, np.zeros_like(value, dtype=float)) + elif isinstance(value, (float, int)): + setattr(self, key, 0.0) + self._initialize_runtime_fields() + return super().__init__() - for key in vars(self): - if isinstance(vars(self)[key], float): - vars(self)[key] = 0.0 - elif isinstance(vars(self)[key], int): - vars(self)[key] = 0 - else: - for i in range(len(vars(self)[key])): - vars(self)[key][i] = 0.0 + self._initialize_variation_fields() + + def _initialize_variation_fields(self) -> None: + """Initialize variation values and optimizer metadata.""" + for key in self.PROFILE_FIELDS: + value = getattr(self, key) + if isinstance(value, np.ndarray): + setattr(self, key, np.zeros_like(value, dtype=float)) + elif isinstance(value, (float, int)): + setattr(self, key, 0.0) self.bohr_magneton = constant.value("Bohr magneton in Hz/T") @@ -690,11 +1055,49 @@ def __init__(self): "width_gauss", ) + self.needed_digits = 0 + self.number_of_genes = 0 + self.variation_array = np.array([], dtype=float) + self.boundaries = [] + + def _initialize_runtime_fields(self) -> None: + """Initialize metadata when constructing from validated data.""" + self.bohr_magneton = constant.value("Bohr magneton in Hz/T") + self.non_vars = ( + "bohr_magneton", + "needed_digits", + "number_of_genes", + "variation_array", + "boundaries", + "isotropic", + "fit_distribution", + "non_vars", + "single_vars", + ) + self.single_vars = ( + "A_eseem", + "omega_I", + "D", + "D_tri", + "E", + "E_tri", + "beta", + "J_0", + "J_ex", + "T_relax_1", + "T_relax_2", + "T_pm", + "decay", + "freq_mw", + "amplitude", + "width_gauss", + ) + def load(self, profile_name: str, degree: bool = False) -> None: """ - Laod a variation object from a profile. + Load a variation object from a profile. - Load the settings from [sys.prefix]/easypairspin/profiles/variation/ + Load the settings from ~/.config/spinanalysis/profiles/variation/ [profile_name].ini into the Variation object. Overwrites previous settings. @@ -708,29 +1111,19 @@ def load(self, profile_name: str, degree: bool = False) -> None: angles are given in radian and will therefore not be converted. The default is 'False'. - Returns - ------- - None. - """ variation_profile = profiles.load_profile(profile_name, "variation") - for key in vars(self): - if key not in ("g1_iso", "g2_iso", "bohr_magneton"): - try: - vars(self)[key] = variation_profile["main"][key] - except KeyError: - pass - - try: - if len(vars(self)[key]) == 3: - vars(self)[key] = np.array(vars(self)[key]) - except TypeError: - pass - - if degree: - if key.endswith("_frame"): - vars(self)[key] *= np.pi / 180 + values = variation_profile["main"] + for key in self.PROFILE_FIELDS: + if key not in values: + continue + value = values[key] + if isinstance(value, (list, tuple)): + value = np.asarray(value) + if degree and key.endswith("_frame"): + value = np.asarray(value, dtype=float) * np.pi / 180 + setattr(self, key, value) def save(self, profile_name: str = "") -> None: """ @@ -744,21 +1137,16 @@ def save(self, profile_name: str = "") -> None: ---------- profile_name : str, optional Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. + will be generated by get_profile_name(). """ var_profile = profiles.new_variation_profile() for key in var_profile["main"]: - if isinstance(vars(self)[key], np.ndarray): - var_profile["main"][key] = list(vars(self)[key]) - else: - var_profile["main"][key] = vars(self)[key] + value = getattr(self, key) + var_profile["main"][key] = ( + value.tolist() if isinstance(value, np.ndarray) else value + ) profiles.add_profile(var_profile, "variation", pname=profile_name) @@ -766,58 +1154,40 @@ def get_needed_digits(self) -> None: """ Get sum of needed digits for all varied parameters. - Returns - ------- - None. - """ self.get_number_of_genes() self.needed_digits = self.number_of_genes * 12 - return None - def get_number_of_genes(self) -> None: """Determine number of parameters which get varied.""" self.number_of_genes = 0 - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0.0: + for key, value in self._variation_fields(): + if key in self.single_vars: + if value > 0.0: self.number_of_genes += 1 else: - for parameter in vars(self)[key]: - if parameter > 0.0: - self.number_of_genes += 1 - - return None + self.number_of_genes += int(np.count_nonzero(np.asarray(value) > 0.0)) def get_variation_array(self) -> None: - """Put all variation ranges in one 1d-Array.""" + """Put all variation ranges in one 1-D array.""" self.get_number_of_genes() self.variation_array = np.zeros(self.number_of_genes) i = 0 - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0: - self.variation_array[i] = vars(self)[key] + for key, value in self._variation_fields(): + if key in self.single_vars: + if value > 0: + self.variation_array[i] = value i += 1 else: - for parameter in vars(self)[key]: + for parameter in value: if parameter > 0: self.variation_array[i] = parameter i += 1 - return None - def update_digits(self) -> None: """Update number of needed_digits.""" self.get_needed_digits() - return None - def get_boundaries(self, Sys: object) -> None: """ Create a sequence of pairs with all bounds for the varied parameters. @@ -829,21 +1199,14 @@ def get_boundaries(self, Sys: object) -> None: Sys : object Spinsystem object. - Returns - ------- - None - Nothing will be returned. - """ self.boundaries = [] - for key in vars(self): - if key in self.non_vars: - pass - elif key in self.single_vars: - if vars(self)[key] > 0: - mid = vars(Sys)[key] - var = vars(self)[key] + for key, value in self._variation_fields(): + if key in self.single_vars: + if value > 0: + mid = getattr(Sys, key) + var = value lb = mid - var ub = mid + var @@ -854,9 +1217,9 @@ def get_boundaries(self, Sys: object) -> None: bounds = (lb, ub) self.boundaries.append(bounds) else: - for i, parameter in enumerate(vars(self)[key]): + for i, parameter in enumerate(value): if parameter > 0: - mid = vars(Sys)[key][i] + mid = getattr(Sys, key)[i] var = parameter lb = mid - var ub = mid + var @@ -865,6 +1228,10 @@ def get_boundaries(self, Sys: object) -> None: self.boundaries.append(bounds) if self.fit_distribution: + if Sys.distribution is None: + raise ValueError( + "Sys.distribution is required when fit_distribution is enabled." + ) for i in range(Sys.distribution_order): b_int = (0, 1) b_pos = (Sys.distribution[0].min(), Sys.distribution[0].max()) @@ -873,28 +1240,37 @@ def get_boundaries(self, Sys: object) -> None: self.boundaries.append(b_sigma) self.boundaries.append(b_pos) - return None + def _variation_fields(self) -> Any: + """Return variation fields in their stable declaration order.""" + excluded = set(self.non_vars) + return ( + (key, getattr(self, key)) + for key in self.PROFILE_FIELDS + if key not in excluded + ) -class SimulationOptions: - """ - A class containing all simulation options. +class SimulationOptions(_MutableModel): + """A class containing all simulation options. Attributes ---------- routine: str - Name of the simulation routine which will be used by easypairspin() and - easypairspin_optimize(). + Name of the simulation routine which will be used by spinanalysis() and + spinanalysis_optimize(). + knots: int + Number of knots used for spherical grid. grid_points: int - Number of points used for spherical grid. + Deprecated alias for ``knots``. It remains supported for compatibility + but will be removed in a future release. space: str Name of the mathematical space used for some calculations. - pop_evolution : boolean + pop_evolution : bool If set to True, the population evolution in calculated using teacups. - eigval_mode : boolean + eigval_mode : bool If set to True, only the eigenvalues of the system are calculated using teacups. - force_cpu : boolean + force_cpu : bool If True, the simulation will be executed on the CPU, even if GPU is available. Default is False. regularization_mode : int @@ -907,8 +1283,6 @@ class SimulationOptions: ------- load(profile_name: str) Load SimulationOptions values from a config file (profile_name.ini). - - Recommended save(profile_name: str) Save the current simulation options as a config file (profile_name.ini). @@ -918,55 +1292,130 @@ class SimulationOptions: Initialize an object of class : >>> SimOpt = SimulationOptions() - >>> SimOpt.grid_points - 500 + >>> SimOpt.knots + 20 Change values: - >>> SimOpt.grid_points = 1000 - >>> SimOpt.grid_points + >>> SimOpt.knots = 1000 + >>> SimOpt.knots 1000 Save your current values as a new profile: >>> SimOpt.save('SimOpt_prof_1') >>> SimOpt_2 = SimulationOptions() - >>> SimOpt_2.grid_points - 500 + >>> SimOpt_2.knots + 20 >>> SimOpt.load('SimOpt_prof_1') - >>> SimOpt_2.grid_points + >>> SimOpt_2.knots 1000 You can also create a simulation options profile from an empty template: >>> simopt_prof = profiles.new_simulation_profile() - >>> simopt_prof['static_radpair']['grid_points'] = 1100 + >>> simopt_prof['static_radpair']['knots'] = 1100 >>> profiles.add_profile(simopt_prof, 'simulation', 'SimOpt_prof_2') >>> SimOpt_3 = SimulationOptions() - >>> SimOpt_3.grid_points - 500 + >>> SimOpt_3.knots + 20 >>> SimOpt.load('SimOpt_prof_2') - >>> SimOpt_3.grid_points + >>> SimOpt_3.knots 1100 """ - def __init__(self): + routine: str = "" + cpu_cores: int = 0 + knots: int = 20 + grid_points: int = 20 + refinement: int = 1 + space: str = "hilbert" + pop_evolution: bool = True + eigval_mode: bool = False + min_r: float = 10.0 + max_r: float = 50.0 + r_points: int = 401 + fast_mode: bool = False + GCV: bool = False + force_cpu: bool = False + regularization_mode: int = 2 + + @field_validator( + "cpu_cores", "grid_points", "refinement", "r_points", mode="before" + ) + @classmethod + def validate_integer_options(cls, value: object) -> int: + if isinstance(value, bool) or not isinstance(value, (int, np.integer)): + raise ValueError("option must be an integer") + return int(value) + + @field_validator("grid_points", mode="before") + @classmethod + def validate_grid_points(cls, value: object) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, (int, np.integer)) + or value < 1 + ): + raise ValueError("grid_points must be a positive integer") + return int(value) + + @field_validator("knots", mode="before") + @classmethod + def validate_knots(cls, value: object) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, (int, np.integer)) + or value < 1 + ): + raise ValueError("knots must be a positive integer") + return int(value) + + @field_validator( + "eigval_mode", "pop_evolution", "fast_mode", "GCV", "force_cpu", mode="before" + ) + @classmethod + def validate_boolean_options(cls, value: object) -> bool: + if value is None: + return value + if not isinstance(value, (bool, np.bool_)): + raise ValueError("option must be a boolean") + return bool(value) + + def __setattr__(self, name: str, value: object) -> None: + if name == "grid_points": + warnings.warn( + "SimulationOptions.grid_points is deprecated; use knots instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(name, value) + + def __init__(self, **data: Any) -> None: """ Initialize object of class 'Simulation_Options' used for simulations. - Returns - ------- - None. - """ + if "grid_points" in data: + warnings.warn( + "SimulationOptions.grid_points is deprecated; use knots instead.", + DeprecationWarning, + stacklevel=2, + ) + if data: + super().__init__(**data) + return + super().__init__() + # [MAIN] self.routine = "" self.cpu_cores = 0 # [STATIC_RADICAL_PAIR] - self.grid_points = 500 + self.knots = 20 + object.__setattr__(self, "grid_points", 20) self.refinement = 1 # [TEACUPS] @@ -991,7 +1440,7 @@ def load(self, profile_name: str) -> None: """ Load simulation options from a profile. - Load the settings from [sys.prefix]/easypairspin/profiles/simulation/ + Load the settings from ~/.config/spinanalysis/profiles/simulation/ [profile_name].ini into the SimulationOptions object. Overwrites previous settings. @@ -1000,17 +1449,23 @@ def load(self, profile_name: str) -> None: profile_name : str Name of the simulation profile which will be loaded. - Returns - ------- - None. - """ simulation_profile = profiles.load_profile(profile_name, "simulation") - self.routine = simulation_profile["main"]["routine"] + self.routine = simulation_profile["main"]["routine"].lower() self.cpu_cores = simulation_profile["main"]["cpu_cores"] - for key in simulation_profile[self.routine].keys(): - vars(self)[key] = simulation_profile[self.routine][key] + routine_profile = simulation_profile.get(self.routine, {}) + for key, value in routine_profile.items(): + if key == "grid_points": + warnings.warn( + "SimulationOptions.grid_points is deprecated; use knots instead.", + DeprecationWarning, + stacklevel=2, + ) + object.__setattr__(self, "grid_points", value) + continue + if hasattr(self, key): + setattr(self, key, value) def save(self, profile_name: str = "") -> None: """ @@ -1024,44 +1479,46 @@ def save(self, profile_name: str = "") -> None: ---------- profile_name : str, optional Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. + will be generated by get_profile_name(). """ simopt_profile = profiles.new_simulation_profile() - for section in ["main", "static_radpair", "teacups", "opossum"]: + for section in [ + "main", + "static_radpair", + "teacups", + "opossum", + "didelphis", + "didelphis_tikhonov", + ]: for key in simopt_profile[section]: - if isinstance(vars(self)[key], np.ndarray): - simopt_profile[section][key] = list(vars(self)[key]) - else: - simopt_profile[section][key] = vars(self)[key] + if hasattr(self, key): + value = getattr(self, key) + simopt_profile[section][key] = ( + value.tolist() if isinstance(value, np.ndarray) else value + ) profiles.add_profile(simopt_profile, "simulation", pname=profile_name) -class FittingOptions: - """ - A class containing all optimization options. +class FittingOptions(_MutableModel): + """A class containing all optimization options. Attributes ---------- routine : str Name of the optimization routine which will be used by - easypairspin_optimize(). + spinanalysis_optimize(). method : str Name of the optimization method used in the scipy.optimize routines. - x0 : numpy.array, np.float64 + x0 : np.ndarray Array containing the initial guess for the optimization routine for the parameters which will be varied. cpu_cores : int Number of cores used for the optimization. gui : bool - Set to True if in GUI mode (PySpin). The default is False. + Set to True if in GUI mode (PySpin). window : object Plot canvas. Only needed in GUI mode. @@ -1069,16 +1526,12 @@ class FittingOptions: ------- load(profile_name: str) Load FittingOptions values from a config file (profile_name.ini). - - Recommended save(profile_name: str) Save the current fitting options as a config file (profile_name.ini). - Recommended - Examples -------- - Initialize an object of class : + Initialize an object of class : >>> FitOpt = FittingOptions() >>> FitOpt.GAVaPS @@ -1114,15 +1567,122 @@ class FittingOptions: """ - def __init__(self): + routine: str | None = None + method: str | None = None + x0: np.ndarray | None = None + cpu_cores: int = 0 + gui: bool = False + window: Any = None + GAVaPS: bool = True + representation: str | None = None + lifetime_mode: str | None = None + crossover_type: str | None = None + mutation_type: str | None = None + min_lifetime: int | None = None + max_lifetime: int | None = None + reproduction_ratio: float | None = None + p_c: float | None = None + p_m: float | None = None + pop_size: int | None = None + min_pop_size: int | None = None + max_pop_size: int | None = None + convergence: float | None = None + peak_prominence: float | None = None + error_weight: np.ndarray | None = None + max_generation: int | None = None + show_status: bool | None = None + maxiter: int | None = None + maxiter_minimizer: int | None = None + initial_temp: float | None = None + restart_temp_ratio: float | None = None + visit: float | None = None + accept: float | None = None + maxfun: int | None = None + no_local_search: bool | None = None + n: int | None = None + iters: int | None = None + maxfev: int | None = None + f_tol: float | None = None + maxev: int | None = None + maxtime: float | None = None + minimize_every_iter: bool | None = None + local_iter: int | None = None + sampling_method: str | None = None + strategy: str | None = None + popsize: int | None = None + tol: float | None = None + mutation: Any = None + recombination: float | None = None + seed: int | None = None + disp: bool | None = None + polish: bool | None = None + init: Any = None + atol: float | None = None + updating: str | None = None + T: float | None = None + niter: int | None = None + stepsize: float | None = None + interval: int | None = None + niter_success: int | None = None + target_accept_rate: float | None = None + stepwise_factor: float | None = None + ftol: float | None = None + xtol: float | None = None + gtol: float | None = None + loss: str | None = None + f_scale: float | None = None + max_nfev: int | None = None + tr_solver: str | None = None + verbose: int | None = None + + @field_validator( + "cpu_cores", + "maxiter", + "maxiter_minimizer", + "maxfun", + "n", + "iters", + "maxfev", + "maxev", + "max_nfev", + mode="before", + ) + @classmethod + def validate_integer_options(cls, value: object) -> object: + if value is not None and ( + isinstance(value, bool) or not isinstance(value, (int, np.integer)) + ): + raise ValueError("option must be an integer") + return value + + @field_validator( + "gui", + "GAVaPS", + "show_status", + "no_local_search", + "disp", + "polish", + "minimize_every_iter", + mode="before", + ) + @classmethod + def validate_boolean_options(cls, value: object) -> bool: + if value is None: + return value + if not isinstance(value, (bool, np.bool_)): + raise ValueError("option must be a boolean") + return bool(value) + + def __init__(self, **data: Any) -> None: """ Initialize object of class 'FittingOptions' used for optimization. - Returns - ------- - None. - """ + if data: + super().__init__(**data) + return + super().__init__() + # [MAIN] self.routine = None self.method = None @@ -1217,7 +1777,7 @@ def load(self, profile_name: str) -> None: """ Load fitting options from a profile. - Load the settings from [sys.prefix]/easypairspin/profiles/optimization/ + Load the settings from ~/.config/spinanalysis/profiles/optimization/ [profile_name].ini into the FittingOptions object. Overwrites previous settings. Only loads the section given in ['main']['routine']. @@ -1226,17 +1786,16 @@ def load(self, profile_name: str) -> None: profile_name : str Name of the optimization profile which will be loaded. - Returns - ------- - None. - """ fitting_profile = profiles.load_profile(profile_name, "optimization") self.routine = fitting_profile["main"]["routine"] self.cpu_cores = fitting_profile["main"]["cpu_cores"] - for key in fitting_profile[self.routine].keys(): - vars(self)[key] = fitting_profile[self.routine][key] + for key, value in fitting_profile.get(self.routine, {}).items(): + if key == "ftol" and not hasattr(self, key): + key = "f_tol" + if hasattr(self, key): + setattr(self, key, value) def save(self, profile_name: str = "") -> None: """ @@ -1250,12 +1809,7 @@ def save(self, profile_name: str = "") -> None: ---------- profile_name : str, optional Name of the profile. If no profile name is given, a default one - will be generated by get_profile_name(). The default is ''. - - Returns - ------- - None - Nothing will be returned. + will be generated by get_profile_name(). """ fitopt_profile = profiles.new_optimization_profile() @@ -1271,9 +1825,14 @@ def save(self, profile_name: str = "") -> None: "least_squares", ]: for key in fitopt_profile[section]: - if isinstance(vars(self)[key], np.ndarray): - fitopt_profile[section][key] = list(vars(self)[key]) + if key == "ftol" and not hasattr(self, key): + value = getattr(self, "f_tol", None) + elif hasattr(self, key): + value = getattr(self, key) else: - fitopt_profile[section][key] = vars(self)[key] + continue + fitopt_profile[section][key] = ( + value.tolist() if isinstance(value, np.ndarray) else value + ) profiles.add_profile(fitopt_profile, "optimization", pname=profile_name) diff --git a/src/spinanalysis/loading.py b/src/spinanalysis/loading.py index ce6ea9f..a3c5379 100644 --- a/src/spinanalysis/loading.py +++ b/src/spinanalysis/loading.py @@ -1,99 +1,150 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Read EPR data from BRUKER BES3T, ESP transient, MATLAB, and text formats. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @author: Florian Quintes """ -import os -import glob -from typing import Tuple +import warnings +from pathlib import Path + import numpy as np from scipy.io import loadmat + from spinanalysis._utils import strtobool -def get_full_path(directory_name: str, start_directory: str = None) -> str: - """ - Get the full path of a given directory. Search starts at home directory. +def get_full_path(directory_name: str, start_directory: str | None = None) -> str: + """Get the full path of a given directory. Search starts at home directory. Parameters ---------- directory_name : str Name of the directory whose path is to be found. - start_directory : str, optional - Directory at which the search starts. If given, the perfomance - increases very sharply. The default is None. + start_directory : str, optional, default is ``None`` + Directory at which the search starts. If given, the performance + increases very sharply. Recommended. - Recommended. + Raises + ------ + FileNotFoundError + If the directory cannot be found. Returns ------- - full_path : str + str The full path of the directory. """ + home = Path.home() + if start_directory is None: - start_directory = os.path.join(os.path.expanduser("~"), "**") + search_root = home else: - start_directory = os.path.join(os.path.expanduser("~"), start_directory, "**") + search_root = home / start_directory - all_subdirectories = glob.glob(start_directory, recursive=True) + matches: list[Path] = [] + for p in search_root.rglob(directory_name): + if p.is_dir() and p.name == directory_name: + matches.append(p) - full_path = None - for path in all_subdirectories: - if path.endswith(directory_name): - full_path = path + if not matches: + raise FileNotFoundError( + "Directory '{}' not found below '{}'.".format(directory_name, search_root) + ) - return full_path + if len(matches) > 1: + warnings.warn( + "Multiple directories named '{}' found; using the first match.".format( + directory_name + ), + stacklevel=2, + ) + return str(matches[0]) + + +def _find_file(folder: Path, suffix: str) -> Path: + """Find a single file in *folder* with the given *suffix* (case-insensitive). + + Parameters + ---------- + folder : Path + Directory to search for the file. + suffix : str + File suffix to match (case-insensitive). + + Raises + ------ + FileNotFoundError + If no matching file is found. + + Returns + ------- + Path + Path of the first matching file found. -def get_DSC_parameters(path_to_folder: str) -> dict: """ - Extract all parameters from the DSC File. + matches = [ + f + for f in folder.iterdir() + if f.is_file() and f.suffix.lower() == suffix.lower() + ] + if not matches: + raise FileNotFoundError( + "No file with suffix '{}' found in '{}'.".format(suffix, folder) + ) + return matches[0] + + +def get_DSC_parameters(path_to_folder: str) -> dict: + """Extract all parameters from the DSC file. + + The DSC file is found by searching for ``*.DSC`` in the data folder + rather than assuming it shares the folder's basename. Parameters ---------- path_to_folder : str - Full path to the folder with .DSC and .DTA file. Files and path must - have the same basename (BRUKER STANDARD). + Full path to the folder containing the ``.DSC`` and ``.DTA`` files. Returns ------- - DSC_parameters : dict - Dictionary with all parameters. Key is the same as in .DSC. + dict + Dictionary with all parameters. Keys are the same as in the .DSC + file. An additional key ``'path_to_folder'`` holds the folder path """ - DSC_parameters = {} + folder = Path(path_to_folder) + dsc_file = _find_file(folder, ".DSC") - basename = os.path.basename(path_to_folder) # Foldername == Filename - DSC_file = os.path.join(path_to_folder, basename + ".DSC") + dsc_parameters: dict[str, object] = {} - with open(DSC_file, "r") as file: + with open(str(dsc_file), "r") as file: for line in file.readlines(): if line.startswith(("*", "#", "'")): - pass - elif line.startswith("FTAcqModeSlct"): - line = line.split() - DSC_parameters[line[0]] = line[-1] + continue + parts = line.split() + if not parts: + continue + if parts[0] == "FTAcqModeSlct": + dsc_parameters[parts[0]] = parts[-1] + elif len(parts) >= 2: + dsc_parameters[parts[0]] = convert_parameter_type(parts[1]) else: - line = line.split() - try: - DSC_parameters[line[0]] = convert_parameter_type(line[1]) - except IndexError: - pass + dsc_parameters[parts[0]] = True - DSC_parameters["path_to_folder"] = path_to_folder + dsc_parameters["path_to_folder"] = str(folder) - return DSC_parameters + return dsc_parameters -def convert_parameter_type(value: str) -> Tuple[bool, int, float, str]: - """ - Convert the type of a given string to bool, int or float if possible. +def convert_parameter_type(value: str) -> bool | int | float | str: + """Convert the type of a given string to bool, int or float if possible. Parameters ---------- @@ -102,87 +153,85 @@ def convert_parameter_type(value: str) -> Tuple[bool, int, float, str]: Returns ------- - value : bool or int or float or str + bool or int or float or str Parameter as bool, int or float. If none is possible, the stripped string will be returned. """ try: - value = bool(strtobool(value)) - return value + return bool(strtobool(value)) except ValueError: pass try: - value = int(value) - return value + return int(value) except ValueError: pass try: - value = float(value) - return value + return float(value) except ValueError: - value = value.strip("'") - return value + return value.strip("'") def get_byte_mode(DSC_dict: dict, data_key: str = "IRFMT") -> str: - """ - Get the used byte mode of the BRUKER BES3T-data. + """Get the used byte mode of the BRUKER BES3T data. - For more information about BES3T go to BRUKER website or easyspin @ github. + For more information about BES3T go to BRUKER website or easyspin on + GitHub. Parameters ---------- DSC_dict : dict Dictionary with all parameters. Key is the same as in .DSC. - data_key : str, optional + data_key : str, optional, default is ``'IRFMT'`` Key for the data array. Either 'IRFMT' or 'IIFMT'. - The default is 'IRFMT'. Raises ------ ValueError - Raised if key value is not C, S, I, F or D. + If the BSEQ value is not 'BIG' or 'LIT', if the format value is + not one of C, S, I, F, D, or if the format is 'A' (ASCII). Returns ------- - byte_mode : str - Return the used byte mode as one string for - numpy.fromfile(dtype=byte_mode). + str + Byte mode string for ``numpy.fromfile(dtype=...)``. """ byte_mode = "" - if DSC_dict["BSEQ"] == "BIG": + bseq = DSC_dict["BSEQ"] + if bseq == "BIG": byte_mode += ">" - elif DSC_dict["BSEQ"] == "LIT": + elif bseq == "LIT": byte_mode += "<" else: - byte_mode += ">" + raise ValueError("Unknown BSEQ value '{}' in .DSC file!".format(bseq)) - if DSC_dict[data_key] in ("C", "S", "I"): + fmt = DSC_dict[data_key] + if fmt in ("C", "S", "I"): byte_mode += "i" - if DSC_dict[data_key] == "I": + if fmt == "I": byte_mode += "4" - elif DSC_dict[data_key] == "S": + elif fmt == "S": byte_mode += "2" - elif DSC_dict[data_key] in ("F", "D"): + elif fmt in ("F", "D"): byte_mode += "f" - if DSC_dict[data_key] == "D": + if fmt == "D": byte_mode += "8" - elif DSC_dict[data_key] == "A": + elif fmt == "A": raise ValueError("Can't read BES3T data in ASCII format!") else: - raise ValueError("Unknown value for keyword {} in .DSC file!".format(data_key)) + raise ValueError( + "Unknown value for keyword '{}' in .DSC file!".format(data_key) + ) return byte_mode -def load_data_vector(DSC_dict: dict) -> np.array: - """ - Load the binary intensity vector(s) from BRUKER BES3T-File. +def load_data_vector(DSC_dict: dict) -> np.ndarray: + """Load the binary intensity vector(s) from a BRUKER BES3T file. Parameters ---------- @@ -192,44 +241,45 @@ def load_data_vector(DSC_dict: dict) -> np.array: Raises ------ ValueError - Will be raised if IKKF isn't CPLX or REAL or if the dimension isn't 1 - or 2. + If IKKF is not 'CPLX' or 'REAL', or if the dimension is not 1 or 2. KeyError - Raised if an axis is given but not the corresponding number of points. + If an axis is given but the corresponding number of points is + missing. Returns ------- - cmplx_data_vector : np.array, np.complex128 - Real and imaginary part of the measured intensities. If no imaginary - part is measured, zeroes will be inserted. + np.ndarray + Real and imaginary part of the measured intensities as a + ``np.complex128`` array. If no imaginary part is measured, zeros + will be inserted. """ get_data_dimension(DSC_dict) byte_mode = get_byte_mode(DSC_dict) - DTA_file = os.path.join(DSC_dict["path_to_folder"], DSC_dict["TITL"] + ".DTA") + folder = Path(DSC_dict["path_to_folder"]) + dta_file = folder / (DSC_dict["TITL"] + ".DTA") - data_vector = np.fromfile(DTA_file, dtype=byte_mode) + data_vector = np.fromfile(str(dta_file), dtype=byte_mode) if DSC_dict["IKKF"] == "CPLX": data_vector_real = data_vector[0::2] data_vector_imag = data_vector[1::2] elif DSC_dict["IKKF"] == "REAL": - data_vector_real = 1 * data_vector + data_vector_real = data_vector data_vector_imag = np.zeros(len(data_vector)) else: raise ValueError("Unknown value for keyword IKKF!") if DSC_dict["dimensions"] == 2: - if "XPTS" and "YPTS" in DSC_dict: + if "XPTS" in DSC_dict and "YPTS" in DSC_dict: data_vector_real = data_vector_real.reshape( (DSC_dict["YPTS"], DSC_dict["XPTS"]) ) data_vector_imag = data_vector_imag.reshape( (DSC_dict["YPTS"], DSC_dict["XPTS"]) ) - - elif "XPTS" and "ZPTS" in DSC_dict: + elif "XPTS" in DSC_dict and "ZPTS" in DSC_dict: data_vector_real = data_vector_real.reshape( (DSC_dict["ZPTS"], DSC_dict["XPTS"]) ) @@ -237,39 +287,37 @@ def load_data_vector(DSC_dict: dict) -> np.array: (DSC_dict["ZPTS"], DSC_dict["XPTS"]) ) else: - raise KeyError("Can't find needed keys from (XPTS, YPTS, ZPTS)!") - + raise KeyError("Can't find needed keys from (XPTS, YPTS, ZPTS)!") elif DSC_dict["dimensions"] == 1: pass else: - raise ValueError("Can't handle dimensions {0}".format(DSC_dict["dimensions"])) + raise ValueError("Can't handle dimensions {}".format(DSC_dict["dimensions"])) cmplx_data_vector = data_vector_real + 1j * data_vector_imag return cmplx_data_vector.T -def load_axis_vector(axis: str, DSC_dict: dict) -> np.array: - """ - Load the points of a given axis (x, y, z). +def load_axis_vector(axis: str, DSC_dict: dict) -> np.ndarray: + """Load the points of a given axis (x, y, z). Parameters ---------- axis : str - Name of the axis. Needs to start with x,X,y,Y or z,Z. E. g.: x-axis. + Name of the axis. Needs to start with x, X, y, Y or z, Z. DSC_dict : dict Dictionary with all parameters. Key is the same as in .DSC. Raises ------ ValueError - Will be raised if the given axis doesn't starts with an allowed - character. + If the given axis doesn't start with an allowed character. Returns ------- - axis_vector : np.array - Array with all axis points. + np.ndarray + Array with all axis points. Empty array if no axis data is + available. """ axis = axis.upper() @@ -282,18 +330,22 @@ def load_axis_vector(axis: str, DSC_dict: dict) -> np.array: else: raise ValueError("Axis needs to start with 'x', 'y' or 'z'!") - axis_file = os.path.join( - DSC_dict["path_to_folder"], DSC_dict["TITL"] + "." + axis + "GF" - ) + folder = Path(DSC_dict["path_to_folder"]) + axis_file = folder / (DSC_dict["TITL"] + "." + axis + "GF") data_key = axis + "FMT" try: byte_mode = get_byte_mode(DSC_dict, data_key=data_key) - axis_vector = np.fromfile(axis_file, dtype=byte_mode) + axis_vector = np.fromfile(str(axis_file), dtype=byte_mode) except (KeyError, FileNotFoundError): try: minimum = DSC_dict[axis + "MIN"] except KeyError: + warnings.warn( + "No axis data found for axis '{}'; returning empty array.".format(axis), + stacklevel=2, + category=UserWarning, + ) axis_vector = np.array([]) else: width = DSC_dict[axis + "WID"] @@ -304,19 +356,15 @@ def load_axis_vector(axis: str, DSC_dict: dict) -> np.array: def get_data_dimension(DSC_dict: dict) -> None: - """ - Get the dimension of the measured spectrum (1d/2d). + """Determine the dimension of the measured spectrum (1d/2d/3d). + + The result is stored in-place in *DSC_dict* under the key + ``'dimensions'``. Parameters ---------- DSC_dict : dict - Dictionary with all parameters. Key is the same as in .DSC. - - Returns - ------- - None - The dimension will be safed in the given dictionary to the key - 'dimensions'. + Dictionary with all parameters. Modified in place. """ dim = 0 @@ -326,112 +374,114 @@ def get_data_dimension(DSC_dict: dict) -> None: DSC_dict["dimensions"] = dim - return None - def load_epr_bruker_bes3t( - folder: str, start_directory: str = None -) -> Tuple[tuple, np.array]: - """ - Load the whole dataset from BRUKER BES3T data folder into numpy.arrays. + folder: str, start_directory: str | None = None +) -> tuple[tuple[np.ndarray, np.ndarray, np.ndarray], np.ndarray]: + """Load the whole dataset from a BRUKER BES3T data folder. - Time axis will be rescaled for OOP-ESEEM experiments, if 'FTAcqModeSlct' - is 'Run from Tables'. + Time axis will be rescaled for OOP-ESEEM experiments if + ``'FTAcqModeSlct'`` is ``'Tables'``. Parameters ---------- folder : str Name of the data folder with the corresponding data files. - start_directory : str, optional - Give the path starting from your home folder which the search for the - data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='nice'. So - the function call would be: + start_directory : str, optional, default is ``None`` + Give the path starting from your home folder which the search for + the data folder should start at. E. g.: data is at + /home/cooluser/nice/data/this_folder and you know, that all your + data is in /home/cooluser/nice/\\*, then you can give this + start_directory with start_directory='nice'. So the function call + would be:: + load_epr_bruker_bes3t(this_folder, 'nice') - Recommended: - Without this parameter, the search for the right folder will be - much longer. + Recommended: without this parameter, the search for the right + folder will be much longer. Returns ------- - axis : tuple - Tuple of all axis vectors as three numpy.arrays (x, y, z). - data : np.array, np.complex128 - All intensity values as one complex numpy.array. + axis : tuple[np.ndarray, np.ndarray, np.ndarray] + Tuple of all axis vectors as three numpy arrays (x, y, z). + data : np.ndarray + All intensity values as one complex ``np.complex128`` numpy array. """ path = get_full_path(folder, start_directory) - DSC_parameters = get_DSC_parameters(path) + dsc_parameters = get_DSC_parameters(path) - data = load_data_vector(DSC_parameters) - x = load_axis_vector("x", DSC_parameters) + data = load_data_vector(dsc_parameters) + x = load_axis_vector("x", dsc_parameters) try: - if DSC_parameters["FTAcqModeSlct"] == "Tables": + if dsc_parameters["FTAcqModeSlct"] == "Tables": x /= 2 except KeyError: pass - y = load_axis_vector("y", DSC_parameters) - z = load_axis_vector("z", DSC_parameters) + y = load_axis_vector("y", dsc_parameters) + z = load_axis_vector("z", dsc_parameters) axis = (x, y, z) return axis, data def load_epr_ESP_transient( - folder: str, start_directory: str = None -) -> Tuple[tuple, np.array]: - """ - Load data from a transient epr experiment measured with ESP380E. + folder: str, start_directory: str | None = None +) -> tuple[tuple[np.ndarray, np.ndarray], np.ndarray]: + """Load data from a transient EPR experiment measured with ESP380E. Parameters ---------- folder : str - Name of the folder with all data files. Data files need to have the - same basename as folder, e. g. '/home/user/exp_1/exp_1.001'. - start_directory : str, optional + Name of the folder with all data files. + start_directory : str, optional, default is ``None`` Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='nice'. So - the function call would be: + the data folder should start at. E. g.: data is at + /home/cooluser/nice/data/this_folder and you know, that all your + data is in /home/cooluser/nice/\\*, then you can give this + start_directory with start_directory='nice'. So the function call + would be:: + load_epr_ESP_transient(this_folder, 'nice') - Recommended: - Without this parameter, the search for the right folder will be - much longer. + Recommended: without this parameter, the search for the right + folder will be much longer. Returns ------- - axis : tuple - Return a tuple with all axis vectors as two numpy.arrays + axis : tuple[np.ndarray, np.ndarray] + Tuple with all axis vectors as two numpy arrays (magnetic_field, time). - data : np.array, np.complex128 - All intensity values as one complex numpy.array. Imaginary part is - always 0. - + data : np.ndarray + All intensity values as one complex ``np.complex128`` numpy array. + Imaginary part is always 0. """ path = get_full_path(folder, start_directory) - # tr_info = get_transient_info(path) - # time_axis = np.linspace(0, tr_info[0], tr_info[1]) - # points = int((abs(tr_info[3] - tr_info[2])) / tr_info[4] + 1) - # magnetic_field = np.linspace(tr_info[2], tr_info[3], points) - # axis = (magnetic_field, time_axis) - - # dimensions = (magnetic_field.shape[0], time_axis.shape[0]) axis, data = get_transient_data(path) return axis, data -def get_transient_info(fpath: str) -> tuple: - """ - Get all informations about the time axis and magnetic field vector from the - .info file. +def _validate_info_file(sections_found: set[str]) -> None: + """Warn if expected sections were not found in the .info file.""" + expected = {"MAGNETIC", "TRANSIENT"} + missing = expected - sections_found + if missing: + warnings.warn( + "Sections {} not found in .info file. The file may be " + "corrupted or from a different spectrometer.".format( + ", ".join(repr(s) for s in sorted(missing)) + ), + stacklevel=3, + ) + + +def get_transient_info(fpath: str) -> tuple[float | int | None, ...]: + """Get all information about the time axis and magnetic field vector from + the .info file. + + The .info file is found by searching for ``*.info`` in the data folder. Parameters ---------- @@ -441,52 +491,50 @@ def get_transient_info(fpath: str) -> tuple: Raises ------ KeyError - Raised if section 'MAGNETIC FIELD' or 'TRANSIENT' could not be found in - .info file. Take care that the sections must be all upper case. + If the section 'MAGNETIC FIELD' or 'TRANSIENT' could not be found + in the .info file. Returns ------- - transient_info : tuple - Contains the needed informations in the following order (time_length, - time_points, mag_field_start, mag_field_stop, mag_field_step). + tuple[float | int | None, ...] + Contains (time_length, time_points, mag_field_start, + mag_field_stop, mag_field_step). """ - fbasename = os.path.basename(fpath) - fname = os.path.join(fpath, fbasename + ".info") - transient_info = [None] * 5 + folder = Path(fpath) + info_file = _find_file(folder, ".info") + + transient_info: list[float | int | None] = [None] * 5 + sections_found: set[str] = set() - with open(fname, "r") as info_file: + with open(str(info_file), "r") as file: current_section = "" - for line in info_file.readlines(): - line = line.strip().split() - if line == []: - pass - else: - if line[0].isupper(): - current_section = line[0] - else: - if current_section == "MAGNETIC": - if line[0] == "Start:": - transient_info[2] = float(line[1]) - elif line[0] == "Stop:": - transient_info[3] = float(line[1]) - elif line[0] == "Step:": - transient_info[4] = float(line[1]) - else: - pass - elif current_section == "TRANSIENT": - if line[0] == "Points:": - transient_info[1] = int(line[1]) - elif line[0] == "Length:": - transient_info[0] = float(line[1]) - else: - pass - else: - pass + for line in file.readlines(): + parts = line.strip().split() + if not parts: + continue + if parts[0].isupper(): + current_section = parts[0] + sections_found.add(current_section) + continue + if current_section == "MAGNETIC": + if parts[0] == "Start:": + transient_info[2] = float(parts[1]) + elif parts[0] == "Stop:": + transient_info[3] = float(parts[1]) + elif parts[0] == "Step:": + transient_info[4] = float(parts[1]) + elif current_section == "TRANSIENT": + if parts[0] == "Points:": + transient_info[1] = int(parts[1]) + elif parts[0] == "Length:": + transient_info[0] = float(parts[1]) if None not in transient_info: break + _validate_info_file(sections_found) + if transient_info[0] is None: raise KeyError("Couldn't find section 'TRANSIENT' in .info file!") if transient_info[2] is None: @@ -495,10 +543,54 @@ def get_transient_info(fpath: str) -> tuple: return tuple(transient_info) -def get_transient_data(fpath: str) -> Tuple[tuple, np.array]: +def _find_transient_basename(folder: Path) -> str: + """Find the shared basename of numbered transient data files. + + Files are named ``.`` where the number width + depends on the total count of traces (e.g. ``.1``, ``.01``, + ``.001``). This function globs for files with numeric suffixes and + returns the common basename. + + Raises + ------ + FileNotFoundError + If no numbered data files are found. + """ + candidates: list[Path] = [] + for f in folder.iterdir(): + if not f.is_file(): + continue + suffix = f.suffix + if suffix and suffix[1:].isdigit(): + candidates.append(f) + + if not candidates: + raise FileNotFoundError( + "No numbered transient data files found in '{}'.".format(folder) + ) + + candidates.sort() + return candidates[0].stem + + +def _discover_max_digits(folder: Path, basename: str) -> int: + """Discover the digit width used for the numbered file suffixes. + + Tries increasing widths (1, 2, 3, ...) until a file + ``.`` is found. """ - Get the measured intensities of the whole spectrum from a transient epr - experiment measured by ESP380E. + for width in range(1, 10): + fname = folder / (basename + "." + str(1).zfill(width)) + if fname.is_file(): + return width + raise FileNotFoundError( + "No first transient data file could be found in '{}'.".format(folder) + ) + + +def get_transient_data(fpath: str) -> tuple[tuple[np.ndarray, np.ndarray], np.ndarray]: + """Get the measured intensities of the whole spectrum from a transient + EPR experiment measured by ESP380E. Parameters ---------- @@ -507,121 +599,119 @@ def get_transient_data(fpath: str) -> Tuple[tuple, np.array]: Returns ------- - axis : tuple - Return a tuple with all axis vectors as two numpy.arrays + axis : tuple[np.ndarray, np.ndarray] + Tuple with all axis vectors as two numpy arrays (magnetic_field, time). - cmplx_data_vector : np.array, np.complex128 - Measured real intensities. All imaginary parts are zero. + data : np.ndarray + Measured real intensities as a ``np.complex128`` numpy array. All + imaginary parts are zero. """ - data_vector_real = [] - magnetic_field = [] - - file_found = False - max_digits = 1 - while not file_found: - fname = os.path.join( - fpath, - os.path.basename(fpath) + ".{0:0>{1}s}".format("1", str(max_digits)), - ) - if os.path.isfile(fname): - file_found = True - else: - max_digits += 1 + folder = Path(fpath) + basename = _find_transient_basename(folder) + max_digits = _discover_max_digits(folder, basename) - if max_digits >= 10: - raise FileNotFoundError("No first ESP file could be found!") + data_vector_real: list[np.ndarray] = [] + magnetic_field: list[float] = [] file_exists = True - file = 1 + file_num = 1 + time_axis: np.ndarray | None = None + while file_exists: - try: - if file == 1: - (field_point, time_axis), data = read_single_transient_file( - fpath, file, max_digits, time=True - ) - else: - field_point, data = read_single_transient_file(fpath, file, max_digits) - file += 1 - except OSError: + ending = "." + str(file_num).zfill(max_digits) + fname = folder / (basename + ending) + if not fname.is_file(): file_exists = False + continue + + if file_num == 1: + (field_point, time_axis), data = read_single_transient_file( + str(folder), basename, file_num, max_digits, time=True + ) else: - data_vector_real.append(data) - magnetic_field.append(round(field_point, 1)) + field_point, data = read_single_transient_file( + str(folder), basename, file_num, max_digits + ) + data_vector_real.append(data) + magnetic_field.append(round(field_point, 1)) + file_num += 1 - magnetic_field = np.array(magnetic_field) - sorting = np.argsort(magnetic_field) - magnetic_field = magnetic_field[sorting] + magnetic_field_arr = np.array(magnetic_field) + sorting = np.argsort(magnetic_field_arr) + magnetic_field_arr = magnetic_field_arr[sorting] cmplx_data_vector = np.array(data_vector_real, dtype=np.complex128) cmplx_data_vector = cmplx_data_vector[sorting] - axis = (magnetic_field, time_axis) + axis = (magnetic_field_arr, time_axis) return axis, cmplx_data_vector.T def read_single_transient_file( fpath: str, + basename: str, filenumber: int, digits: int, time: bool = False, -) -> Tuple[float, np.array]: - """ - Get the measured intensities of a single field point from a transient epr - experiment measured by ESP380E. +) -> tuple[float, np.ndarray] | tuple[tuple[float, np.ndarray], np.ndarray]: + """Get the measured intensities of a single field point from a transient + EPR experiment measured with ESP380E. Parameters ---------- fpath : str Full path to the folder with the experimental data. + basename : str + Shared basename of the numbered data files. filenumber : int - Number of the dataset for the magnetic field point e. g. 003. + Number of the dataset for the magnetic field point, e. g. 3. digits : int - Number of digits from the highest filenumber (ESP380E has an increasing - number as fileextension for each magnetic field point e. g. data.001). - time : bool, optional - If True, the time axis will be returned. + Number of digits in the file-number suffix. + time : bool, optional, default is ``False`` + If True, the time axis will also be returned. Returns ------- field : float - Magnetic field point. - time_axis : np.array - Linear time axis. - data_vector : np.array + Magnetic field point. Returned alone with *data_vector* when + *time* is False. + data_vector : np.ndarray Measured intensities. + time_axis : tuple[float, np.ndarray] + Tuple of (field, time_axis) returned with *data_vector* when + *time* is True. """ - fbasename = os.path.basename(fpath) - ending = ".{0:0>{1}s}".format(str(filenumber), str(digits)) - fname = os.path.join(fpath, fbasename + ending) + folder = Path(fpath) + ending = "." + str(filenumber).zfill(digits) + fname = folder / (basename + ending) + + with open(str(fname), "r") as file: + lines = file.readlines() + + field: float = 0.0 + time_axis: np.ndarray | None = None + + for i, line in enumerate(lines): + if i >= 5: + break + parts = line.strip().split() + if parts and parts[0] == "B0": + field = float(parts[2]) + if i == 3 and time: + points = int(parts[1]) + start = float(parts[2]) + stop = float(parts[3]) + time_axis = np.linspace(start, stop, points) try: - data_vector = np.genfromtxt(fname, skip_header=5) + data_vector = np.genfromtxt(str(fname), skip_header=5) data_vector = data_vector.flatten() except ValueError: - data_vector = [] - with open(fname) as file: - for i, line in enumerate(file.readlines()): - if i < 5: - continue - else: - line = line.strip().split() - data_vector.extend(line) - data_vector = np.array(data_vector, dtype=np.complex128) - - with open(fname) as file: - for i, line in enumerate(file.readlines()): - if line.startswith("B0"): - line = line.strip().split() - field = float(line[2]) - if i == 3 and time: - line = line.strip().split() - points = int(line[1]) - start = float(line[2]) - stop = float(line[3]) - time_axis = np.linspace(start, stop, points) - if i == 4: - break + data_lines: list[str] = [] + for line in lines[5:]: + data_lines.extend(line.strip().split()) + data_vector = np.array(data_lines, dtype=np.complex128) if time: return (field, time_axis), data_vector @@ -630,91 +720,102 @@ def read_single_transient_file( def load_simulated_data( - folder: str, start_directory: str = None -) -> Tuple[np.array, np.array, np.array]: - """ - Load the simulated data from EasyPairSpin or data saved with - saving.save_simulation(). Using numpy.loadtxt(). + folder: str, start_directory: str | None = None +) -> tuple[np.ndarray, ...]: + """Load simulated data from spinanalysis or data saved with + :func:`saving.save_simulation`. Uses :func:`numpy.loadtxt`. Parameters ---------- folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional + Name of the data folder with the corresponding data files + (``x_axis.txt``, ``intensity.txt``, optionally ``y_axis.txt``). + start_directory : str, optional, default is ``None`` Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: + the data folder should start at. E. g.: data is at + /home/cooluser/nice/data/this_folder and you know, that all your + data is in /home/cooluser/nice/\\*, then you can give this + start_directory with start_directory='cooluser/nice'. So the + function call would be:: + load_simulated_data(this_folder, 'cooluser/nice') - Recommended: - Without this parameter, the search for the right folder - will be much longer. + Recommended: without this parameter, the search for the right + folder will be much longer. Returns ------- - x : np.array + x : np.ndarray Axis vector for the x-axis. - y : np.array - Axis vector for the y-axis. Only returned, if the simulated data is 2d. - intensity : np.array, np.complex128 - Simulated intensities. Either 1d or 2d. + y : np.ndarray + Axis vector for the y-axis. Only returned if the simulated data + is 2d. + intensity : np.ndarray + Simulated intensities as a ``np.complex128`` numpy array. Either + 1d or 2d. """ path = get_full_path(folder, start_directory) + data_folder = Path(path) - x = np.loadtxt(os.path.join(path, "x_axis.txt")) + x = np.loadtxt(str(data_folder / "x_axis.txt")) try: - intensity = np.loadtxt(os.path.join(path, "intensity.txt")) + intensity = np.loadtxt(str(data_folder / "intensity.txt")) except ValueError: - intensity = np.loadtxt(os.path.join(path, "intensity.txt"), dtype=np.complex_) + intensity = np.loadtxt(str(data_folder / "intensity.txt"), dtype=np.complex_) try: - y = np.loadtxt(os.path.join(path, "y_axis.txt")) - except IOError: + y = np.loadtxt(str(data_folder / "y_axis.txt")) + except OSError: return (x, intensity) - else: - return (x, y, intensity) + + return (x, y, intensity) def load_matlab( folder: str, - start_directory: str = None, + start_directory: str | None = None, field: str = "field", signal: str = "signal", -) -> Tuple[np.array, np.array, np.array]: - """ - Load EPR data from a matlab data file .mat +) -> tuple[tuple[np.ndarray, np.ndarray], np.ndarray]: + """Load EPR data from a MATLAB data file (``.mat``). + + The *folder* argument is the name of the directory containing the + ``.mat`` file. The first ``.mat`` file found in the directory is + loaded. Parameters ---------- folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional + Name of the data folder with the ``.mat`` file. + start_directory : str, optional, default is ``None`` Give the path starting from your home folder which the search for - the data folder should start at. The default is None. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: - load_simulated_data(this_folder, 'cooluser/nice') - field : str, optional - Name of the field array. The default is 'field'. - signal : str, optional - Name of the signal array. The default is 'signal'. + the data folder should start at. E. g.: data is at + /home/cooluser/nice/data/this_folder and you know, that all your + data is in /home/cooluser/nice/\\*, then you can give this + start_directory with start_directory='cooluser/nice'. So the + function call would be:: + + load_matlab(this_folder, 'cooluser/nice') + + field : str, optional, default is ``'field'`` + Name of the field array in the .mat file. + signal : str, optional, default is ``'signal'`` + Name of the signal array in the .mat file. Returns ------- - axis : np.array - Tuple of np.array containing the x and y axis. - data : np.array - np.array with the measured intensities. + axis : tuple[np.ndarray, np.ndarray] + Tuple of np.ndarray containing the x and y axis. + data : np.ndarray + np.ndarray with the measured intensities. """ path = get_full_path(folder, start_directory) - mat_data = loadmat(path) + data_folder = Path(path) + mat_file = _find_file(data_folder, ".mat") + + mat_data = loadmat(str(mat_file)) x = mat_data[field].flatten() y = np.array([]) data = mat_data[signal].flatten() @@ -724,45 +825,64 @@ def load_matlab( def load_txt( - folder: str, start_directory: str = None -) -> Tuple[np.array, np.array, np.array]: - """ - Load EPR data from a .txt file. + folder: str, start_directory: str | None = None +) -> tuple[tuple[np.ndarray, np.ndarray], np.ndarray]: + """Load EPR data from a ``.txt`` file. + + The *folder* argument is the name of the directory containing the + ``.txt`` file. The first ``.txt`` file found in the directory is + loaded. Parameters ---------- folder : str - Name of the data folder with the corresponding data files. - start_directory : str, optional + Name of the data folder with the ``.txt`` file. + start_directory : str, optional, default is ``None`` Give the path starting from your home folder which the search for - the data folder should start at. The default is ''. - E. g.: data is at /home/cooluser/nice/data/this_folder - and you know, that all your data is in /home/cooluser/nice/*, then you - can give this start_directory with start_directory='cooluser/nice'. So - the function call would be: - load_simulated_data(this_folder, 'cooluser/nice') + the data folder should start at. E. g.: data is at + /home/cooluser/nice/data/this_folder and you know, that all your + data is in /home/cooluser/nice/\\*, then you can give this + start_directory with start_directory='cooluser/nice'. So the + function call would be:: + + load_txt(this_folder, 'cooluser/nice') Returns ------- - axis : np.array - Tuple of np.array containing the x and y axis. - data : np.array - np.array with the measured intensities. + axis : tuple[np.ndarray, np.ndarray] + Tuple of np.ndarray containing the x and y axis. + data : np.ndarray + np.ndarray with the measured intensities. """ path = get_full_path(folder, start_directory) - try: - x, data = np.loadtxt(path) + data_folder = Path(path) + txt_file = _find_file(data_folder, ".txt") + + n_cols = 0 + with open(str(txt_file), "r") as f: + for line in f: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + n_cols = len(stripped.split()) + break + + if n_cols == 0: + raise ValueError("File '{}' contains no data.".format(txt_file)) + + data = np.loadtxt(str(txt_file), unpack=True) + + if n_cols == 2: + x, intensity = data y = np.array([]) - except ValueError: - try: - x, data = np.loadtxt(path, unpack=True) - y = np.array([]) - except ValueError: - try: - x, y, data = np.loadtxt(path) - except ValueError: - x, y, data = np.loadtxt(path, unpack=True) + elif n_cols == 3: + x, y, intensity = data + else: + raise ValueError( + "Expected 2 or 3 columns in '{}', got {}.".format(txt_file, n_cols) + ) + axis = (x, y) - return axis, data + return axis, intensity diff --git a/src/spinanalysis/plotting.py b/src/spinanalysis/plotting.py index ceef098..f0bac90 100644 --- a/src/spinanalysis/plotting.py +++ b/src/spinanalysis/plotting.py @@ -1,92 +1,111 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Render EPR spectra as 2D, 3D, heatmap, and shifted-line plots. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @author: Florian Quintes """ -from matplotlib import cm +from typing import Any import matplotlib.pyplot as plt import numpy as np -import os -import sys +import warnings +from contextlib import contextmanager from spinanalysis import profiles -class HiddenPrints: - """Supress Error Messages in a context manager.""" +@contextmanager +def _suppress_warnings(): + """Suppress warnings in a context manager.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + yield + - def __enter__(self): - """Deactivate the error stream when entering the context manager.""" - self._original_stderr = sys.stderr - sys.stderr = open(os.devnull, "w") +def _prepare_style(mpl_stylesheet: str, kwargs: dict) -> tuple[dict, list]: + """Load the plot profile and build the style list. - def __exit__(self, exc_type, exc_val, exc_tb): - """Activate the error stream when leaving the context manager.""" - sys.stderr.close() - sys.stderr = self._original_stderr + Profile keys that also appear in *kwargs* are moved from *kwargs* + into the returned profile dict, so that the caller's *kwargs* + only contains keys meant for the matplotlib plotting function. + + Parameters + ---------- + mpl_stylesheet : str + Name of the matplotlib style sheet / plotting profile. + kwargs : dict + Keyword arguments from the caller; modified in place. + + Returns + ------- + profile : dict + Plotting profile with kwargs overrides applied. + style_list : list + List of style paths suitable for ``plt.style.context``. + + """ + profile = profiles.load_plot_profile(mpl_stylesheet) + profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) + for k in profile.keys() & kwargs.keys(): + kwargs.pop(k) + style = _get_style_path(mpl_stylesheet) + style_list = _load_default_styles() + style_list.append(style) + return profile, style_list def plot_2D( - x: np.array, - y: np.array, + x: np.ndarray, + y: np.ndarray, mpl_stylesheet: str = "default_stylesheet", - labels: list[str] = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot the given y value(s) against the given x array. + labels: list[str] | str = "no_label", + ax: plt.Axes | None = None, + **kwargs: Any, +) -> plt.Figure: + """Plot the given y value(s) against the given x array. Using matplotlib.pylab.plot(). The plot can be configured via plot profiles. Parameters ---------- - x : np.array + x : np.ndarray Array with values for the x axis. - y : np.array + y : np.ndarray 1D-Array or 2D-Array with values for y axis. - mpl_stylesheet : str, optional + mpl_stylesheet : str, default is ``'default_stylesheet'`` Name of the matplotlib style sheet (see: matplotlib documentation). If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : list[str], optional + will be used. + labels : list[str] or str, default is ``'no_label'`` List of labels for the legend. If only one label is given, all labels - will be the same. The default is 'no_label'. - ax : object, optional + will be the same. + ax : plt.Axes, optional Axes object, used for the PySpin GUI. - **kwargs : optional + **kwargs Keyword arguments passed to the matplotlib plot function. Overrides the arguments given in the stylesheet. Returns ------- - fig : object - Figure object of matplotlib.pylab. + fig : plt.Figure + Figure object of matplotlib. """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) + profile, style_list = _prepare_style(mpl_stylesheet, kwargs) - with HiddenPrints(): - with plt.style.context(default_styles): + with _suppress_warnings(): + with plt.style.context(style_list): if ax is None: - # [NEW FIGURE] fig, ax = plt.subplots() else: - fig, _ = plt.subplots() + fig = ax.figure # [LABELS] - if y.ndim > 1 and not (type(labels) is list): + if y.ndim > 1 and not isinstance(labels, list): labels = _get_label_list(y.shape[0], labels) # [PLOT DATA] @@ -98,59 +117,52 @@ def plot_2D( # [Axes] _set_axis(ax, profile, x, y) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) + _set_figure(ax, profile, mpl_stylesheet=mpl_stylesheet) return fig def shifted_2D( - x: np.array, - Y: np.array, + x: np.ndarray, + Y: np.ndarray, mpl_stylesheet: str = "default_stylesheet", - labels: list[str] = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot multiples lines in 2D, shifted vertically. + labels: list[str] | str = "no_label", + ax: plt.Axes | None = None, + **kwargs: Any, +) -> plt.Figure: + """Plot multiple lines in 2D, shifted vertically. The plot can be configured via plot profiles. Parameters ---------- - x : np.array + x : np.ndarray Array with values for the x axis. - Y : np.array + Y : np.ndarray 2D-Array with values for y axis. - mpl_stylesheet : str, optional + mpl_stylesheet : str, default is ``'default_stylesheet'`` Name of the matplotlib style sheet (see: matplotlib documentation). If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : list[str], optional + will be used. + labels : list[str] or str, default is ``'no_label'`` List of labels for the legend. If only one label is given, all labels - will be the same. The default is 'no_label'. - ax : object, optional + will be the same. + ax : plt.Axes, optional Axes object, used for the PySpin GUI. - **kwargs : optional + **kwargs Keyword arguments passed to the matplotlib plot function. Overrides the arguments given in the stylesheet. Returns ------- - fig : object - Figure object of matplotlib.pylab. + fig : plt.Figure + Figure object of matplotlib. """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) + profile, style_list = _prepare_style(mpl_stylesheet, kwargs) - with HiddenPrints(): - with plt.style.context(default_styles): + with _suppress_warnings(): + with plt.style.context(style_list): # [NORMALIZE DATA] Y = Y / np.max(np.abs(Y)) @@ -159,13 +171,12 @@ def shifted_2D( Y += shift_matrix if ax is None: - # [NEW FIGURE] fig, ax = plt.subplots() else: - fig = None + fig = ax.figure # [LABELS] - if not type(labels) is list: + if not isinstance(labels, list): labels = _get_label_list(Y.shape[0], labels) # [PLOT DATA] @@ -174,220 +185,190 @@ def shifted_2D( # [AXES] _set_axis(ax, profile, x, Y, y_axis=False) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) + _set_figure(ax, profile, mpl_stylesheet=mpl_stylesheet) return fig def plot_3D( - x: np.array, - y: np.array, - Z: np.array, + x: np.ndarray, + y: np.ndarray, + Z: np.ndarray, mpl_stylesheet: str = "default_stylesheet", labels: str = "no_label", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data in 3D using matplotlib.pylab.plot_surface(). + ax: plt.Axes | None = None, + **kwargs: Any, +) -> plt.Figure: + """Plot 2D data in 3D using matplotlib.pylab.plot_surface(). The plot can be configured via plot profiles. Parameters ---------- - x : np.array + x : np.ndarray Array with values for the x axis. - y : np.array + y : np.ndarray Array with values for the y axis. - Z : np.array + Z : np.ndarray 2D-Array with intensities. - mpl_stylesheet : str, optional + mpl_stylesheet : str, default is ``'default_stylesheet'`` Name of the matplotlib style sheet (see: matplotlib documentation). If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - labels : str, optional - At the moment no function. The default is 'no_label'. # TODO - ax : object, optional + will be used. + labels : str, default is ``'no_label'`` + At the moment no function. + ax : plt.Axes, optional Axes object, used for the PySpin GUI. - **kwargs : optional + **kwargs Keyword arguments passed to the matplotlib plot function. Overrides the arguments given in the stylesheet. Returns ------- - fig : object - Figure object of matplotlib.pylab. + fig : plt.Figure + Figure object of matplotlib. """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) + profile, style_list = _prepare_style(mpl_stylesheet, kwargs) - kwargs_ = {"cmap": cm.coolwarm, "antialiased": True, "linewidth": 0} + kwargs_ = {"cmap": "coolwarm", "antialiased": True, "linewidth": 0} kwargs_.update(kwargs) - with HiddenPrints(): - with plt.style.context(default_styles): + with _suppress_warnings(): + with plt.style.context(style_list): if ax is None: - # [NEW FIGURE] fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) else: - fig = None + fig = ax.figure # [GRID] X, Y = np.meshgrid(x, y) # [PLOT DATA] - try: - ax.plot_surface(X, Y, Z, **kwargs_) - except ValueError: - ax.plot_surface(X, Y, Z.T, **kwargs_) + Z = _align_z(x, y, Z) + ax.plot_surface(X, Y, Z, **kwargs_) # [AXES] _set_axis(ax, profile, X, Y, Z) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) + _set_figure(ax, profile, mpl_stylesheet=mpl_stylesheet) return fig def plot_3D_multiple_lines( - x: np.array, - y: np.array, - Z: np.array, + x: np.ndarray, + y: np.ndarray, + Z: np.ndarray, mpl_stylesheet: str = "default_stylesheet", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data in 3D using matplotlib.pylab.plot(). + ax: plt.Axes | None = None, + **kwargs: Any, +) -> plt.Figure: + """Plot 2D data in 3D using matplotlib.pylab.plot(). Each y trace as a single line plot. The plot can be configured via plot profiles. Parameters ---------- - x : np.array + x : np.ndarray Array with values for the x axis. - y : np.array + y : np.ndarray Array with values for the y axis. - Z : np.array + Z : np.ndarray 2D-Array with intensities. - mpl_stylesheet : str, optional + mpl_stylesheet : str, default is ``'default_stylesheet'`` Name of the matplotlib style sheet (see: matplotlib documentation). If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - ax : object, optional + will be used. + ax : plt.Axes, optional Axes object, used for the PySpin GUI. - **kwargs : optional + **kwargs Keyword arguments passed to the matplotlib plot function. Overrides the arguments given in the stylesheet. Returns ------- - fig : object - Figure object of matplotlib.pylab. + fig : plt.Figure + Figure object of matplotlib. """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) + profile, style_list = _prepare_style(mpl_stylesheet, kwargs) - with HiddenPrints(): - with plt.style.context(default_styles): + with _suppress_warnings(): + with plt.style.context(style_list): if ax is None: - # [NEW FIGURE] fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) else: - fig = None + fig = ax.figure # [PLOT DATA] - # TODO: Hier muss drüber nachgedacht werden, wann Z transponiert werden - # muss, damit man die x- und y-Achse in beliebiger Reihenfolge geben - # kann. + Z = _align_z(x, y, Z) X = np.ones((len(y), len(x))) * x X = X.T - for dataset in range(len(Z.T)): + for dataset in range(Z.shape[1]): ax.plot(X[dataset], y, Z[:, dataset], **kwargs) # [AXES] _set_axis(ax, profile, x, y, Z) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) + _set_figure(ax, profile, mpl_stylesheet=mpl_stylesheet) return fig def heatmap( - x: np.array, - y: np.array, - Z: np.array, + x: np.ndarray, + y: np.ndarray, + Z: np.ndarray, mpl_stylesheet: str = "default_stylesheet", - ax: object = None, - **kwargs, -) -> object: - """ - Plot 2D Data as a heatmap using matplotlib.pylab.pcolormesh(). + ax: plt.Axes | None = None, + **kwargs: Any, +) -> plt.Figure: + """Plot 2D data as a heatmap using matplotlib.pylab.pcolormesh(). The plot can be configured via plot profiles. Parameters ---------- - x : np.array + x : np.ndarray Array with values for the x axis. - y : np.array + y : np.ndarray Array with values for the y axis. - Z : np.array + Z : np.ndarray 2D-Array with intensities. - mpl_stylesheet : str, optional + mpl_stylesheet : str, default is ``'default_stylesheet'`` Name of the matplotlib style sheet (see: matplotlib documentation). If no style sheet is given, the styles defined in the plotting profile - will be used. The default is None. - ax : object, optional + will be used. + ax : plt.Axes, optional Axes object, used for the PySpin GUI. - **kwargs : optional + **kwargs Keyword arguments passed to the matplotlib plot function. Overrides the arguments given in the stylesheet. Returns ------- - fig : object - Figure object of matplotlib.pylab. + fig : plt.Figure + Figure object of matplotlib. """ - profile = profiles.load_plot_profile(mpl_stylesheet) - profile.update((k, kwargs[k]) for k in profile.keys() & kwargs.keys()) - for k in profile.keys() & kwargs.keys(): - kwargs.pop(k) - style = _get_style_path(mpl_stylesheet) - default_styles = _load_default_styles() - default_styles.append(style) + profile, style_list = _prepare_style(mpl_stylesheet, kwargs) kwargs_ = {"cmap": "RdBu", "shading": "auto"} kwargs_.update(kwargs) - with HiddenPrints(): - with plt.style.context(default_styles): + with _suppress_warnings(): + with plt.style.context(style_list): if ax is None: - # [NEW FIGURE] fig, ax = plt.subplots() else: - fig = None + fig = ax.figure # [GRID] X, Y = np.meshgrid(x, y) # [PLOT DATA] - try: - c = ax.pcolormesh(X, Y, Z, **kwargs_) - except TypeError: - c = ax.pcolormesh(X, Y, Z.T, **kwargs_) + Z = _align_z(x, y, Z) + c = ax.pcolormesh(X, Y, Z, **kwargs_) # [PLOT COLORBAR] if profile["colorbar"]: @@ -395,16 +376,49 @@ def heatmap( # [AXES] _set_axis(ax, profile, X, Y) - _set_figure(ax, profile, mplstylesheet=mpl_stylesheet) + _set_figure(ax, profile, mpl_stylesheet=mpl_stylesheet) return fig -def _get_shift_matrix(shape: tuple[int, int]) -> np.array: +def _align_z(x: np.ndarray, y: np.ndarray, Z: np.ndarray) -> np.ndarray: + """Transpose Z if needed so that Z.shape == (len(y), len(x)). + + ``np.meshgrid(x, y)`` returns arrays with shape ``(len(y), len(x))``, + so matplotlib plotting functions expect Z to match this orientation. + If Z has the transposed shape ``(len(x), len(y))`` it is returned + transposed. Any other shape raises ``ValueError``. + + Parameters + ---------- + x : np.ndarray + Array with values for the x axis. + y : np.ndarray + Array with values for the y axis. + Z : np.ndarray + 2D-Array with intensities. + + Returns + ------- + np.ndarray + Z oriented as (len(y), len(x)). + """ - Create a matrix to shift the single lines vertically. + nx, ny = len(x), len(y) + if Z.shape == (ny, nx): + return Z + if Z.shape == (nx, ny): + return Z.T + raise ValueError( + "Z shape {} is incompatible with x (len={}) and y (len={}); " + "expected ({}, {}) or ({}, {})".format(Z.shape, nx, ny, ny, nx, nx, ny) + ) + + +def _get_shift_matrix(shape: tuple[int, int]) -> np.ndarray: + """Create a matrix to shift the single lines vertically. - Needed for plot_shifted_2D(). + Needed for shifted_2D(). Parameters ---------- @@ -414,9 +428,9 @@ def _get_shift_matrix(shape: tuple[int, int]) -> np.array: Returns ------- - shift_matrix : np.array + shift_matrix : np.ndarray Matrix to shift your data vertically by adding the shift matrix. - E. g. : structure of the shift matrix for shape (3, 2): + E.g.: structure of the shift matrix for shape (3, 2): :: [[0, 0], @@ -432,27 +446,25 @@ def _get_shift_matrix(shape: tuple[int, int]) -> np.array: def _set_axis( - ax: object, figure_par: dict, *axes: np.array, y_axis: bool = True -) -> object: - """ - Set up all axis for a plot created by matplotlib. + ax: plt.Axes, figure_par: dict, *axes: np.ndarray, y_axis: bool = True +) -> plt.Axes: + """Set up all axis for a plot created by matplotlib. Parameters ---------- - ax : object + ax : plt.Axes Axis object of matplotlib.pylab. figure_par : dict Dictionary containing all settings for the figure. - *axes : np.array - All axes used for the plot. Used to determine the dimension an set up + *axes : np.ndarray + All axes used for the plot. Used to determine the dimension and set up the z axis if needed. - y_axis : bool, optional - If a y axis is needed. If False, all y ticks will be removed. The - default is True. + y_axis : bool, default is True + If a y axis is needed. If False, all y ticks will be removed. Returns ------- - ax : object + ax : plt.Axes Modified axis object of matplotlib.pylab. """ @@ -477,24 +489,25 @@ def _set_axis( return ax -def _set_figure(ax: object, figure_par: dict, mplstylesheet: str = None) -> object: - """ - Set up the figure of a plot created by matplotlib. +def _set_figure( + ax: plt.Axes, figure_par: dict, mpl_stylesheet: str | None = None +) -> plt.Axes: + """Set up the figure of a plot created by matplotlib. Parameters ---------- - ax : object + ax : plt.Axes Axis object of matplotlib.pylab. figure_par : dict Dictionary containing all settings for the figure. - mplstylesheet : str, optional + mpl_stylesheet : str, default is None Name of the stylesheet which should be used instead of a plotting profile. If no filename is given, the settings from the given plotting - profile will be used. The default is None. + profile will be used. Returns ------- - ax : object + ax : plt.Axes Modified axis object of matplotlib.pylab. """ @@ -506,47 +519,37 @@ def _set_figure(ax: object, figure_par: dict, mplstylesheet: str = None) -> obje return ax -def _get_style_path(mplstylesheet: str = None) -> str: - """ - Get the path to the choosen mpl stylesheet. +def _get_style_path(mpl_stylesheet: str | None = None) -> str: + """Get the path to the chosen mpl stylesheet. Parameters ---------- - mplstylesheet : str, optional - Name of the mpl stylesheet. The default is None. + mpl_stylesheet : str, default is None + Name of the mpl stylesheet. Returns ------- str - Path to the choosen stylesheet. + Path to the chosen stylesheet. """ - if mplstylesheet is None: - style = os.path.join( - sys.prefix, - "easypairspin", - "profiles", - "plot", - "default_stylesheet", - ) - elif mplstylesheet in plt.style.available: - style = mplstylesheet + if mpl_stylesheet is None: + style = str(profiles.PROFILE_ROOT / "plot" / "default_stylesheet") + elif mpl_stylesheet in plt.style.available: + style = mpl_stylesheet else: - style = os.path.join( - sys.prefix, "easypairspin", "profiles", "plot", mplstylesheet - ) + style = str(profiles.PROFILE_ROOT / "plot" / mpl_stylesheet) return style -def _load_default_styles() -> list: - """ - Load the pathes for the default matplotlib styles. +def _load_default_styles() -> list[str]: + """Load the paths for the default matplotlib styles. Returns ------- - default_styles : list - Pathes for the default style sheetss. + default_styles : list[str] + Paths for the default style sheets. """ default_styles = [] @@ -558,8 +561,7 @@ def _load_default_styles() -> list: def _get_label_list(length: int, label: str) -> list[str]: - """ - Get a list with length times the label. + """Get a list with length times the label. Parameters ---------- @@ -571,7 +573,7 @@ def _get_label_list(length: int, label: str) -> list[str]: Returns ------- list[str] - Length times label as a list. E. g.: ['label', 'label', 'label']. + Length times label as a list. E.g.: ['label', 'label', 'label']. """ labels = [label] * length @@ -580,28 +582,28 @@ def _get_label_list(length: int, label: str) -> list[str]: def _get_axis_limit( - data_axis: np.array, ax_lim: list, percentage_mode: bool -) -> [float, float]: - """ - Get the plot limits for a given data axis. + data_axis: np.ndarray, ax_lim: list | str, percentage_mode: bool +) -> tuple[float, float]: + """Get the plot limits for a given data axis. If no limit is specified, the minimal and maximal value of the given dataset will be taken. Parameters ---------- - data_axis : np.array + data_axis : np.ndarray Data vector for an axis. - ax_lim : list - Limits for the data vector taken from a profile. + ax_lim : list or str + Limits for the data vector taken from a profile. Empty string means + no limit is specified. percentage_mode : bool If True, the ax.lim will be handled as a percentage in context to the lowest and highest values with respect to the given axis. - E. g.: x-vector with values [0, 200], ax_lim: [-2., 2.] -> [4, 204]. + E.g.: x-vector with values [0, 200], ax_lim: [-2., 2.] -> [4, 204]. Returns ------- - [float, float] + tuple[float, float] Limits for plotting. """ diff --git a/src/spinanalysis/processing.py b/src/spinanalysis/processing.py index 1a71c74..eb6f5b1 100644 --- a/src/spinanalysis/processing.py +++ b/src/spinanalysis/processing.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Transform EPR spectra through normalization, background correction, and reconstruction. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @@ -9,35 +10,35 @@ """ import numpy as np -from scipy.optimize import curve_fit from scipy import integrate +from scipy.optimize import curve_fit from statsmodels.tsa.ar_model import AutoReg, ar_select_order -def normalization(x: np.array, mode: str = None, dx: float = None) -> np.array: - r""" - Normalize the given data. +def normalization( + x: np.ndarray, mode: str | None = None, dx: float | None = None +) -> np.ndarray: + r"""Normalize the given data. .. math:: - x_{\mathrm{norm}} = \frac{x_i - min(x)}{max(x)-min(x)} + x_{\mathrm{norm}} = \frac{x_i - \min(x)}{\max(x)-\min(x)} Parameters ---------- - x : np.array + x : np.ndarray Unnormalized data. mode : str, optional - If 'area', the total AUC will be 1, if 'value', the maximum value will - be 1 or -1, else, the formula mentioned above will be used. Default is - None. + If ``'area'``, the total AUC will be 1; if ``'value'``, the + maximum absolute value will be 1; otherwise the formula above + is used. The default is ``None``. dx : float, optional - Distance between two points on the x axis. Only used for simpson - integration. Default is None. + Distance between two points on the x axis. Only used for + Simpson integration. The default is ``None``. Returns ------- - x_norm : np.array + np.ndarray Normalized data. - """ if mode is None: if x.dtype == "complex": @@ -70,20 +71,18 @@ def normalization(x: np.array, mode: str = None, dx: float = None) -> np.array: return x_norm -def reduce_offset(x: np.array) -> np.array: - """ - Eliminate the offset of the data by using the mean of the last quarter. +def reduce_offset(x: np.ndarray) -> np.ndarray: + """Eliminate the offset of the data by subtracting the mean of the last quarter. Parameters ---------- - x : np.array - Given data, e. g. measured intensities. + x : np.ndarray + Given data, e.g. measured intensities. Returns ------- - x_shifted : np.array + np.ndarray Shifted data without offset. - """ start = 3 * x.shape[0] // 4 if x.dtype == "complex": @@ -96,31 +95,30 @@ def reduce_offset(x: np.array) -> np.array: return x_shifted -def background_corr(x: np.array, y: np.array, mode: str = "biexp") -> np.array: - """ - Perform a background correction of measured data. +def background_corr(x: np.ndarray, y: np.ndarray, mode: str = "biexp") -> np.ndarray: + """Perform a background correction of measured data. - Available correction modes are: biexp, exp, lin, poly2, poly3 and poly4. - biexp and exp are using exponential models for the background. lin, poly2-4 - are polynominal models of first to fourth order. Default is 'biexp'. + Available correction modes are: ``biexp``, ``exp``, ``lin``, + ``poly2``, ``poly3`` and ``poly4``. ``biexp`` and ``exp`` use + exponential models for the background; ``lin`` and ``poly2-4`` are + polynomial models of first to fourth order. .. warning:: - poly3 and poly4 can lead to overfitting! + ``poly3`` and ``poly4`` can lead to overfitting! Parameters ---------- - x : np.array + x : np.ndarray x axis of the dataset. - y : np.array + y : np.ndarray y data which will be background corrected. mode : str, optional - Select the type of the background. The default is 'biexp'. + Select the type of the background. The default is ``'biexp'``. Returns ------- - y_corr : np.array + np.ndarray Background corrected y data. - """ if mode == "biexp": p0 = [0.9, -0.002, 0.05, -0.0009, 0.0] @@ -150,161 +148,146 @@ def background_corr(x: np.array, y: np.array, mode: str = "biexp") -> np.array: return y_corr -def exp_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized monoexponential function for background correction. +def exp_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized monoexponential function for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float Variables for the monoexponential function which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b, c = coeff y = a * np.exp(b * x) + c return y -def biexp_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized biexponential function for background correction. +def biexp_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized biexponential function for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float Variables for the biexponential function which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b, c, d, e = coeff y = a * np.exp(b * x) + c * np.exp(d * x) + e return y -def lin_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized linear function for background correction. +def lin_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized linear function for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float Variables for the linear function which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b = coeff y = a * x + b return y -def poly2_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 2 for background correction. +def poly2_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized polynomial function of degree 2 for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float - Variables for the polynominal function of degree 2 which will be + Variables for the polynomial function of degree 2 which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b, c = coeff y = a * x**2 + b * x + c return y -def poly3_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 3 for background correction. +def poly3_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized polynomial function of degree 3 for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float - Variables for the polynominal function of degree 3 which will be + Variables for the polynomial function of degree 3 which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b, c, d = coeff y = a * x**3 + b * x**2 + c * x + d return y -def poly4_fun(x: np.array, *coeff: float) -> np.array: - """ - Generalized polynominal function of degree 4 for background correction. +def poly4_fun(x: np.ndarray, *coeff: float) -> np.ndarray: + """Generalized polynomial function of degree 4 for background correction. Parameters ---------- - x : np.array + x : np.ndarray x values used to calculate corresponding y values. *coeff : float - Variables for the polynominal function of degree 4 which will be + Variables for the polynomial function of degree 4 which will be fitted. Returns ------- - y : np.array + np.ndarray Calculated y values. - """ a, b, c, d, e = coeff y = a * x**4 + b * x**3 + c * x**2 + d * x + e return y -def reconstruct(x: np.array, y: np.array) -> np.array: - """ - Reconstruction of a time signal use the Yule-Walker algorithm. +def reconstruct(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Reconstruct a time signal using the Yule-Walker algorithm. Parameters ---------- - x : np.array + x : np.ndarray x axis. - y : np.array + y : np.ndarray Intensities. Returns ------- - x_new : np.array + x_new : np.ndarray Reconstructed x axis. - y : np.array + y_new : np.ndarray Reconstructed intensities. - """ - # prepare the new array x_step = x[1] - x[0] if x[0] % x_step != 0: x_fill_points = int(x[0] / x_step) + 1 @@ -314,11 +297,9 @@ def reconstruct(x: np.array, y: np.array) -> np.array: (np.linspace(x[0] - x_step * x_fill_points, x[0] - x_step, x_fill_points), x) ) - # determine the order of the p value for the reconstruction order = ar_select_order(y[::-1], maxlag=40) nlag = len(order.ar_lags) - # Fit the model to the data and make a predicition AutoRegFit = AutoReg(y[::-1], lags=order.ar_lags).fit() y_pred = AutoRegFit.predict(start=0, end=x_new.shape[0] + nlag - 1) diff --git a/src/spinanalysis/profiles.py b/src/spinanalysis/profiles.py index d375795..1caa3b3 100644 --- a/src/spinanalysis/profiles.py +++ b/src/spinanalysis/profiles.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Manage configuration profiles for EPR simulations, plots, and optimization. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @@ -11,481 +12,423 @@ from configobj import ConfigObj from matplotlib.pyplot import style from spinanalysis._utils import strtobool -import validate import os -import sys +import validate +from pathlib import Path from zipfile import ZipFile +PROFILE_ROOT = Path( + os.environ.get( + "SPINANALYSIS_PROFILE_DIR", + Path.home() / ".config" / "spinanalysis" / "profiles", + ) +) + +_VALID_PROFILE_KINDS = ( + "plot", + "simulation", + "optimization", + "spinsystem", + "variation", +) + +_VALID_OPTIMIZATION_ROUTINES = ( + "genetic", + "minimize", + "dual_annealing", + "shgo", + "differential_evolution", + "basinhopping", + "least_squares", +) + +_VALID_SIMULATION_ROUTINES = ( + "static_radpair", + "teacups", + "opossum", + "didelphis", + "didelphis_tikhonov", +) + + +def _normalize_pkind(pkind: str) -> str: + """Normalise a profile kind string to lower-case and map British spelling.""" + pkind = pkind.lower() + if pkind == "optimisation": + pkind = "optimization" + if pkind not in _VALID_PROFILE_KINDS: + raise ValueError( + "pkind must be one of {}!".format( + ", ".join(repr(k) for k in _VALID_PROFILE_KINDS) + ) + ) + return pkind + + +def _normalize_kinds(pkind: str | list[str]) -> list[str]: + """Normalise *pkind* (str or list) to a list of valid profile kinds.""" + if pkind == "all": + return list(_VALID_PROFILE_KINDS) + + if isinstance(pkind, str): + pkind = pkind.split() + + stack: list[str] = [] + for el in pkind: + el = el.lower() + if el == "optimisation": + el = "optimization" + if el in _VALID_PROFILE_KINDS: + stack.append(el) + return stack + + def import_profiles(zipfile: str, override: bool = False) -> None: - """ - Import profiles from a zip archive. + """Import profiles from a zip archive. + + The archive must contain subdirectories matching valid profile kinds + (e.g. ``spinsystem/``, ``simulation/``). Files are extracted directly + into :data:`PROFILE_ROOT`. Parameters ---------- zipfile : str Path to the zip archive. override : bool, optional - If True, existing profile with the same name will be overriden. The - default is False. + If True, existing profiles with the same name will be overwritten, + default is ``False``. - Returns - ------- - None - Nothing will be returned. - - """ - zipfile = os.path.abspath(zipfile) - profiles_folder = os.path.join(sys.prefix, "easypairspin", "profiles") - - with ZipFile(zipfile, "r") as zipfile: - for file in zipfile.namelist(): - if os.path.isfile(os.path.join(profiles_folder, file)) and not override: + Raises + ------ + ValueError + If the archive contains a top-level entry that does not correspond + to a valid profile kind. + """ + archive = Path(zipfile).resolve() + root = PROFILE_ROOT + + with ZipFile(str(archive), "r") as zf: + for entry in zf.namelist(): + top = entry.split("/")[0] + if top and top not in _VALID_PROFILE_KINDS: + raise ValueError( + "Archive entry '{}' does not match any valid profile kind.".format( + top + ) + ) + target = root / entry + if target.is_file() and not override: continue + zf.extract(entry, str(root)) - zipfile.extract(file, profiles_folder) - - zipfile.close() - -def export(path: str = None, pkind: [str, list] = "all", pname: str = "all") -> None: - """ - Export the choosen profile(s) as a zip archive. +def export( + path: str | None = None, + pkind: str | list[str] = "all", + pname: str = "all", +) -> None: + """Export the chosen profile(s) as a zip archive. Parameters ---------- path : str, optional - Path where the zip file will be stored. If no path given, the zip file - will be stored in the current working directory. The default is None. - pkind : str, optional - Define which kind(s) of profiles should be exported. Multiple kinds of - profiles possible. Options are 'plot', 'spinsystem', 'optimization', - 'save', 'variation', 'simulation' and 'all'. The default is 'all'. + Directory where the zip file will be stored. If None, the zip file + will be stored in the current working directory, default is ``None``. + pkind : str or list of str, optional + Define which kind(s) of profiles should be exported. Options are + 'plot', 'spinsystem', 'optimization', 'simulation', 'variation' and + 'all', default is ``'all'``. pname : str, optional - Give the basename of the profile. The default is 'all'. - - Returns - ------- - None - Nothing will be returned. - + Give the basename of the profile, default is ``'all'``. """ - profiles_folder = os.path.join(sys.prefix, "easypairspin", "profiles") + root = PROFILE_ROOT + kinds = _normalize_kinds(pkind) - if pkind == "all": - pkind = [ - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ] - else: - if not isinstance(pkind, list): - stack = [] - for el in pkind.lower().strip().split(): - if el in ( - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ): - stack.append(el) - elif el == "optimisation": - stack.append("optimization") - else: - continue - pkind = stack - - else: - stack = [] - for el in pkind: - if el.lower() in ( - "plot", - "spinsystem", - "optimization", - "simulation", - "variation", - "save", - ): - stack.append(el.lower()) - elif el.lower() == "optimisation": - stack.append("optimization") - else: - continue - pkind = stack - - file_paths = [] - for kind in pkind: - file_paths.extend(_get_profile_paths(os.path.join(profiles_folder, kind))) + file_paths: list[Path] = [] + for kind in kinds: + file_paths.extend(_get_profile_paths(root / kind, pname)) if path is None: - path = os.path.join(os.getcwd(), "profiles_easypairspin.zip") + archive = Path.cwd() / "profiles_spinanalysis.zip" else: - path = os.path.join(path, "profiles_easypairspin.zip") - - with ZipFile(path, "w") as zipfile: - for file in file_paths: - arcname = file[len(profiles_folder) + 1 :] - zipfile.write(file, arcname) + archive = Path(path) / "profiles_spinanalysis.zip" - zipfile.close() + root_str = str(root) + with ZipFile(str(archive), "w") as zf: + for fp in file_paths: + arcname = str(fp)[len(root_str) + 1 :] + zf.write(str(fp), arcname) -def _get_profile_paths(pfolder: str, pname: str = "all") -> None: - """ - Get the pathes of all profile. +def _get_profile_paths(pfolder: Path, pname: str = "all") -> list[Path]: + """Get the paths of all profiles in *pfolder*. Parameters ---------- - pfolder: : str + pfolder : Path Folder with profiles. pname : str, optional - Basename of the profile. The default is "all". + Basename of the profile, default is ``'all'``. Returns ------- - None - Nothing will be returned. - + list[Path] + Paths to matching profile files. """ - paths = [] + paths: list[Path] = [] - for root, directories, files in os.walk(pfolder): - for file in files: - if file.endswith("configspec.ini"): - continue + if not pfolder.is_dir(): + return paths - if pname.lower() == "all": - paths.append(os.path.join(pfolder, file)) - elif pname.lower() == os.path.basename(file): - paths.append(os.path.join(pfolder, file)) - else: - continue + for fp in pfolder.rglob("*"): + if not fp.is_file(): + continue + if fp.name == "configspec.ini": + continue + if pname.lower() == "all": + paths.append(fp) + elif pname.lower() == fp.name: + paths.append(fp) return paths -def add_profile(profile: dict, pkind: str, pname: str = "") -> None: - """ - Add a new profile for EasyPairSpin. +def _validate_profile_sections(profile: dict, pkind: str) -> None: + """Check that all required sections and keys are present in the profile. Parameters ---------- profile : dict Dictionary with all profile settings. pkind : str - Give the kind of the kind of the profile. Not case sensitive. pkind can - be 'plot', 'save', 'simulation', 'optimization', 'spinsystem' or - 'variation'. - pname : str, optional - Name of the profile. If no profile name is given, a default one will - be generated by _get_profile_name(). The default is ''. - + Kind of the profile (already normalised to lower-case). Raises ------ ValueError - Raised if pkind isn't 'plot', 'save', 'simulation', 'optimization', - 'spinsystem' or 'variation'. - - Returns - ------- - None - Nothing will be returned. - - Examples - -------- - Creating and adding a new profile: - - >>> Sys = epr_setup.Spinsystem() - >>> Sys_profile = new_spinsystem_profile() - >>> Sys_profile['g_1'] = [2.0034, 2.00156, 2.00228] - >>> prom.add_profile(Sys.profile, 'spinsystem', 'Sys_prof_1') - + If a required section or key is missing. """ - pkind = pkind.lower() - if pkind == "optimisation": - pkind = "optimization" - - if pkind not in ( - "plot", - "save", - "simulation", - "optimization", - "spinsystem", - "variation", - ): + if "main" not in profile: raise ValueError( - "pkind must be 'plot', 'save', 'simulation'," - " 'optimization', 'spinsystem' or 'variation'! " + "Profile for '{}' is missing the 'main' section.".format(pkind) ) - if pname == "": - pname = _get_profile_name(pkind) - - config_path = os.path.join(sys.prefix, "easypairspin", "profiles", pkind) - if pkind == "plot": - config_name = os.path.join(config_path, pname) - else: - config_name = os.path.join(config_path, pname + ".ini") - path_to_configspec = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, "configspec.ini" - ) - - if not os.path.exists(config_path): - os.makedirs(config_path) - - config = ConfigObj(config_name, configspec=path_to_configspec) - - # TODO Exception Handling, ob alle relevanten Werte (korrekt) gegeben sind - # TODO match case Struktur einführen (wenn Python 3.10 möglich) - if pkind == "plot": - _save_config_plot(profile, config_name) - elif pkind == "spinsystem": - config = _set_configobj_spinsystem(profile, config) - elif pkind == "variation": - config = _set_configobj_variation(profile, config) - elif pkind == "optimization": - config = _set_configobj_optimization(profile, config) - elif pkind == "save": - config = _set_configobj_save(profile, config) - elif pkind == "simulation": - config = _set_configobj_simulation(profile, config) - - if not (pkind == "plot"): - validator = validate.Validator() - config.validate(validator) - config.write() + match pkind: + case "optimization" | "simulation": + routine = profile["main"].get("routine", "") + if not routine: + raise ValueError( + "Profile for '{}' is missing 'routine' in the 'main' " + "section.".format(pkind) + ) + if routine not in profile: + raise ValueError( + "Profile for '{}' is missing the '{}' section.".format( + pkind, routine + ) + ) - return None +def _set_configobj(profile: dict, config: ConfigObj, pkind: str) -> ConfigObj: + """Write profile parameters into the ConfigObj. -def _save_config_plot(profile: dict, path: str) -> object: - """ - Write profile parameters into the ConfigObject for a plot profile. + For profiles that carry a routine (optimization, simulation), the + routine-specific section is validated and copied in addition to + ``main``. Parameters ---------- profile : dict - Plot profile. - path : str - Path, where the profile will be stored. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - preamble = "## {:*^76s}\n## {:*^76s}\n## {:*^76s}\n".format( - "", " EASYPAIRSPIN SPECIAL SETTINGS ", "" - ) - with open(path, "w") as f: - f.write(preamble) - f.write("\n") - for key in profile: - if key.endswith("lim"): - line = "{}: {}, {}".format( - str(key), str(profile[key][0]), str(profile[key][1]) - ) - else: - if isinstance(profile[key], (int, float, bool)): - line = "{}: {}".format(str(key), str(profile[key])) - else: - line = '{}: "{}"'.format(str(key), str(profile[key])) - f.write(line) - f.write("\n") - f.close() - - -def _set_configobj_spinsystem(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a spinsystem profile. + Dictionary with all profile settings. + config : ConfigObj + ConfigObj to populate. + pkind : str + Normalised profile kind. - Parameters - ---------- - profile : dict - Spinsystem profile. - ConObj : object - ConfigObj from module configobj. + Raises + ------ + ValueError + If the routine is not valid for the given profile kind. Returns ------- - object - ConfigObj from module configobj. - + ConfigObj + The populated ConfigObj. """ - ConObj["main"] = profile["main"] - - return ConObj + main = {k: v for k, v in profile["main"].items() if v is not None} + config["main"] = main + match pkind: + case "optimization": + routine = profile["main"]["routine"] + if routine not in _VALID_OPTIMIZATION_ROUTINES: + raise ValueError("{} is no valid optimization routine!".format(routine)) + config[routine] = profile[routine] + case "simulation": + routine = profile["main"]["routine"] + if routine not in _VALID_SIMULATION_ROUTINES: + raise ValueError("{} is no valid simulation routine!".format(routine)) + config[routine] = profile[routine] -def _set_configobj_variation(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a variation profile. + return config - Parameters - ---------- - profile : dict - Variation profile. - ConObj : object - ConfigObj from module configobj. - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - return ConObj - - -def _set_configobj_optimization(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for an optimization profile. +def add_profile(profile: dict, pkind: str, pname: str = "") -> None: + """Add a new profile for spinanalysis. Parameters ---------- profile : dict - Optimization profile. - ConObj : object - ConfigObj from module configobj. + Dictionary with all profile settings. + pkind : str + Kind of the profile. Not case sensitive. Can be 'plot', + 'simulation', 'optimization', 'spinsystem' or 'variation'. + pname : str, optional + Name of the profile. If no profile name is given, a default one + will be generated by :func:`_get_profile_name`, default is ``''``. Raises ------ ValueError - If no valid optimization routine is given. - - Returns - ------- - object - ConfigObj from module configobj. - - """ - ConObj["main"] = profile["main"] - - routine = profile["main"]["routine"] - if routine in [ - "genetic", - "minimize", - "dual_annealing", - "shgo", - "differential_evolution", - "basinhopping", - "least_squares", - ]: - ConObj[routine] = profile[routine] - else: - raise ValueError("{} is no valid optimization routine!".format(routine)) - - return ConObj + If *pkind* is not a valid profile kind, or if the profile fails + configspec validation. + Examples + -------- + Creating and adding a new profile: -def _set_configobj_simulation(profile: dict, ConObj: object) -> object: + >>> from spinanalysis.epr import Spinsystem + >>> from spinanalysis import profiles + >>> Sys = Spinsystem() + >>> Sys_profile = profiles.new_spinsystem_profile() + >>> Sys_profile['main']['g1'] = [2.0034, 2.00156, 2.00228] + >>> profiles.add_profile(Sys_profile, 'spinsystem', 'Sys_prof_1') """ - Write profile parameters into the ConfigObject for a simulation profile. + pkind = _normalize_pkind(pkind) - Parameters - ---------- - profile : dict - Simulation profile. - ConObj : object - ConfigObj from module configobj. + if pname == "": + pname = _get_profile_name(pkind) - Raises - ------ - ValueError - If no valid simulation routine is given. + root = PROFILE_ROOT + config_path = root / pkind - Returns - ------- - object - ConfigObj from module configobj. + match pkind: + case "plot": + _save_config_plot(profile, config_path / pname) + case "spinsystem" | "variation" | "optimization" | "simulation": + _validate_profile_sections(profile, pkind) + config_name = config_path / (pname + ".ini") + path_to_configspec = config_path / "configspec.ini" + config_path.mkdir(parents=True, exist_ok=True) + config = ConfigObj(str(config_name), configspec=str(path_to_configspec)) - """ - ConObj["main"] = profile["main"] + config = _set_configobj(profile, config, pkind) - routine = profile["main"]["routine"] - if routine in ["static_radpair", "teacups", "opossum", "didelphis"]: - ConObj[routine] = profile[routine] - else: - raise ValueError("{} is no valid simulation routine!".format(routine)) + _validate_config(config, pname) + config.write() - return ConObj + return None -def _set_configobj_save(profile: dict, ConObj: object) -> object: - """ - Write profile parameters into the ConfigObject for a save profile. +def _save_config_plot(profile: dict, path: Path) -> None: + """Write plot profile parameters to a stylesheet file. Parameters ---------- profile : dict - Save profile. - ConObj : object - ConfigObj from module configobj. - - Returns - ------- - object - ConfigObj from module configobj. - + Plot profile. + path : Path + Path where the stylesheet will be stored. """ - ConObj["main"] = profile["main"] - - return ConObj + preamble = "## {:*^76s}\n## {:*^76s}\n## {:*^76s}\n".format( + "", " SPINANALYSIS SPECIAL SETTINGS ", "" + ) + lines = [preamble, "\n"] + for key in profile: + if key.endswith("lim"): + line = "{}: {}, {}".format( + str(key), str(profile[key][0]), str(profile[key][1]) + ) + elif isinstance(profile[key], (int, float, bool)): + line = "{}: {}".format(str(key), str(profile[key])) + else: + line = '{}: "{}"'.format(str(key), str(profile[key])) + lines.append(line + "\n") + Path(path).write_text("".join(lines)) def _get_profile_name(pkind: str) -> str: - """ - Search for the smallest number available for the default profile name. + """Search for the smallest number available for the default profile name. - Scheme for default profile name is 'profile_[number]' + Scheme for default profile name is 'profile_[number]'. Parameters ---------- pkind : str - Give the kind of the kind of the profile. Not case sensitive. pkind can - be 'plot', 'save', 'simulation', 'optimization', 'spinsystem' or - 'variation'. + Kind of the profile. Not case sensitive. Returns ------- - name : str - String with the available profile name e.g. 'profile_12'. - + str + Available profile name, e.g. 'profile_12'. """ - name = "profile_" suffix = "" if pkind == "plot" else ".ini" - profile_number = 0 - profile_number_used = True + folder = PROFILE_ROOT / pkind + + existing: set[int] = set() + if folder.is_dir(): + for fp in folder.glob("profile_*" + suffix): + stem = fp.stem if suffix else fp.name + num_part = stem.replace("profile_", "") + try: + existing.add(int(num_part)) + except ValueError: + continue - while profile_number_used: - profile_number += 1 - for root, dirs, files in os.walk( - os.path.join(sys.prefix, "easypairspin", "profiles", pkind) - ): - counter = 0 - for file in files: - if file.endswith("profile_" + str(profile_number) + suffix): - counter += 1 + number = 1 + while number in existing: + number += 1 - profile_number_used = counter + return "profile_{}".format(number) - name += str(profile_number) - return name +def _validate_config(config: ConfigObj, pname: str) -> None: + """Validate *config* against its configspec and raise on failure. + + Only sections and keys present before validation are checked. + Keys with the value ``"None"`` (serialised Python ``None``) are + skipped, as they represent optional fields that were not set. + """ + validator = validate.Validator() + present = {s: set(config[s].keys()) for s in config.sections} + result = config.validate(validator, preserve_errors=True) + if result is not True: + for section, keys in present.items(): + section_result = result.get(section, True) + if section_result is True: + continue + if isinstance(section_result, dict): + failed = { + k: v + for k, v in section_result.items() + if k in keys and v is not True and config[section].get(k) != "None" + } + if failed: + raise ValueError( + "Profile '{}' failed configspec validation " + "in section '{}': {}".format(pname, section, failed) + ) + elif section_result is not True: + raise ValueError( + "Profile '{}' failed configspec validation in section '{}'.".format( + pname, section + ) + ) def load_profile(pname: str, pkind: str) -> dict: - """ - Load a given plotting profile. + """Load a profile from a config file. Parameters ---------- @@ -493,108 +436,104 @@ def load_profile(pname: str, pkind: str) -> dict: Name of the profile. Case sensitive. Either with .ini or not. E. g.: load_profile('test') or load_profile('test.ini'). pkind : str - Give the kind of the profile. Not case sensitive. pkind can be 'save', - 'simulation', 'optimization', 'spinsystem' or 'variation'. + Kind of the profile. Not case sensitive. Can be 'simulation', + 'optimization', 'spinsystem' or 'variation'. + + Raises + ------ + ValueError + If *pkind* is not valid or the profile fails configspec validation. Returns ------- - profile: dict + dict Loaded profile as a dictionary. - """ + pkind = _normalize_pkind(pkind) + if pname.endswith(".ini"): pname = pname[:-4] - path_to_profile = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, pname + ".ini" - ) - path_to_configspec = os.path.join( - sys.prefix, "easypairspin", "profiles", pkind, "configspec.ini" - ) - - config = ConfigObj(path_to_profile, configspec=path_to_configspec, file_error=True) - validator = validate.Validator() - config.validate(validator) + root = PROFILE_ROOT + path_to_profile = root / pkind / (pname + ".ini") + path_to_configspec = root / pkind / "configspec.ini" - profile = {} + config = ConfigObj( + str(path_to_profile), configspec=str(path_to_configspec), file_error=True + ) + _validate_config(config, pname) + profile: dict[str, dict] = {} for section in config.sections: profile[section] = config[section] return profile -def load_plot_profile(pname: str) -> dict: - """ - Load a plotting profile from a mplstylesheet. +def load_plot_profile(pname: str | None) -> dict: + """Load a plotting profile from an mplstylesheet. Parameters ---------- - pname : str - Name of the profile. Case sensitive. + pname : str | None + Name of the profile. Case sensitive. If None or a built-in + matplotlib style, the default stylesheet is loaded. Returns ------- dict - Contains the settings for the plotting functions. - + Settings for the plotting functions. """ if pname is None or pname in style.available: pname = "default_stylesheet" - path_to_profile = os.path.join( - sys.prefix, "easypairspin", "profiles", "plot", pname + path_to_profile = PROFILE_ROOT / "plot" / pname + + profile: dict[str, object] = {} + valid_keys = ( + "percentage_mode", + "xlim", + "ylim", + "zlim", + "xlabel", + "ylabel", + "zlabel", + "show_title", + "title", + "legend", + "colorbar", ) - profile = {} - with open(path_to_profile, "r") as file: - for line in file.readlines(): - if line.startswith("#"): + with open(str(path_to_profile), "r") as file: + for raw_line in file: + if raw_line.startswith("#") or not raw_line.strip(): continue + parts = raw_line.strip().split(":", 1) + if len(parts) != 2: + continue + key, value = parts[0].strip(), parts[1].split("#")[0].strip() + if key not in valid_keys: + continue + if "lim" in key and len(key) == 4: + bounds = value.split(",") + if len(bounds) != 2: + raise ValueError("Malformed bounds for '{}': {}".format(key, value)) + profile[key] = [float(bounds[0]), float(bounds[1])] + elif ("label" in key and len(key) == 6) or key == "title": + profile[key] = value.strip('"').strip("'") else: - line = line.strip().split(":") - if line[0] in ( - "percentage_mode", - "xlim", - "ylim", - "zlim", - "xlabel", - "ylabel", - "zlabel", - "show_title", - "title", - "legend", - "colorbar", - ): - if "lim" in line[0] and len(line[0]) == 4: - bounds = line[1].split("#")[0].strip() - bounds = bounds.split(",") - lb = float(bounds[0]) - ub = float(bounds[1]) - profile[line[0]] = [lb, ub] - elif ("label" in line[0] and len(line[0]) == 6) or ( - line[0] == "title" - ): - label = line[1].split("#")[0].strip() - label = label.strip('"').strip("'") - profile[line[0]] = label - else: - boolean = line[1].split("#")[0].strip() - boolean = boolean.strip('"').strip("'") - profile[line[0]] = bool(strtobool(boolean)) + profile[key] = bool(strtobool(value.strip('"').strip("'"))) return profile def new_plot_profile() -> dict: - """ - Get an empty plotting profile. + """Get an empty plotting profile. Returns ------- - default_profile: dict - Dictionary with default settings for plottings. - + dict + Dictionary with default settings for plotting. """ default_profile = { "percentage_mode": False, @@ -614,14 +553,12 @@ def new_plot_profile() -> dict: def new_spinsystem_profile() -> dict: - """ - Get a default spinsystem profile. + """Get a default spinsystem profile. Returns ------- - default_profile: dict + dict Dictionary with default settings for a spinsystem. - """ default_profile = { "main": { @@ -685,14 +622,12 @@ def new_spinsystem_profile() -> dict: def new_variation_profile() -> dict: - """ - Get a default variation profile. + """Get a default variation profile. Returns ------- - default_profile: dict + dict Dictionary with default settings for variation. - """ default_profile = { "main": { @@ -732,38 +667,20 @@ def new_variation_profile() -> dict: "population": [0.0, 0.0, 0.0], "freq_mw": 0.0, "amplitude": 0, + "fit_distribution": False, } } return default_profile -def new_save_profile() -> dict: - """ - Get a default save profile. - - Returns - ------- - default_profile: dict - Dictionary with default settings for saving. - - """ - default_profile = {"main": {}} - # TODO Profil verwenden um Projektordner festzulegen -> Speicherort für - # Bilder, Simulations/Optimierungsergebnisse, out-Files etc. - - return default_profile - - def new_optimization_profile() -> dict: - """ - Get a default optimization profile. + """Get a default optimization profile. Returns ------- - default_profile: dict + dict Dictionary with default settings for optimization routines. - """ default_profile = { "main": {"routine": "", "method": None, "cpu_cores": 0}, @@ -853,20 +770,18 @@ def new_optimization_profile() -> dict: def new_simulation_profile() -> dict: - """ - Get an empty simulation profile. + """Get an empty simulation profile. Returns ------- - default_profile: dict + dict Dictionary with default settings for simulation profiles. - """ default_profile = { "main": {"routine": "", "cpu_cores": 0}, - "static_radpair": {"grid_points": 500}, + "static_radpair": {"knots": 20}, "teacups": { - "grid_points": 500, + "knots": 20, "space": "hilbert", }, "opossum": {}, diff --git a/src/spinanalysis/saving.py b/src/spinanalysis/saving.py index 931d2f0..0e0121c 100644 --- a/src/spinanalysis/saving.py +++ b/src/spinanalysis/saving.py @@ -1,302 +1,372 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -© M. Sc. Florian Quintes, 2021-2022 +"""Write simulation results, figures, and output files to disk. + +© M. Sc. Florian Quintes, 2026 @contact: florian.quintes@pc.uni.freiburg.de @author: Florian Quintes """ +from __future__ import annotations + +import datetime +from importlib.metadata import metadata as _pkg_metadata +from pathlib import Path +from typing import Any + import numpy as np -import os -def save_plot(fname: str, *figures: object, path: str = None, **kwargs: dict) -> None: +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +_G_FIELDS: tuple[str, ...] = ("g1", "g2", "g_tri", "g") +_A_FIELDS: tuple[str, ...] = ("A1", "A2", "A3", "A4", "A5", "A_eseem", "omega_I") +_ZFS_FIELDS: tuple[str, ...] = ("D", "D_tri", "E", "E_tri", "J_ex", "J_0", "beta") +_NUCLEUS_PAIRS: tuple[tuple[str, str], ...] = ( + ("n1", "I1"), + ("n2", "I2"), + ("n3", "I3"), + ("n4", "I4"), + ("n5", "I5"), +) +_FRAME_FIELDS: tuple[str, ...] = ( + "g1_frame", + "g2_frame", + "g_tri_frame", + "g_frame", + "A1_frame", + "A2_frame", + "A3_frame", + "A4_frame", + "A5_frame", + "D_frame", + "D_tri_frame", +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _get_authors() -> str: + """Return the author name(s) from package metadata.""" + try: + meta = _pkg_metadata("spinanalysis") + except Exception: + return "spinanalysis" + author_email = meta.get("Author-email", "") + if author_email: + names: list[str] = [] + for part in author_email.split(","): + part = part.strip() + if "<" in part: + names.append(part.split("<")[0].strip()) + else: + names.append(part) + return ", ".join(names) + return meta.get("Author", "spinanalysis") + + +def _resolve_output_dir(path: str | Path | None) -> Path: + """Resolve the output directory for :func:`write_out_file`. + + If *path* is given, use it (creating it if needed). Otherwise + create ``spinanalysis_YYYY-MM-DD_N`` in the current working + directory, where *N* is the lowest available positive integer. + + Parameters + ---------- + path : str or pathlib.Path, optional + Directory path to use. If ``None``, a new directory is created + in the current working directory. + + Returns + ------- + pathlib.Path + The resolved output directory. + """ - Save the figures plotted with matplotlib. + if path is not None: + out_dir = Path(path) + out_dir.mkdir(parents=True, exist_ok=True) + return out_dir + + date_str = datetime.datetime.now().strftime("%Y-%m-%d") + base_name = f"spinanalysis_{date_str}" + cwd = Path.cwd() + + n = 1 + while True: + candidate = cwd / f"{base_name}_{n}" + if not candidate.exists(): + break + n += 1 + + candidate.mkdir(parents=True) + return candidate + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def save_plot( + fname: str, + *figures: object, + path: str | Path | None = None, + **kwargs: Any, +) -> None: + """Save the figures plotted with matplotlib. Parameters ---------- fname : str - Filename for the figure(s). If multiple figures are given, '_[number]' - will be append to the filename. + Filename for the figure(s). If multiple figures are given, + '_[number]' will be appended to the filename. *figures : object Matplotlib figure object(s). - path : str, optional - Absolute path where the figures will be stored. The default is - '~/Results/Plots/'. - **kwargs : dict - Other keyword arguments. Will be passed to plt.savefig(). See - matplotlib documentation for further informations. - - Returns - ------- - None - Nothing will be returned. + path : str or pathlib.Path, optional + Directory where the figures will be stored, default is + ``~/spinanalysis/plots/``. + **kwargs + Other keyword arguments. Will be passed to ``plt.savefig()``. + See matplotlib documentation for further information. """ - # [SETUP] - if fname[-4] == ".": - fmt = fname[-4:] - fname = fname[:-4] - elif fname[-5] == ".": - fmt = fname[-5:] - fname = fname[:-5] - else: - fmt = "" - if type(figures[0]) == list: + if not figures: + raise ValueError("At least one figure must be provided.") + + p = Path(fname) + fmt = p.suffix + base_name = p.stem + + if isinstance(figures[0], list): figures = tuple(*figures) - if len(figures) == 1: - multi_plot = False - else: - multi_plot = True + multi_plot = len(figures) > 1 - if path is None: - save_path = os.path.join(os.path.expanduser("~"), "Results", "Plots") - else: - save_path = path + if "format" in kwargs: + fmt = "." + kwargs["format"] - if not os.path.exists(save_path): - os.makedirs(save_path) + save_path = ( + Path(path) if path is not None else Path.home() / "spinanalysis" / "plots" + ) + save_path.mkdir(parents=True, exist_ok=True) - # [SAVE] for n, fig in enumerate(figures): if multi_plot: - save_name = os.path.join(save_path, fname + "_" + str(n + 1) + fmt) + save_name = save_path / f"{base_name}_{n + 1}{fmt}" else: - save_name = os.path.join(save_path, fname + fmt) - - if "format" in kwargs: - save_name += "." + kwargs["format"] + save_name = save_path / f"{base_name}{fmt}" + fig.savefig(str(save_name), **kwargs) - fig.savefig(save_name, **kwargs) - - return None +def save_simulation( + name: str, + *data: np.ndarray, + path: str | Path | None = None, +) -> None: + """Save the simulated data at ``[path]/[name]/[files]``. -def save_simulation(name: str, *data: "np.array", path: str = None) -> None: - """ - Save the simulated data at '[path]/[name]/[files]' using np.savetxt. + Uses :func:`numpy.savetxt`. Parameters ---------- name : str - Foldername for the dataset. - *data : np.array - Arrays with the simulated data. Must be 2d or 3d. If 2d: x_axis, int; - if 3d: x_axis, y_axis, int. - path : str, optional - Full path where the data will be stored. The default is - '~/Results/Simulated Data/[name]'. + Folder name for the dataset. + *data : np.ndarray + Arrays with the simulated data. If 2 arrays: ``x_axis``, ``int``; + if 3 arrays: ``x_axis``, ``y_axis``, ``int``. + path : str or pathlib.Path, optional + Directory where the data will be stored, default is + ``~/spinanalysis/simulations/``. Raises ------ ValueError - Will be raised, if dimension of the data isn't 2d or 3d. - - Returns - ------- - None - Nothing will be returned. + If the number of data arrays is not 2 or 3. """ - # [SETUP] + if name.lower().endswith(".txt"): name = name[:-4] - if path is None: - save_path = os.path.join(os.path.expanduser("~"), "Results", "Simulated Data") - else: - save_path = path - if not os.path.exists(save_path): - os.makedirs(save_path) + save_path = ( + Path(path) if path is not None else Path.home() / "spinanalysis" / "simulations" + ) + save_path.mkdir(parents=True, exist_ok=True) - save_folder = os.path.join(save_path, name) - if not os.path.exists(save_folder): - os.makedirs(save_folder) + save_folder = save_path / name + save_folder.mkdir(parents=True, exist_ok=True) if len(data) == 2: x, intensity = data elif len(data) == 3: x, y, intensity = data else: - raise ValueError("Can't handle {0}D Data. Need 2D or 3D.".format(len(data))) + raise ValueError("Can't handle {0}D data. Need 2D or 3D.".format(len(data))) - # [SAVE] - - np.savetxt(os.path.join(save_folder, "x_axis.txt"), x) - np.savetxt(os.path.join(save_folder, "intensity.txt"), intensity) + np.savetxt(str(save_folder / "x_axis.txt"), x) + np.savetxt(str(save_folder / "intensity.txt"), intensity) if len(data) == 3: - np.savetxt(os.path.join(save_folder, "y_axis.txt"), y) - - return None + np.savetxt(str(save_folder / "y_axis.txt"), y) def write_out_file( - Sys: object, - Exp: object, - SimOpt: object, - *FitOpt: object, + Sys: Any, + Exp: Any, + SimOpt: Any, + *FitOpt: Any, current_best: bool = False, -) -> None: - """ - Write an output file with all datas from Sys, Exp, SimOpt and, if running - in optimization mode, FitOpt. + path: str | Path | None = None, +) -> Path: + """Write an output file with all data from ``Sys``, ``Exp``, ``SimOpt``. + + If running in optimization mode, ``FitOpt`` data is also written. Parameters ---------- Sys : object - Spinsystem object of module 'epr_setup'. + :class:`~spinanalysis.epr.Spinsystem` object. Exp : object - Experimental object of module 'epr_setup'. + :class:`~spinanalysis.epr.Experimental` object. SimOpt : object - SimulationOptions object of module 'epr_setup'. + :class:`~spinanalysis.epr.SimulationOptions` object. *FitOpt : object - FittingOptions object of module 'epr_setup'. + :class:`~spinanalysis.epr.FittingOptions` object (optional, only + in optimization mode). current_best : bool, optional - True if the given Sys and Exp are the current best while running in - optimization mode. False if Sys and Exp are the final result / are the - given Sys and Exp in normal simulation mode. The default is False. + ``True`` if *Sys* and *Exp* are the current best while running + in optimization mode. ``False`` if they are the final result or + in normal simulation mode, default is ``False``. + path : str or pathlib.Path, optional + Directory where the output file will be stored. If ``None``, + default is to create a new folder ``spinanalysis_YYYY-MM-DD_N`` + in the current working directory, where *N* is the lowest + available positive integer. Returns ------- - None - Nothing will be returned. + pathlib.Path + Path to the written output file. """ - if current_best: - title_part = "_current_best" - else: - title_part = "_result" - - # TODO Pfad überlegen um Ergebnisse zu speichern? - if len(FitOpt) == 1: - fit_mode = True - out = open(FitOpt[0].routine + title_part + ".eps_out", "w") - else: - fit_mode = False - out = open(SimOpt.routine + title_part + ".eps_out", "w") - - out.write("{:#^50s}\n".format("")) - - if current_best: - out.write("{:#^50s}\n".format(" CURRENT BEST ")) - else: - out.write("{:#^50s}\n".format(" OUTPUT-FILE ")) - - if fit_mode: - out.write("{:#^50s}\n".format(" " + FitOpt[0].routine + " ")) - else: - out.write("{:#^50s}\n".format(" " + SimOpt.routine + " ")) - - out.write("{:#^50s}\n".format(" AUTHOR: FLORIAN QUINTES ")) - out.write("{:#^50s}\n".format("")) - out.write("\n") - - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" SPINSYSTEM ")) - out.write("\n") - out.write("\n") - - out.write("{: ^15s} {: ^20s}\n".format("g-Tensor", "Value")) - np.set_printoptions(formatter={"float": "{:0.7f}".format}) - - for key in vars(Sys): - if key in ("g1", "g2"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) - - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("A-Tensor", "Value / MHz")) - np.set_printoptions(formatter={"float": "{:2.2f}".format}) + title_part = "_current_best" if current_best else "_result" + fit_mode = len(FitOpt) == 1 - for key in vars(Sys): - if key.startswith("A") and len(key) < 3: - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) + out_dir = _resolve_output_dir(path) - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("ZFS", "Value / MHz")) + routine = FitOpt[0].routine if fit_mode else SimOpt.routine + out_file = out_dir / f"{routine}{title_part}.spinanalysis" - for key in vars(Sys): - if key in ("D", "E", "J_ex"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) + authors = _get_authors() - out.write("\n") - out.write("{: ^15s} {: ^20s}\n".format("Orientations", "Angle / rad")) - np.set_printoptions(formatter={"float": "{: 0.1f}".format}) + with open(out_file, "w") as out: + out.write("{:#^50s}\n".format("")) - for key in vars(Sys): - if key.endswith("_frame"): - out.write("{0:>14s}: {1}\n".format(key, vars(Sys)[key])) + header = " CURRENT BEST " if current_best else " OUTPUT-FILE " + out.write("{:#^50s}\n".format(header)) - out.write("\n") - out.write("{: ^15s}|{: ^20s}\n".format("Nuclear spin", "Number of cores")) - - spins = [] - cores = [] - for key in vars(Sys): - if (key.startswith("n") or key.startswith("I")) and len(key) < 3: - if key.startswith("n"): - cores.append(key) - else: - spins.append(key) - cores.sort() - spins.sort() - - atoms = zip(cores, spins) - for core, spin in atoms: - out.write( - "{0:>7s}: {1:<6.1f}|{2:>9s}: {3:<9}\n".format( - core, vars(Sys)[core], spin, vars(Sys)[spin] - ) - ) + out.write("{:#^50s}\n".format(" " + routine + " ")) + out.write("{:#^50s}\n".format(f" AUTHOR: {authors.upper()} ")) + out.write("{:#^50s}\n".format("")) + out.write("\n") - out.write("\n") - out.write("{:>14s}: {:.2f} mT\n".format("width_gauss", Sys.width_gauss)) + # -- Spinsystem ------------------------------------------------ + out.write("\n\n") + out.write("{:#^50s}\n".format(" SPINSYSTEM ")) + out.write("\n\n") - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" EXPERIMENTAL ")) - out.write("\n") - out.write("\n") + # g-Tensor + out.write("{: ^15s} {: ^20s}\n".format("g-Tensor", "Value")) + with np.printoptions(formatter={"float": "{:0.7f}".format}): + for key in _G_FIELDS: + value = getattr(Sys, key, None) + if value is not None: + out.write("{0:>14s}: {1}\n".format(key, value)) - out.write("{0:>20s}: {1:<9.6f} mT\n".format("microwave amplitude", Exp.B_mw)) - out.write( - "{0:>20s}: {1:<9.6f} GHz\n".format("microwave frequency", Exp.freq_mw / 1e9) - ) - out.write("{0:>20s}: {1:<7.2f} mT\n".format("min B_z", Exp.B_z.min())) - out.write("{0:>20s}: {1:<7.2f} mT\n".format("max B_z", Exp.B_z.max())) - out.write("{0:>20s}: {1}\n".format("B_z points", len(Exp.B_z))) - - out.write("\n") - out.write("\n") - out.write("{:#^50s}\n".format(" SIMULATION OPTIONS ")) - out.write("\n") - out.write("\n") - - out.write("{0:>20s}: {1:<20s}\n".format("routine", SimOpt.routine)) - out.write("{0:>20s}: {1} cores\n".format("run at", SimOpt.cpu_cores)) - out.write("{0:>20s}: {1:<20}\n".format("grid points", SimOpt.grid_points)) - out.write("{0:>20s}: {1} space\n".format("using", SimOpt.space)) - - if fit_mode: + # A-Tensor out.write("\n") + out.write("{: ^15s} {: ^20s}\n".format("A-Tensor", "Value / MHz")) + with np.printoptions(formatter={"float": "{:2.2f}".format}): + for key in _A_FIELDS: + value = getattr(Sys, key, None) + if value is not None: + out.write("{0:>14s}: {1}\n".format(key, value)) + + # ZFS out.write("\n") - out.write("{:#^50s}\n".format(" FITTING OPTIONS ")) + out.write("{: ^15s} {: ^20s}\n".format("ZFS", "Value / MHz")) + for key in _ZFS_FIELDS: + value = getattr(Sys, key, None) + if value is not None: + out.write("{0:>14s}: {1}\n".format(key, value)) + + # Orientations out.write("\n") + out.write("{: ^15s} {: ^20s}\n".format("Orientations", "Angle / rad")) + with np.printoptions(formatter={"float": "{: 0.1f}".format}): + for key in _FRAME_FIELDS: + value = getattr(Sys, key, None) + if value is not None: + out.write("{0:>14s}: {1}\n".format(key, value)) + + # Nuclear spins out.write("\n") - - for key in vars(FitOpt[0]): - if key is not None: + out.write("{: ^15s}|{: ^20s}\n".format("Nuclear spin", "Number of cores")) + for core_key, spin_key in _NUCLEUS_PAIRS: + core_val = getattr(Sys, core_key, None) + spin_val = getattr(Sys, spin_key, None) + if core_val is not None and spin_val is not None: out.write( - ("{0:>20s}: " + "{1:<20s}\n").format(key, str(vars(FitOpt[0])[key])) + "{0:>7s}: {1:<6.1f}|{2:>9s}: {3:<9}\n".format( + core_key, core_val, spin_key, spin_val + ) ) - out.close() + out.write("\n") + out.write("{:>14s}: {:.2f} mT\n".format("width_gauss", Sys.width_gauss)) + + # -- Experimental ---------------------------------------------- + out.write("\n\n") + out.write("{:#^50s}\n".format(" EXPERIMENTAL ")) + out.write("\n\n") - return None + out.write("{0:>20s}: {1:<9.6f} mT\n".format("microwave amplitude", Exp.B_mw)) + out.write( + "{0:>20s}: {1:<9.6f} GHz\n".format("microwave frequency", Exp.freq_mw / 1e9) + ) + out.write("{0:>20s}: {1:<7.2f} mT\n".format("min B_z", Exp.B_z.min())) + out.write("{0:>20s}: {1:<7.2f} mT\n".format("max B_z", Exp.B_z.max())) + out.write("{0:>20s}: {1}\n".format("B_z points", len(Exp.B_z))) + + # -- Simulation options ---------------------------------------- + out.write("\n\n") + out.write("{:#^50s}\n".format(" SIMULATION OPTIONS ")) + out.write("\n\n") + + out.write("{0:>20s}: {1:<20s}\n".format("routine", SimOpt.routine)) + out.write("{0:>20s}: {1} cores\n".format("run at", SimOpt.cpu_cores)) + out.write("{0:>20s}: {1:<20}\n".format("knots", SimOpt.knots)) + out.write("{0:>20s}: {1} space\n".format("using", SimOpt.space)) + + # -- Fitting options ------------------------------------------- + if fit_mode: + out.write("\n\n") + out.write("{:#^50s}\n".format(" FITTING OPTIONS ")) + out.write("\n\n") + + for key, value in FitOpt[0].model_dump().items(): + if value is not None: + out.write("{0:>20s}: {1:<20s}\n".format(key, str(value))) + + return out_file diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..f3ae16a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,8 @@ +"""Test suite for the spinanalysis package. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" diff --git a/tests/__pycache__/__init__.cpython-313.pyc b/tests/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index db8aef5..0000000 Binary files a/tests/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/tests/__pycache__/test_core.cpython-313-pytest-9.1.1.pyc b/tests/__pycache__/test_core.cpython-313-pytest-9.1.1.pyc deleted file mode 100644 index 19cc397..0000000 Binary files a/tests/__pycache__/test_core.cpython-313-pytest-9.1.1.pyc and /dev/null differ diff --git a/tests/__pycache__/test_profiles.cpython-313-pytest-9.1.1.pyc b/tests/__pycache__/test_profiles.cpython-313-pytest-9.1.1.pyc deleted file mode 100644 index 5469b43..0000000 Binary files a/tests/__pycache__/test_profiles.cpython-313-pytest-9.1.1.pyc and /dev/null differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e22fd0e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,39 @@ +"""Shared pytest fixtures and configuration for the test suite. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import zipfile +from pathlib import Path + +import pytest + + +_ZIP_PATH = Path(__file__).parent / "data" / "test_loading.zip" + + +@pytest.fixture(scope="session") +def test_loading_dir(tmp_path_factory): + """Extract the test_loading ZIP archive once per session. + + Returns the path to the extracted ``test_loading`` directory. + """ + extract_root = tmp_path_factory.mktemp("test_loading_data") + with zipfile.ZipFile(_ZIP_PATH) as zf: + zf.extractall(extract_root) + return extract_root / "test_loading" + + +@pytest.fixture(autouse=True) +def _mock_loading_home(monkeypatch, test_loading_dir): + """Point ``Path.home()`` at the extracted test_loading directory. + + This lets ``get_full_path()`` find the test data folders without + ``start_directory`` for any test module that imports from + ``spinanalysis.loading``. + """ + monkeypatch.setattr(Path, "home", lambda: test_loading_dir) diff --git a/tests/data/test_loading.zip b/tests/data/test_loading.zip new file mode 100644 index 0000000..4e63a09 Binary files /dev/null and b/tests/data/test_loading.zip differ diff --git a/tests/data/test_profiles/spinsystem/test_Sys_triplet.ini b/tests/data/test_profiles/spinsystem/test_Sys_triplet.ini new file mode 100644 index 0000000..912d9a4 --- /dev/null +++ b/tests/data/test_profiles/spinsystem/test_Sys_triplet.ini @@ -0,0 +1,54 @@ +[main] +spin_system = trip +precursor = zf +g1 = 2.010, 2.011, 2.012 +g2 = 2.020, 2.021, 2.022 +g_tri = 2.003, 2.004, 2.005 +g = 2.006, 2.007, 2.008 +g1_frame = 10, 20, 30 +g2_frame = 40, 50, 60 +g_tri_frame = 0, 0, 0 +g_frame = 0, 0, 0 +A1 = 10, 20, 30 +A2 = 0, 40, 0 +A3 = 0, 0, 50 +A4 = 0, 0, 0 +A5 = 0, 0, 0 +A1_frame = 1, 2, 3 +A2_frame = 0, 0, 0 +A3_frame = 0, 0, 0 +A4_frame = 0, 0, 0 +A5_frame = 0, 0, 0 +D_frame = 0, 0, 0 +D_tri_frame = 0, 0, 0 +A_eseem = 2.0 +omega_I = 3.0 +n1 = 1 +I1 = 0.5 +n2 = 2 +I2 = 1.0 +n3 = 0 +I3 = 0.0 +n4 = 0 +I4 = 0.0 +n5 = 0 +I5 = 0.0 +D = 5.0 +D_tri = 6.0 +E = 0.5 +E_tri = 0.6 +J_ex = 0.7 +J_0 = 8.0 +beta = 1.2 +width_gauss = 0.4 +T_relax_1 = 1.0 +T_relax_2 = 2.0 +T_pm = 3.0 +decay = 4.0 +population = 0.2, 0.3, 0.5 +dynamics = 1, 2, 3, 4 +amplitude = 0.8 +acceptor_list = 1, 3 +donor_list = 2, 4 +distribution_order = 2 +distribution = 0.1, 0.2, 0.3, 0.4 diff --git a/tests/data/test_profiles/variation/test_Var_radpair.ini b/tests/data/test_profiles/variation/test_Var_radpair.ini new file mode 100644 index 0000000..b2ac892 --- /dev/null +++ b/tests/data/test_profiles/variation/test_Var_radpair.ini @@ -0,0 +1,13 @@ +[main] +g1 = 0.001, 0.002, 0.0 +g2 = 0.0, 0.003, 0.0 +A1 = 5.0, 0.0, 0.0 +D = 2.0 +E = 0.5 +width_gauss = 0.1 +g1_frame = 0.0, 0.0, 0.0 +g2_frame = 0.0, 0.0, 0.0 +A1_frame = 0.0, 0.0, 0.0 +D_frame = 0.0, 0.0, 0.0 +freq_mw = 0.0 +amplitude = 0.0 diff --git a/tests/test_core.py b/tests/test_core.py index e69de29..e7492f2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -0,0 +1,345 @@ +"""Tests for :mod:`spinanalysis.core` simulate and optimize workflows. + +The external simulation packages (``mkl``, ``genetic_radpair``, ``oop_eseem``, +``teacups``, ``static_radical_pair``, ``PySpin``) are mocked via +``sys.modules`` so that ``spinanalysis.core`` and +``spinanalysis._interface_handler`` can be imported in a clean environment. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import sys +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest + +# --------------------------------------------------------------------------- +# Pre-populate sys.modules with mock external dependencies. +# This must happen *before* importing spinanalysis.core or +# spinanalysis._interface_handler, both of which import these packages. +# --------------------------------------------------------------------------- +_mock_modules = { + "mkl": MagicMock(), + "genetic_radpair": MagicMock(), + "genetic_radpair.genetic_classes": MagicMock(), + "oop_eseem": MagicMock(), + "oop_eseem.opossum": MagicMock(), + "teacups": MagicMock(), + "teacups.simulations": MagicMock(), + "static_radical_pair": MagicMock(), + "static_radical_pair.radpair": MagicMock(), + "PySpin": MagicMock(), + "PySpin.plotter": MagicMock(), +} +for _name, _mod in _mock_modules.items(): + sys.modules.setdefault(_name, _mod) + +# Pre-import _interface_handler so it is available for patching. Its +# top-level external imports resolve to the mock modules above. +import spinanalysis._interface_handler # noqa: E402, F401 +from spinanalysis.core import optimize, simulate # noqa: E402 +from spinanalysis.epr import ( # noqa: E402 + Experimental, + FittingOptions, + SimulationOptions, + Spinsystem, + Variation, +) + + +@pytest.fixture(autouse=True) +def _reset_mocks(): + """Reset all mock modules before each test.""" + for mod in _mock_modules.values(): + mod.reset_mock() + yield + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _default_objects(routine=""): + """Return ``(Sys, Exp, SimOpt)`` with sensible defaults for simulate.""" + return ( + Spinsystem(), + Experimental(), + SimulationOptions(routine=routine), + ) + + +def _default_opt_objects(routine=""): + """Return ``(Sys, Exp, SimOpt, FitOpt, Var)`` for optimize.""" + return ( + Spinsystem(), + Experimental(), + SimulationOptions(), + FittingOptions(routine=routine), + Variation(), + ) + + +# --------------------------------------------------------------------------- +# simulate – dispatch +# --------------------------------------------------------------------------- + + +class TestSimulateDispatch: + """Each routine calls the correct external function with the right args.""" + + def test_static_radpair(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.array([1.0, 2.0, 3.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + + def test_teacups(self): + mock_fn = sys.modules["teacups.simulations"].teacups + mock_fn.return_value = np.array([0.5, 1.0, 0.5]) + sys_obj, exp, simopt = _default_objects("teacups") + simopt.eigval_mode = False + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + + def test_opossum(self): + mock_fn = sys.modules["oop_eseem.opossum"].oop_eseem + mock_fn.return_value = np.array([1.0, -1.0]) + sys_obj, exp, simopt = _default_objects("opossum") + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + + def test_didelphis(self): + mock_fn = sys.modules["oop_eseem.opossum"].oop_eseem_distribution + mock_fn.return_value = np.array([2.0, 4.0]) + sys_obj, exp, simopt = _default_objects("didelphis") + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + + def test_didelphis_tikhonov(self): + mock_fn = sys.modules["oop_eseem.opossum"].oop_eseem_distance_distribution + mock_fn.return_value = np.array([3.0, 6.0]) + sys_obj, exp, simopt = _default_objects("didelphis_tikhonov") + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + + def test_unknown_routine_raises(self): + sys_obj, exp, simopt = _default_objects("nonexistent") + with pytest.raises(ValueError, match="Can't find a routine named"): + simulate(sys_obj, exp, simopt) + + def test_routine_case_insensitive(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.array([1.0]) + sys_obj, exp, simopt = _default_objects("STATIC_RADPAIR") + simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once() + + +# --------------------------------------------------------------------------- +# simulate – teacups eigval_mode branch +# --------------------------------------------------------------------------- + + +class TestSimulateTeacupsBranch: + """The teacups routine branches on ``SimOpt.eigval_mode``.""" + + def test_eigval_mode_true_returns_ones(self): + mock_fn = sys.modules["teacups.simulations"].teacups + sys_obj, exp, simopt = _default_objects("teacups") + simopt.eigval_mode = True + exp.t_points = 5 + exp.B_z = np.linspace(300, 310, 10) + result = simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + assert result.shape == (5, 10) + assert np.all(result == 1.0) + + def test_eigval_mode_false_calls_teacups_and_returns_its_value(self): + mock_fn = sys.modules["teacups.simulations"].teacups + mock_fn.return_value = np.array([2.0, 4.0, 8.0]) + sys_obj, exp, simopt = _default_objects("teacups") + simopt.eigval_mode = False + result = simulate(sys_obj, exp, simopt) + mock_fn.assert_called_once_with(sys_obj, exp, simopt) + assert np.allclose(result, np.array([0.25, 0.5, 1.0])) + + def test_eigval_mode_true_does_not_set_fitting_mode(self): + sys.modules["teacups.simulations"].teacups + sys_obj, exp, simopt = _default_objects("teacups") + simopt.eigval_mode = True + simulate(sys_obj, exp, simopt) + assert simopt.mode == "simulation" + + +# --------------------------------------------------------------------------- +# simulate – normalization +# --------------------------------------------------------------------------- + + +class TestSimulateNormalization: + """The returned spectrum is normalized to a maximum absolute value of 1.""" + + def test_positive_spectrum_normalized(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.array([1.0, 2.0, 4.0, 2.0, 1.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + result = simulate(sys_obj, exp, simopt) + assert np.isclose(abs(result).max(), 1.0) + + def test_negative_spectrum_normalized(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.array([-1.0, -3.0, -6.0, -3.0, -1.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + result = simulate(sys_obj, exp, simopt) + assert np.isclose(abs(result).max(), 1.0) + assert np.isclose(result.min(), -1.0) + + def test_all_zero_spectrum_no_nan(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.zeros(100) + sys_obj, exp, simopt = _default_objects("static_radpair") + result = simulate(sys_obj, exp, simopt) + assert not np.any(np.isnan(result)) + assert np.all(result == 0.0) + + +# --------------------------------------------------------------------------- +# simulate – side effects +# --------------------------------------------------------------------------- + + +class TestSimulateSideEffects: + """``simulate`` mutates ``SimOpt.mode`` and ``Exp.spec_sim``.""" + + def test_mode_set_to_simulation(self): + sys.modules[ + "static_radical_pair.radpair" + ].do_simulation_multicore.return_value = np.array([1.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + simulate(sys_obj, exp, simopt) + assert simopt.mode == "simulation" + + def test_spec_sim_set_to_returned_spectrum(self): + mock_fn = sys.modules["static_radical_pair.radpair"].do_simulation_multicore + mock_fn.return_value = np.array([3.0, 6.0, 3.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + result = simulate(sys_obj, exp, simopt) + assert np.array_equal(exp.spec_sim, result) + + def test_teacups_non_eigval_sets_mode_to_fitting(self): + sys.modules["teacups.simulations"].teacups.return_value = np.array([1.0]) + sys_obj, exp, simopt = _default_objects("teacups") + simopt.eigval_mode = False + simulate(sys_obj, exp, simopt) + assert simopt.mode == "fitting" + + +# --------------------------------------------------------------------------- +# simulate – return type +# --------------------------------------------------------------------------- + + +class TestSimulateReturnType: + def test_returns_ndarray(self): + sys.modules[ + "static_radical_pair.radpair" + ].do_simulation_multicore.return_value = np.array([1.0, 2.0]) + sys_obj, exp, simopt = _default_objects("static_radpair") + result = simulate(sys_obj, exp, simopt) + assert isinstance(result, np.ndarray) + + +# --------------------------------------------------------------------------- +# optimize – dispatch +# --------------------------------------------------------------------------- + + +class TestOptimizeDispatch: + """Each optimization routine calls the correct function with the right args.""" + + def test_genetic(self): + mock_gr = sys.modules["genetic_radpair.genetic_classes"].Genetic_Radpair + mock_gr.return_value.best_spinsystem = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("genetic") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_gr.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.dualannealing") + def test_dual_annealing(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("dual_annealing") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.shgo") + def test_shgo(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("shgo") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.differential_evolution") + def test_differential_evolution(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects( + "differential_evolution" + ) + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.basinhopping") + def test_basinhopping(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("basinhopping") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.least_squares") + def test_least_squares(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("least_squares") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.minimize") + def test_minimize(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("minimize") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_fn.assert_called_once_with(sys_obj, exp, simopt, fitopt, var) + assert result == "best_sys" + + def test_unknown_routine_raises(self): + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("nonexistent") + with pytest.raises( + ValueError, match="Can't find an optimization routine named" + ): + optimize(sys_obj, exp, simopt, fitopt, var) + + def test_routine_case_insensitive(self): + mock_gr = sys.modules["genetic_radpair.genetic_classes"].Genetic_Radpair + mock_gr.return_value.best_spinsystem = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("GENETIC") + result = optimize(sys_obj, exp, simopt, fitopt, var) + mock_gr.assert_called_once() + assert result == "best_sys" + + @patch("spinanalysis._interface_handler.minimize") + def test_mode_set_to_fitting(self, mock_fn): + mock_fn.return_value = "best_sys" + sys_obj, exp, simopt, fitopt, var = _default_opt_objects("minimize") + optimize(sys_obj, exp, simopt, fitopt, var) + assert simopt.mode == "fitting" diff --git a/tests/test_epr.py b/tests/test_epr.py new file mode 100644 index 0000000..04b6111 --- /dev/null +++ b/tests/test_epr.py @@ -0,0 +1,840 @@ +"""Tests for the Pydantic EPR data models in :mod:`spinanalysis.epr`. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest +from pydantic import ValidationError +import scipy.constants as constant + +from spinanalysis.epr import Experimental, Spinsystem, Variation +from spinanalysis.epr import FittingOptions, SimulationOptions + + +class TestExperimental: + def test_default_values(self): + experiment = Experimental() + + assert experiment.B_z.shape == (100,) + assert experiment.B_z.dtype == np.float64 + assert np.array_equal(experiment.magnetic_field, experiment.B_z) + assert experiment.magnetic_field.dtype == np.float64 + assert experiment.freq_mw == pytest.approx(9.7e9) + assert experiment.B_mw == pytest.approx(1e-3) + assert experiment.int is None + assert np.array_equal(experiment.spec_sim, np.zeros(100)) + assert experiment.spec_sim.dtype == np.complex128 + + @pytest.mark.parametrize( + ("kwargs", "expected"), + [ + ({"real_int": [1, 2, 3]}, [1 + 0j, 2 + 0j, 3 + 0j]), + ({"imag_int": [1, 2, 3]}, [0 + 1j, 0 + 2j, 0 + 3j]), + ( + {"real_int": [1, 2], "imag_int": [3, 4]}, + [1 + 3j, 2 + 4j], + ), + ( + {"cmplx_int": [1 + 2j, 3 + 4j]}, + [1 / np.sqrt(5) + 2j / np.sqrt(5), 3 / np.sqrt(5) + 4j / np.sqrt(5)], + ), + ({"real_int": [-1, -2, -10]}, [-0.1, -0.2, -1.0]), + ({"imag_int": [-1, -2, -10]}, [-0.1j, -0.2j, -1.0j]), + ( + {"real_int": [-2, 4], "imag_int": [6, -20]}, + [-0.1 + 0.3j, 0.2 - 1.0j], + ), + ( + {"cmplx_int": [-1 - 2j, 3 + 4j, -15 + 8j]}, + [(-1 - 2j) / 17, (3 + 4j) / 17, (-15 + 8j) / 17], + ), + ], + ) + def test_intensity_input_combinations(self, kwargs, expected): + experiment = Experimental(magnetic_field=np.arange(len(expected)), **kwargs) + expected = np.asarray(expected, dtype=np.complex128) + expected /= np.abs(expected).max() + + assert np.allclose(experiment.int, expected) + assert experiment.int.dtype == np.complex128 + assert experiment.spec_sim.shape == experiment.int.shape + assert experiment.spec_sim.dtype == np.complex128 + + @pytest.mark.parametrize("field", [[1, 2, 3], (1, 2, 3)]) + def test_field_input_sequences_become_float64_arrays(self, field): + experiment = Experimental(magnetic_field=field) + + assert isinstance(experiment.B_z, np.ndarray) + assert isinstance(experiment.magnetic_field, np.ndarray) + assert experiment.B_z.dtype == np.float64 + assert experiment.magnetic_field.dtype == np.float64 + + @pytest.mark.parametrize("name", ["real_int", "imag_int", "time_axis"]) + @pytest.mark.parametrize("value", [[1, 2, 3], (1, 2, 3)]) + def test_real_input_sequences_become_float64_arrays(self, name, value): + kwargs = {name: value} + if name != "time_axis": + kwargs["magnetic_field"] = [1, 2, 3] + experiment = Experimental(**kwargs) + + result = experiment.time_axis if name == "time_axis" else experiment.int + assert isinstance(result, np.ndarray) + assert result.dtype == (np.float64 if name == "time_axis" else np.complex128) + + @pytest.mark.parametrize("value", [[1 + 2j, 3 + 4j], (1 + 2j, 3 + 4j)]) + def test_complex_input_sequences_become_complex128_arrays(self, value): + experiment = Experimental(magnetic_field=[1, 2], cmplx_int=value) + + assert isinstance(experiment.int, np.ndarray) + assert experiment.int.dtype == np.complex128 + + @pytest.mark.parametrize("name", ["real_int", "imag_int", "time_axis"]) + def test_real_inputs_reject_complex_values(self, name): + with pytest.raises(ValidationError): + Experimental(**{name: [1 + 1j, 2 + 0j]}) + + def test_cmplx_int_rejects_real_values(self): + with pytest.raises(ValidationError): + Experimental(cmplx_int=[1, 2]) + + def test_cmplx_int_cannot_be_combined_with_component_inputs(self): + with pytest.raises(ValidationError): + Experimental(cmplx_int=np.array([1 + 1j]), real_int=[1]) + with pytest.raises(ValidationError): + Experimental(cmplx_int=np.array([1 + 1j]), imag_int=[1]) + + def test_component_intensities_require_matching_shapes(self): + with pytest.raises(ValidationError): + Experimental(real_int=[1, 2], imag_int=[1]) + + def test_zero_intensity_is_not_changed_by_rescaling(self): + zeros = np.zeros(3, dtype=np.complex128) + experiment = Experimental(magnetic_field=[1, 2, 3], cmplx_int=zeros) + + assert np.array_equal(experiment.int, zeros) + assert np.array_equal(experiment.spec_sim, zeros) + + def test_time_axis_is_converted_and_configured(self): + experiment = Experimental(time_axis=[0.0, 0.5, 1.0]) + + assert isinstance(experiment.time_axis, np.ndarray) + assert np.array_equal(experiment.time_axis, [0.0, 0.5, 1.0]) + assert experiment.t_scale == [0.0, 1.0] + assert experiment.t_points == 3 + + @pytest.mark.parametrize("name", ["magnetic_field", "time_axis"]) + def test_axes_reject_complex_values(self, name): + with pytest.raises(ValidationError): + Experimental(**{name: [1 + 1j, 2 + 0j]}) + + def test_get_linear_time_axis_defaults(self): + experiment = Experimental() + + with pytest.raises(ValueError): + experiment.get_linear_time_axis() + + @pytest.mark.parametrize( + "kwargs", + [{"t_min": 1.0}, {"t_max": 2.0}, {"t_min": None, "t_max": None}], + ) + def test_get_linear_time_axis_requires_both_bounds_without_time_axis(self, kwargs): + with pytest.raises(ValueError): + Experimental().get_linear_time_axis(**kwargs) + + @pytest.mark.parametrize( + "kwargs", + [{"t_min": 1.0}, {"t_max": 2.0}, {}], + ) + def test_get_linear_time_axis_uses_existing_time_axis(self, kwargs): + experiment = Experimental(time_axis=[0.0, 1.0, 2.0]) + experiment.get_linear_time_axis(**kwargs) + + assert isinstance(experiment.time_axis, np.ndarray) + assert experiment.time_axis.dtype == np.float64 + + @pytest.mark.parametrize( + "kwargs", + [ + {"t_min": 1.0, "t_max": 2.0}, + {"t_min": 1.0, "t_max": 2.0, "t_points": 5}, + ], + ) + def test_get_linear_time_axis_accepts_both_bounds_without_time_axis(self, kwargs): + experiment = Experimental() + experiment.get_linear_time_axis(**kwargs) + + assert isinstance(experiment.time_axis, np.ndarray) + assert experiment.time_axis.dtype == np.float64 + + def test_get_linear_time_axis_accepts_valid_overrides(self): + experiment = Experimental(time_axis=[0.0, 1.0]) + experiment.get_linear_time_axis(t_min=1.0, t_max=2.0, t_points=5) + + assert np.array_equal(experiment.time_axis, np.linspace(1.0, 2.0, 5)) + assert experiment.time_axis.dtype == np.float64 + assert experiment.t_scale == [1.0, 2.0] + assert experiment.t_points == 5 + + @pytest.mark.parametrize( + "kwargs", + [ + {"t_points": 1}, + {"t_points": 0}, + {"t_min": 2.0, "t_max": 1.0}, + {"t_min": 1.0, "t_max": 1.0}, + ], + ) + def test_get_linear_time_axis_rejects_invalid_values(self, kwargs): + with pytest.raises(ValueError): + Experimental().get_linear_time_axis(**kwargs) + + +class TestSimulationOptions: + def test_default_values(self): + options = SimulationOptions() + + assert options.routine == "" + assert options.cpu_cores == 0 + assert options.knots == 20 + assert options.refinement == 1 + assert options.space == "hilbert" + assert options.pop_evolution is True + assert options.eigval_mode is False + assert options.r_points == 401 + assert options.regularization_mode == 2 + + @pytest.mark.parametrize( + ("name", "value"), + [ + ("cpu_cores", "one"), + ("knots", "many"), + ("space", 2), + ("eigval_mode", "yes"), + ("r_points", 401.5), + ], + ) + def test_invalid_types_are_rejected(self, name, value): + with pytest.raises(ValidationError): + SimulationOptions(**{name: value}) + + def test_assignment_is_validated(self): + options = SimulationOptions() + options.knots = 1000 + assert options.knots == 1000 + + with pytest.raises(ValidationError): + options.knots = "many" + + def test_grid_points_warns_and_remains_supported(self): + options = SimulationOptions() + + with pytest.deprecated_call(match="grid_points.*knots"): + options.grid_points = 1000 + + assert options.grid_points == 1000 + assert options.knots == 20 + + with pytest.deprecated_call(match="grid_points.*knots"): + legacy = SimulationOptions(grid_points=750) + + assert legacy.grid_points == 750 + assert legacy.knots == 20 + + def test_profile_round_trip(self, tmp_path, monkeypatch): + _install_option_profiles(tmp_path, monkeypatch, "simulation") + options = SimulationOptions(routine="static_radpair", cpu_cores=2, knots=25) + options.save("simulation_options") + + loaded = SimulationOptions() + loaded.load("simulation_options") + + assert loaded.routine == "static_radpair" + assert loaded.cpu_cores == 2 + assert loaded.knots == 25 + + @pytest.mark.parametrize("name", ["knots", "grid_points"]) + @pytest.mark.parametrize("value", [0, -1]) + def test_grid_values_must_be_positive(self, name, value): + if name == "grid_points": + with pytest.deprecated_call(match="grid_points.*knots"): + with pytest.raises(ValidationError): + SimulationOptions(**{name: value}) + else: + with pytest.raises(ValidationError): + SimulationOptions(**{name: value}) + + options = SimulationOptions() + if name == "grid_points": + with pytest.deprecated_call(match="grid_points.*knots"): + with pytest.raises(ValidationError): + setattr(options, name, value) + else: + with pytest.raises(ValidationError): + setattr(options, name, value) + + +class TestFittingOptions: + def test_default_values(self): + options = FittingOptions() + + assert options.routine is None + assert options.method is None + assert options.cpu_cores == 0 + assert options.gui is False + assert options.GAVaPS is True + + @pytest.mark.parametrize( + ("name", "value"), + [ + ("cpu_cores", "one"), + ("gui", "yes"), + ("maxiter", "many"), + ("p_c", "half"), + ("error_weight", 1.0), + ], + ) + def test_invalid_types_are_rejected(self, name, value): + with pytest.raises(ValidationError): + FittingOptions(**{name: value}) + + def test_assignment_is_validated(self): + options = FittingOptions() + options.maxiter = 100 + assert options.maxiter == 100 + + with pytest.raises(ValidationError): + options.maxiter = "many" + + def test_profile_round_trip(self, tmp_path, monkeypatch): + profile_root = _install_option_profiles(tmp_path, monkeypatch, "optimization") + options = FittingOptions(routine="minimize", cpu_cores=2, maxiter=250) + options.save("fitting_options") + + loaded = FittingOptions() + loaded.load("fitting_options") + + assert loaded.routine == "minimize" + assert loaded.cpu_cores == 2 + assert loaded.maxiter == 250 + assert (profile_root / "optimization" / "fitting_options.ini").exists() + + +class TestSpinsystem: + def test_default_values(self): + system = Spinsystem() + + assert np.array_equal(system.g1, [2.002, 2.002, 2.002]) + assert np.array_equal(system.g2, [2.004, 2.004, 2.004]) + assert np.array_equal(system.g_tri, [2.002, 2.002, 2.002]) + assert np.array_equal(system.g, [2.002, 2.002, 2.002]) + assert system.g1_iso == pytest.approx(2.002) + assert system.g2_iso == pytest.approx(2.004) + assert system.spin_system == "rp" + assert system.precursor == "singlet" + assert np.array_equal(system.acceptor_list, [1, 2, 3]) + assert np.array_equal(system.donor_list, [4, 5]) + assert system.width_gauss == pytest.approx(0.5) + assert system.distribution is None + assert system.distribution_order == 3 + + def test_values_can_be_set(self): + system = Spinsystem() + values = { + "g1": [1.0, 2.0, 3.0], + "g2": (4.0, 5.0, 6.0), + "spin_system": "trip", + "precursor": "zf", + "acceptor_list": [1, 3], + "donor_list": (2, 4), + "n1": 2, + "I1": 0.5, + "D": 10.0, + "E": 1.0, + "width_gauss": 0.25, + } + + for name, value in values.items(): + setattr(system, name, value) + + assert np.array_equal(system.g1, [1.0, 2.0, 3.0]) + assert system.g1_iso == pytest.approx(2.0) + assert np.array_equal(system.g2, [4.0, 5.0, 6.0]) + assert system.g2_iso == pytest.approx(5.0) + assert system.spin_system == "trip" + assert system.precursor == "zf" + assert system.n1 == 2 + assert system.I1 == pytest.approx(0.5) + assert system.D == pytest.approx(10.0) + assert system.E == pytest.approx(1.0) + assert system.width_gauss == pytest.approx(0.25) + + @pytest.mark.parametrize("name", ["n1", "n2", "n3", "n4", "n5"]) + @pytest.mark.parametrize("value", [0, 1, 5]) + def test_nuclear_counts_accept_nonnegative_integers(self, name, value): + system = Spinsystem(**{name: value}) + assert getattr(system, name) == value + + @pytest.mark.parametrize("name", ["n1", "n2", "n3", "n4", "n5"]) + @pytest.mark.parametrize("value", [-1, 0.5, 1.2, "1", True]) + def test_nuclear_counts_reject_invalid_values(self, name, value): + with pytest.raises(ValidationError): + Spinsystem(**{name: value}) + + @pytest.mark.parametrize("name", ["I1", "I2", "I3", "I4", "I5"]) + @pytest.mark.parametrize("value", [0, 0.5, 1.0, 2.5]) + def test_nuclear_spins_accept_nonnegative_half_integers(self, name, value): + system = Spinsystem(**{name: value}) + assert getattr(system, name) == pytest.approx(value) + + @pytest.mark.parametrize("name", ["I1", "I2", "I3", "I4", "I5"]) + @pytest.mark.parametrize("value", [-0.5, 0.25, 1.2, "spin", True]) + def test_nuclear_spins_reject_invalid_values(self, name, value): + with pytest.raises(ValidationError): + Spinsystem(**{name: value}) + + def test_nuclear_values_are_validated_on_assignment(self): + system = Spinsystem() + + system.n1 = 3 + system.I1 = 1.5 + assert system.n1 == 3 + assert system.I1 == pytest.approx(1.5) + + with pytest.raises(ValidationError): + system.n1 = -1 + with pytest.raises(ValidationError): + system.I1 = 0.25 + + @pytest.mark.parametrize( + "attribute", + [ + "g1", + "g2", + "g_tri", + "g", + "A1", + "A1_frame", + "acceptor_list", + "donor_list", + "population", + ], + ) + @pytest.mark.parametrize("value", [[1, 2, 3], (1, 2, 3)]) + def test_list_and_tuple_values_are_numpy_arrays(self, attribute, value): + system = Spinsystem(**{attribute: value}) + + assert isinstance(getattr(system, attribute), np.ndarray) + + @pytest.mark.parametrize("profile_name", ["test_Sys_radpair", "test_Sys_triplet"]) + def test_load_profile(self, tmp_path, monkeypatch, profile_name): + self._install_profile_fixture(tmp_path, monkeypatch) + system = Spinsystem() + system.load(profile_name) + + if profile_name == "test_Sys_radpair": + assert np.array_equal(system.g1, [2.002, 2.002, 2.002]) + assert np.array_equal(system.A1, [20.0, 0.0, 0.0]) + assert system.D == pytest.approx(10.0) + assert system.n1 == 2 + else: + assert system.spin_system == "trip" + assert system.precursor == "zf" + assert np.array_equal(system.g1_frame, [10.0, 20.0, 30.0]) + assert np.array_equal(system.distribution, [[0.1, 0.2], [0.3, 0.4]]) + assert system.distribution_order == 2 + assert system.I2 == pytest.approx( + 1.0 if profile_name == "test_Sys_triplet" else 1.0 + ) + + def test_load_profile_with_degree_angles(self, tmp_path, monkeypatch): + self._install_profile_fixture(tmp_path, monkeypatch) + system = Spinsystem() + system.load("test_Sys_radpair", degree=True) + + assert np.array_equal(system.g1_frame, [0.0, 0.0, 0.0]) + assert np.array_equal(system.D_frame, [0.0, 0.0, 0.0]) + + def test_save_contains_all_profile_fields(self, tmp_path, monkeypatch): + profile_root = self._install_profile_fixture(tmp_path, monkeypatch) + + system = Spinsystem() + system.g1 = np.array([2.1, 2.2, 2.3]) + system.g2 = np.array([2.4, 2.5, 2.6]) + system.distribution = np.array([[0.1, 0.2], [0.3, 0.4]]) + system.save("saved_system") + + saved = profile_root / "spinsystem" / "saved_system.ini" + assert saved.exists() + + loaded = Spinsystem() + loaded.load("saved_system") + for name in Spinsystem.PROFILE_FIELDS: + expected = getattr(system, name) + actual = getattr(loaded, name) + if isinstance(expected, np.ndarray): + assert np.array_equal(actual, expected), name + else: + assert actual == expected, name + + @pytest.mark.parametrize("init_degree", [False, True]) + @pytest.mark.parametrize("save_degree", [False, True]) + def test_frame_units_round_trip( + self, tmp_path, monkeypatch, init_degree, save_degree + ): + profile_root = self._install_profile_fixture(tmp_path, monkeypatch) + input_angles = np.array([10.0, 20.0, 30.0]) + expected_angles = input_angles * np.pi / 180 if init_degree else input_angles + + system = Spinsystem( + degree=init_degree, + g1_frame=input_angles, + A1_frame=input_angles, + D_tri_frame=input_angles, + ) + system.save("units_system", degree=save_degree) + + loaded = Spinsystem() + loaded.load("units_system", degree=save_degree) + + assert np.allclose(loaded.g1_frame, expected_angles) + assert np.allclose(loaded.A1_frame, expected_angles) + assert np.allclose(loaded.D_tri_frame, expected_angles) + assert (profile_root / "spinsystem" / "units_system.ini").exists() + + def _install_profile_fixture(self, tmp_path, monkeypatch): + profile_root = tmp_path / "profiles" + profile_dir = profile_root / "spinsystem" + profile_dir.mkdir(parents=True) + source_dir = Path(__file__).parent / "data" / "test_profiles" / "spinsystem" + for fixture in source_dir.glob("*.ini"): + shutil.copy(fixture, profile_dir) + shutil.copy( + Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini"), + profile_dir, + ) + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", profile_root) + return profile_root + + +class TestVariation: + def test_default_values(self): + var = Variation() + + assert np.array_equal(var.g1, [0.0, 0.0, 0.0]) + assert np.array_equal(var.g2, [0.0, 0.0, 0.0]) + assert np.array_equal(var.A1, [0.0, 0.0, 0.0]) + assert var.D == pytest.approx(0.0) + assert var.E == pytest.approx(0.0) + assert var.width_gauss == pytest.approx(0.0) + assert var.freq_mw == pytest.approx(0.0) + assert var.amplitude == pytest.approx(0.0) + assert var.fit_distribution is False + assert var.needed_digits == 0 + assert var.number_of_genes == 0 + assert np.array_equal(var.variation_array, np.array([])) + assert var.boundaries == [] + assert var.bohr_magneton == pytest.approx( + constant.value("Bohr magneton in Hz/T") + ) + + def test_default_data_types(self): + var = Variation() + + assert isinstance(var.g1, np.ndarray) + assert var.g1.dtype == np.float64 + assert isinstance(var.g2, np.ndarray) + assert var.g2.dtype == np.float64 + assert isinstance(var.A1, np.ndarray) + assert var.A1.dtype == np.float64 + assert isinstance(var.D, float) + assert isinstance(var.E, float) + assert isinstance(var.width_gauss, float) + assert isinstance(var.freq_mw, float) + assert isinstance(var.amplitude, float) + assert isinstance(var.fit_distribution, bool) + assert isinstance(var.needed_digits, int) + assert isinstance(var.number_of_genes, int) + assert isinstance(var.variation_array, np.ndarray) + assert var.variation_array.dtype == np.float64 + assert isinstance(var.boundaries, list) + assert isinstance(var.bohr_magneton, float) + assert isinstance(var.non_vars, tuple) + assert isinstance(var.single_vars, tuple) + + def test_values_can_be_set(self): + var = Variation() + var.g1 = np.array([0.001, 0.002, 0.003]) + var.D = 2.0 + var.width_gauss = 0.1 + var.fit_distribution = True + + assert np.allclose(var.g1, [0.001, 0.002, 0.003]) + assert var.D == pytest.approx(2.0) + assert var.width_gauss == pytest.approx(0.1) + assert var.fit_distribution is True + + @pytest.mark.parametrize( + "attribute", + [ + "g1", + "g2", + "g_tri", + "g", + "A1", + "A2", + "A3", + "A4", + "A5", + "g1_frame", + "A1_frame", + "D_frame", + "population", + ], + ) + @pytest.mark.parametrize("value", [[1, 2, 3], (1, 2, 3)]) + def test_list_and_tuple_values_are_numpy_arrays(self, attribute, value): + var = Variation(**{attribute: value}) + + assert isinstance(getattr(var, attribute), np.ndarray) + assert getattr(var, attribute).dtype == np.float64 + + def test_init_with_data_zeros_unspecified_profile_fields(self): + var = Variation(D=2.0) + + assert var.D == pytest.approx(2.0) + assert np.array_equal(var.g1, [0.0, 0.0, 0.0]) + assert np.array_equal(var.A1, [0.0, 0.0, 0.0]) + assert var.E == pytest.approx(0.0) + assert var.width_gauss == pytest.approx(0.0) + + def test_assignment_is_validated(self): + var = Variation() + var.D = 5.0 + assert var.D == pytest.approx(5.0) + + var.g1 = [0.1, 0.2, 0.3] + assert isinstance(var.g1, np.ndarray) + assert var.g1.dtype == np.float64 + assert np.allclose(var.g1, [0.1, 0.2, 0.3]) + + def test_get_number_of_genes_counts_single_vars(self): + var = Variation() + var.D = 2.0 + var.E = 0.5 + var.width_gauss = 0.1 + + var.get_number_of_genes() + assert var.number_of_genes == 3 + + def test_get_number_of_genes_counts_array_elements(self): + var = Variation() + var.g1 = np.array([0.001, 0.0, 0.003]) + var.A1 = np.array([5.0, 0.0, 0.0]) + + var.get_number_of_genes() + assert var.number_of_genes == 3 + + def test_get_number_of_genes_counts_mixed_vars(self): + var = Variation() + var.g1 = np.array([0.001, 0.0, 0.003]) + var.D = 2.0 + var.A1 = np.array([5.0, 0.0, 0.0]) + var.E = 0.0 + + var.get_number_of_genes() + assert var.number_of_genes == 4 + + def test_get_number_of_genes_zero_when_nothing_varied(self): + var = Variation() + var.get_number_of_genes() + assert var.number_of_genes == 0 + + def test_get_variation_array_collects_values(self): + var = Variation() + var.g1 = np.array([0.001, 0.0, 0.003]) + var.D = 2.0 + var.A1 = np.array([5.0, 0.0, 0.0]) + + var.get_variation_array() + assert var.variation_array.shape == (4,) + assert np.allclose( + np.sort(var.variation_array), np.sort([0.001, 0.003, 2.0, 5.0]) + ) + assert var.variation_array.dtype == np.float64 + + def test_get_needed_digits(self): + var = Variation() + var.D = 2.0 + var.E = 0.5 + + var.get_needed_digits() + assert var.number_of_genes == 2 + assert var.needed_digits == 24 + + def test_update_digits(self): + var = Variation() + var.D = 2.0 + var.E = 0.5 + var.g1 = np.array([0.001, 0.0, 0.0]) + + var.update_digits() + assert var.number_of_genes == 3 + assert var.needed_digits == 36 + + def test_get_boundaries_single_vars(self): + var = Variation() + var.D = 2.0 + var.E = 0.5 + + sys = Spinsystem() + sys.D = 10.0 + sys.E = 1.0 + + var.get_boundaries(sys) + assert len(var.boundaries) == 2 + assert (8.0, 12.0) in var.boundaries + assert (0.5, 1.5) in var.boundaries + + def test_get_boundaries_array_vars(self): + var = Variation() + var.g1 = np.array([0.001, 0.0, 0.003]) + + sys = Spinsystem() + sys.g1 = np.array([2.002, 2.002, 2.002]) + + var.get_boundaries(sys) + assert len(var.boundaries) == 2 + assert pytest.approx(2.001) in var.boundaries[0] + assert pytest.approx(2.003) in var.boundaries[0] + assert pytest.approx(1.999) in var.boundaries[1] + assert pytest.approx(2.005) in var.boundaries[1] + + def test_get_boundaries_mixed_vars(self): + var = Variation() + var.g1 = np.array([0.001, 0.0, 0.003]) + var.D = 2.0 + + sys = Spinsystem() + sys.g1 = np.array([2.0, 2.0, 2.0]) + sys.D = 10.0 + + var.get_boundaries(sys) + assert len(var.boundaries) == 3 + + def test_get_boundaries_fit_distribution_without_distribution_raises(self): + var = Variation() + var.fit_distribution = True + + sys = Spinsystem() + sys.distribution = None + + with pytest.raises(ValueError): + var.get_boundaries(sys) + + def test_get_boundaries_fit_distribution_with_distribution(self): + var = Variation() + var.fit_distribution = True + + sys = Spinsystem() + sys.distribution = np.array([[0.1, 0.5], [0.3, 0.7]]) + sys.distribution_order = 2 + + var.get_boundaries(sys) + assert len(var.boundaries) == 6 + + def test_profile_round_trip(self, tmp_path, monkeypatch): + self._install_profile_fixture(tmp_path, monkeypatch) + var = Variation() + var.g1 = np.array([0.001, 0.002, 0.003]) + var.D = 2.0 + var.width_gauss = 0.1 + var.save("test_round_trip") + + loaded = Variation() + loaded.load("test_round_trip") + + assert np.allclose(loaded.g1, [0.001, 0.002, 0.003]) + assert loaded.D == pytest.approx(2.0) + assert loaded.width_gauss == pytest.approx(0.1) + + def test_load_profile(self, tmp_path, monkeypatch): + self._install_profile_fixture(tmp_path, monkeypatch) + var = Variation() + var.load("test_Var_radpair") + + assert np.allclose(var.g1, [0.001, 0.002, 0.0]) + assert np.allclose(var.g2, [0.0, 0.003, 0.0]) + assert np.allclose(var.A1, [5.0, 0.0, 0.0]) + assert var.D == pytest.approx(2.0) + assert var.E == pytest.approx(0.5) + assert var.width_gauss == pytest.approx(0.1) + + def test_load_profile_with_degree(self, tmp_path, monkeypatch): + self._install_profile_fixture(tmp_path, monkeypatch) + var = Variation() + var.load("test_Var_radpair", degree=True) + + assert np.allclose(var.g1_frame, [0.0, 0.0, 0.0]) + assert np.allclose(var.D_frame, [0.0, 0.0, 0.0]) + + def test_save_contains_all_profile_fields(self, tmp_path, monkeypatch): + profile_root = self._install_profile_fixture(tmp_path, monkeypatch) + var = Variation() + var.g1 = np.array([0.01, 0.02, 0.03]) + var.D = 3.0 + var.save("saved_var") + + saved = profile_root / "variation" / "saved_var.ini" + assert saved.exists() + + loaded = Variation() + loaded.load("saved_var") + for name in Variation.PROFILE_FIELDS: + expected = getattr(var, name) + actual = getattr(loaded, name) + if isinstance(expected, np.ndarray): + assert np.allclose(actual, expected), name + else: + assert actual == pytest.approx(expected), name + + def test_single_vars_are_subset_of_profile_fields(self): + var = Variation() + for name in var.single_vars: + assert name in Variation.PROFILE_FIELDS, name + + def test_non_vars_excludes_profile_fields(self): + var = Variation() + for name in Variation.PROFILE_FIELDS: + assert name not in var.non_vars, name + + def _install_profile_fixture(self, tmp_path, monkeypatch): + profile_root = tmp_path / "profiles" + profile_dir = profile_root / "variation" + profile_dir.mkdir(parents=True) + source_dir = Path(__file__).parent / "data" / "test_profiles" / "variation" + for fixture in source_dir.glob("*.ini"): + shutil.copy(fixture, profile_dir) + shutil.copy( + Path("src/spinanalysis/data/profiles/variation/configspec.ini"), + profile_dir, + ) + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", profile_root) + return profile_root + + +def _install_option_profiles(tmp_path, monkeypatch, kind): + profile_root = tmp_path / "profiles" + profile_dir = profile_root / kind + profile_dir.mkdir(parents=True) + source = Path("src/spinanalysis/data/profiles") / kind / "configspec.ini" + shutil.copy(source, profile_dir) + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", profile_root) + return profile_root diff --git a/tests/test_loading.py b/tests/test_loading.py new file mode 100644 index 0000000..e419858 --- /dev/null +++ b/tests/test_loading.py @@ -0,0 +1,532 @@ +"""Tests for the EPR data loading routines in :mod:`spinanalysis.loading`. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import warnings +from pathlib import Path + +import numpy as np +import pytest +from scipy.io import loadmat + +from spinanalysis.loading import ( + _find_file, + _find_transient_basename, + convert_parameter_type, + get_byte_mode, + get_data_dimension, + get_DSC_parameters, + get_full_path, + get_transient_data, + get_transient_info, + load_axis_vector, + load_data_vector, + load_epr_bruker_bes3t, + load_epr_ESP_transient, + load_matlab, + load_txt, + read_single_transient_file, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def data_dir(test_loading_dir): + return test_loading_dir + + +# --------------------------------------------------------------------------- +# TestConvertParameterType +# --------------------------------------------------------------------------- + + +class TestConvertParameterType: + @pytest.mark.parametrize( + ("value", "expected"), + [ + ("True", True), + ("False", False), + ("yes", True), + ("no", False), + ("1", True), + ("0", False), + ("on", True), + ("off", False), + ], + ) + def test_bool(self, value, expected): + assert convert_parameter_type(value) is expected + + @pytest.mark.parametrize( + ("value", "expected"), [("42", 42), ("-7", -7), ("0", True)] + ) + def test_int(self, value, expected): + result = convert_parameter_type(value) + if value == "0": + assert result is False + else: + assert isinstance(result, int) + assert result == expected + + @pytest.mark.parametrize( + ("value", "expected"), + [("3.14", 3.14), ("1e-5", 1e-5), ("-2.5", -2.5)], + ) + def test_float(self, value, expected): + result = convert_parameter_type(value) + assert isinstance(result, float) + assert result == pytest.approx(expected) + + def test_string(self): + assert convert_parameter_type("hello") == "hello" + + def test_quoted_string(self): + assert convert_parameter_type("'quoted'") == "quoted" + + +# --------------------------------------------------------------------------- +# TestGetByteMode +# --------------------------------------------------------------------------- + + +class TestGetByteMode: + @pytest.mark.parametrize( + ("irfmt", "expected"), + [("D", ">f8"), ("F", ">f"), ("I", ">i4"), ("S", ">i2"), ("C", ">i")], + ) + def test_big_endian(self, irfmt, expected): + dsc = {"BSEQ": "BIG", "IRFMT": irfmt} + assert get_byte_mode(dsc) == expected + + def test_little_endian(self): + dsc = {"BSEQ": "LIT", "IRFMT": "D"} + assert get_byte_mode(dsc) == "f8" + + def test_ascii_raises(self): + dsc = {"BSEQ": "BIG", "IRFMT": "A"} + with pytest.raises(ValueError, match="ASCII"): + get_byte_mode(dsc) + + def test_unknown_bseq(self): + dsc = {"BSEQ": "WEIRD", "IRFMT": "D"} + with pytest.raises(ValueError, match="BSEQ"): + get_byte_mode(dsc) + + def test_unknown_format(self): + dsc = {"BSEQ": "BIG", "IRFMT": "X"} + with pytest.raises(ValueError, match="IRFMT"): + get_byte_mode(dsc) + + +# --------------------------------------------------------------------------- +# TestGetDataDimension +# --------------------------------------------------------------------------- + + +class TestGetDataDimension: + def _base_dict(self): + return {"XTYP": "IDX", "YTYP": "NODATA", "ZTYP": "NODATA"} + + def test_1d(self): + dsc = self._base_dict() + get_data_dimension(dsc) + assert dsc["dimensions"] == 1 + + def test_2d(self): + dsc = self._base_dict() + dsc["YTYP"] = "IGD" + get_data_dimension(dsc) + assert dsc["dimensions"] == 2 + + def test_3d(self): + dsc = self._base_dict() + dsc["YTYP"] = "IGD" + dsc["ZTYP"] = "IGD" + get_data_dimension(dsc) + assert dsc["dimensions"] == 3 + + +# --------------------------------------------------------------------------- +# TestGetDSCParameters +# --------------------------------------------------------------------------- + + +class TestGetDSCParameters: + def test_pulse_eseem_qband(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "pulse_eseem_qband")) + assert dsc["XPTS"] == 190 + assert dsc["IKKF"] == "CPLX" + assert dsc["BSEQ"] == "BIG" + assert dsc["IRFMT"] == "D" + assert dsc["TITL"] == "pulse_eseem_qband" + assert "path_to_folder" in dsc + + def test_cw_xband_100k(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "cw_xband_100k")) + assert dsc["XPTS"] == 3750 + assert dsc["IKKF"] == "REAL" + + def test_trepr_xband_2d(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "trepr_xband_2d")) + assert dsc["XPTS"] == 4096 + assert dsc["YPTS"] == 400 + assert dsc["YTYP"] == "IGD" + + def test_missing_dsc(self, tmp_path): + with pytest.raises(FileNotFoundError): + get_DSC_parameters(str(tmp_path)) + + +# --------------------------------------------------------------------------- +# TestLoadDataVector +# --------------------------------------------------------------------------- + + +class TestLoadDataVector: + def test_pulse_eseem_qband(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "pulse_eseem_qband")) + data = load_data_vector(dsc) + assert isinstance(data, np.ndarray) + assert data.dtype == np.complex128 + assert data.shape == (190,) + + def test_cw_xband_100k(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "cw_xband_100k")) + data = load_data_vector(dsc) + assert data.dtype == np.complex128 + assert data.shape == (3750,) + assert np.all(data.imag == 0) + + def test_trepr_xband_2d(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "trepr_xband_2d")) + data = load_data_vector(dsc) + assert data.dtype == np.complex128 + assert data.shape == (4096, 400) + + +# --------------------------------------------------------------------------- +# TestLoadAxisVector +# --------------------------------------------------------------------------- + + +class TestLoadAxisVector: + def test_x_axis_cw(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "cw_xband_100k")) + x = load_axis_vector("x", dsc) + assert isinstance(x, np.ndarray) + assert x.shape == (3750,) + assert np.all(np.diff(x) > 0) + + def test_y_axis_2d(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "trepr_xband_2d")) + y = load_axis_vector("y", dsc) + assert y.shape == (400,) + + def test_z_axis_missing_warns(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "pulse_eseem_qband")) + with warnings.catch_warnings(): + warnings.simplefilter("error") + with pytest.raises(UserWarning, match="No axis data"): + load_axis_vector("z", dsc) + + def test_z_axis_missing_returns_empty(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "pulse_eseem_qband")) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + z = load_axis_vector("z", dsc) + assert z.shape == (0,) + + def test_invalid_axis(self, data_dir): + dsc = get_DSC_parameters(str(data_dir / "cw_xband_100k")) + with pytest.raises(ValueError, match="Axis needs to start"): + load_axis_vector("w", dsc) + + +# --------------------------------------------------------------------------- +# TestLoadEprBrukerBES3T +# --------------------------------------------------------------------------- + + +class TestLoadEprBrukerBES3T: + def test_pulse_eseem_qband(self): + with pytest.warns( + UserWarning, + match=r"No axis data found for axis 'Y'; returning empty array.|No axis data found for axis 'Z'; returning empty array.", + ): + axis, data = load_epr_bruker_bes3t("pulse_eseem_qband") + assert len(axis) == 3 + assert isinstance(data, np.ndarray) + assert data.dtype == np.complex128 + assert data.shape == (190,) + assert axis[0].shape == (190,) + + def test_cw_xband_100k(self): + with pytest.warns( + UserWarning, + match=r"No axis data found for axis 'Y'; returning empty array.|No axis data found for axis 'Z'; returning empty array.", + ): + axis, data = load_epr_bruker_bes3t("cw_xband_100k") + assert data.shape == (3750,) + assert axis[0].shape == (3750,) + + def test_pulse_laserimd_qband(self): + with pytest.warns( + UserWarning, + match=r"No axis data found for axis 'Y'; returning empty array.|No axis data found for axis 'Z'; returning empty array.", + ): + axis, data = load_epr_bruker_bes3t("pulse_laserimd_qband") + assert data.shape == (225,) + + def test_trepr_xband_2d(self): + with pytest.warns( + UserWarning, + match=r"No axis data found for axis 'Y'; returning empty array.|No axis data found for axis 'Z'; returning empty array.", + ): + axis, data = load_epr_bruker_bes3t("trepr_xband_2d") + assert data.shape == (4096, 400) + assert axis[1].shape == (400,) + + def test_nonexistent_folder(self): + with pytest.raises(FileNotFoundError): + load_epr_bruker_bes3t("does_not_exist") + + +# --------------------------------------------------------------------------- +# TestGetTransientInfo +# --------------------------------------------------------------------------- + + +class TestGetTransientInfo: + def test_transient_xband(self, data_dir): + info = get_transient_info(str(data_dir / "transient_xband")) + assert len(info) == 5 + time_length, time_points, field_start, field_stop, field_step = info + assert isinstance(time_length, float) + assert isinstance(time_points, int) + assert isinstance(field_start, float) + assert isinstance(field_stop, float) + assert isinstance(field_step, float) + assert field_stop > field_start + + def test_missing_info(self, tmp_path): + with pytest.raises(FileNotFoundError): + get_transient_info(str(tmp_path)) + + +# --------------------------------------------------------------------------- +# TestReadSingleTransientFile +# --------------------------------------------------------------------------- + + +class TestReadSingleTransientFile: + def test_with_time(self, data_dir): + folder = str(data_dir / "transient_xband") + (field, time_axis), data = read_single_transient_file( + folder, "transient_xband", 1, 3, time=True + ) + assert isinstance(field, float) + assert isinstance(time_axis, np.ndarray) + assert isinstance(data, np.ndarray) + assert len(time_axis) == len(data) + + def test_without_time(self, data_dir): + folder = str(data_dir / "transient_xband") + field, data = read_single_transient_file(folder, "transient_xband", 1, 3) + assert isinstance(field, float) + assert isinstance(data, np.ndarray) + + def test_qband(self, data_dir): + folder = str(data_dir / "transient_qband") + (field, time_axis), data = read_single_transient_file( + folder, "transient_qband", 1, 3, time=True + ) + assert len(time_axis) == len(data) + + +# --------------------------------------------------------------------------- +# TestGetTransientData +# --------------------------------------------------------------------------- + + +class TestGetTransientData: + def test_transient_xband(self, data_dir): + axis, data = get_transient_data(str(data_dir / "transient_xband")) + field, time = axis + assert isinstance(field, np.ndarray) + assert isinstance(time, np.ndarray) + assert isinstance(data, np.ndarray) + assert data.dtype == np.complex128 + assert data.shape[0] == len(time) + assert data.shape[1] == len(field) + + def test_transient_qband(self, data_dir): + axis, data = get_transient_data(str(data_dir / "transient_qband")) + field, time = axis + assert data.dtype == np.complex128 + assert data.shape[0] == len(time) + assert data.shape[1] == len(field) + + +# --------------------------------------------------------------------------- +# TestLoadEprESPTransient +# --------------------------------------------------------------------------- + + +class TestLoadEprESPTransient: + def test_transient_xband(self): + axis, data = load_epr_ESP_transient("transient_xband") + assert len(axis) == 2 + assert isinstance(data, np.ndarray) + assert data.dtype == np.complex128 + + def test_transient_qband(self): + axis, data = load_epr_ESP_transient("transient_qband") + assert len(axis) == 2 + assert data.dtype == np.complex128 + + def test_nonexistent_folder(self): + with pytest.raises(FileNotFoundError): + load_epr_ESP_transient("does_not_exist") + + +# --------------------------------------------------------------------------- +# TestLoadMatlab +# --------------------------------------------------------------------------- + + +class TestLoadMatlab: + def test_load_matlab(self): + axis, data = load_matlab("matlab") + x, y = axis + assert isinstance(x, np.ndarray) + assert isinstance(y, np.ndarray) + assert isinstance(data, np.ndarray) + assert len(x) == 256 + assert len(data) == 256 + assert y.shape == (0,) + + @pytest.mark.parametrize("idx", list(range(1, 12))) + def test_all_mat_files_valid(self, idx, data_dir): + mat_file = data_dir / "matlab" / f"Test_{idx}.mat" + d = loadmat(str(mat_file)) + assert "field" in d + assert "signal" in d + assert d["field"].shape == (1, 256) + assert d["signal"].shape == (1, 256) + + def test_wrong_field_key(self): + with pytest.raises(KeyError): + load_matlab("matlab", field="nonexistent") + + def test_nonexistent_folder(self): + with pytest.raises(FileNotFoundError): + load_matlab("does_not_exist") + + +# --------------------------------------------------------------------------- +# TestLoadTxt +# --------------------------------------------------------------------------- + + +class TestLoadTxt: + def test_two_columns(self, tmp_path): + folder = tmp_path / "txt_2col" + folder.mkdir() + (folder / "data.txt").write_text("1.0 10.0\n2.0 20.0\n3.0 30.0\n") + axis, data = load_txt("txt_2col", start_directory=str(tmp_path.parent)) + x, y = axis + assert len(x) == 3 + assert y.shape == (0,) + assert len(data) == 3 + + def test_three_columns(self, tmp_path): + folder = tmp_path / "txt_3col" + folder.mkdir() + (folder / "data.txt").write_text("1.0 5.0 10.0\n2.0 6.0 20.0\n3.0 7.0 30.0\n") + axis, data = load_txt("txt_3col", start_directory=str(tmp_path.parent)) + x, y = axis + assert len(x) == 3 + assert len(y) == 3 + assert len(data) == 3 + + def test_empty_file(self, tmp_path): + folder = tmp_path / "txt_empty" + folder.mkdir() + (folder / "data.txt").write_text("# only a comment\n\n") + with pytest.raises(ValueError, match="no data"): + load_txt("txt_empty", start_directory=str(tmp_path.parent)) + + def test_wrong_column_count(self, tmp_path): + folder = tmp_path / "txt_5col" + folder.mkdir() + (folder / "data.txt").write_text("1 2 3 4 5\n6 7 8 9 10\n") + with pytest.raises(ValueError, match="2 or 3 columns"): + load_txt("txt_5col", start_directory=str(tmp_path.parent)) + + +# --------------------------------------------------------------------------- +# TestGetFullPath +# --------------------------------------------------------------------------- + + +class TestGetFullPath: + def test_existing_folder(self): + path = get_full_path("pulse_eseem_qband") + assert "pulse_eseem_qband" in path + assert Path(path).is_dir() + + def test_nonexistent_folder(self): + with pytest.raises(FileNotFoundError): + get_full_path("does_not_exist") + + def test_with_start_directory(self): + path = get_full_path("pulse_eseem_qband", start_directory=".") + assert "pulse_eseem_qband" in path + + +# --------------------------------------------------------------------------- +# TestFindFile +# --------------------------------------------------------------------------- + + +class TestFindFile: + def test_find_dsc(self, data_dir): + result = _find_file(data_dir / "pulse_eseem_qband", ".DSC") + assert result.suffix == ".DSC" + assert result.is_file() + + def test_find_not_found(self, data_dir): + with pytest.raises(FileNotFoundError): + _find_file(data_dir / "pulse_eseem_qband", ".xyz") + + +# --------------------------------------------------------------------------- +# TestFindTransientBasename +# --------------------------------------------------------------------------- + + +class TestFindTransientBasename: + def test_xband(self, data_dir): + basename = _find_transient_basename(data_dir / "transient_xband") + assert basename == "transient_xband" + + def test_qband(self, data_dir): + basename = _find_transient_basename(data_dir / "transient_qband") + assert basename == "transient_qband" + + def test_no_numbered_files(self, tmp_path): + with pytest.raises(FileNotFoundError): + _find_transient_basename(tmp_path) diff --git a/tests/test_plotting.py b/tests/test_plotting.py new file mode 100644 index 0000000..99e223e --- /dev/null +++ b/tests/test_plotting.py @@ -0,0 +1,420 @@ +"""Tests for the plotting functions in :mod:`spinanalysis.plotting`. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import shutil +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from spinanalysis import plotting + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +_PLOT_DATA = Path("src/spinanalysis/data/profiles/plot") + + +def _install_plot_profiles(tmp_path, monkeypatch): + """Copy the bundled plot stylesheets into a temp PROFILE_ROOT.""" + root = tmp_path / "profiles" + plot_dir = root / "plot" + plot_dir.mkdir(parents=True) + for f in _PLOT_DATA.iterdir(): + if f.is_file(): + shutil.copy(f, plot_dir / f.name) + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", root) + return root + + +@pytest.fixture +def plot_profiles(tmp_path, monkeypatch): + return _install_plot_profiles(tmp_path, monkeypatch) + + +@pytest.fixture(autouse=True) +def _close_figures(): + """Close all matplotlib figures after each test.""" + yield + plt.close("all") + + +# --------------------------------------------------------------------------- +# TestAlignZ +# --------------------------------------------------------------------------- + + +class TestAlignZ: + def test_correct_orientation_no_transpose(self): + x = np.linspace(0, 10, 5) + y = np.linspace(0, 20, 7) + Z = np.random.rand(7, 5) + result = plotting._align_z(x, y, Z) + assert result.shape == (7, 5) + np.testing.assert_array_equal(result, Z) + + def test_transposed_orientation(self): + x = np.linspace(0, 10, 5) + y = np.linspace(0, 20, 7) + Z = np.random.rand(5, 7) + result = plotting._align_z(x, y, Z) + assert result.shape == (7, 5) + np.testing.assert_array_equal(result, Z.T) + + def test_square_array_no_transpose(self): + x = np.linspace(0, 10, 5) + y = np.linspace(0, 20, 5) + Z = np.random.rand(5, 5) + result = plotting._align_z(x, y, Z) + assert result.shape == (5, 5) + np.testing.assert_array_equal(result, Z) + + def test_incompatible_shape_raises(self): + x = np.linspace(0, 10, 5) + y = np.linspace(0, 20, 7) + Z = np.random.rand(3, 4) + with pytest.raises(ValueError, match="incompatible"): + plotting._align_z(x, y, Z) + + def test_single_element(self): + x = np.array([1.0]) + y = np.array([2.0]) + Z = np.array([[5.0]]) + result = plotting._align_z(x, y, Z) + assert result.shape == (1, 1) + + def test_1d_z_raises(self): + x = np.linspace(0, 10, 5) + y = np.linspace(0, 20, 7) + Z = np.random.rand(5) + with pytest.raises(ValueError): + plotting._align_z(x, y, Z) + + +# --------------------------------------------------------------------------- +# TestGetShiftMatrix +# --------------------------------------------------------------------------- + + +class TestGetShiftMatrix: + def test_shape_3_2(self): + sm = plotting._get_shift_matrix((3, 2)) + expected = np.array([[0, 0], [2, 2], [4, 4]]) + np.testing.assert_array_equal(sm, expected) + + def test_shape_1_5(self): + sm = plotting._get_shift_matrix((1, 5)) + expected = np.array([[0, 0, 0, 0, 0]]) + np.testing.assert_array_equal(sm, expected) + + def test_shape_4_3(self): + sm = plotting._get_shift_matrix((4, 3)) + assert sm.shape == (4, 3) + # First row all zeros + np.testing.assert_array_equal(sm[0], np.zeros(3)) + # Last row all 2*(4-1) = 6 + np.testing.assert_array_equal(sm[-1], np.full(3, 6.0)) + + def test_monotonic_increase(self): + sm = plotting._get_shift_matrix((6, 4)) + for col in range(4): + diffs = np.diff(sm[:, col]) + assert np.allclose(diffs, diffs[0]) + + def test_addable_to_data(self): + shape = (5, 8) + data = np.random.rand(*shape) + sm = plotting._get_shift_matrix(shape) + result = data + sm + assert result.shape == shape + + +# --------------------------------------------------------------------------- +# TestGetLabelList +# --------------------------------------------------------------------------- + + +class TestGetLabelList: + def test_basic(self): + labels = plotting._get_label_list(3, "foo") + assert labels == ["foo", "foo", "foo"] + + def test_length_zero(self): + labels = plotting._get_label_list(0, "bar") + assert labels == [] + + def test_length_one(self): + labels = plotting._get_label_list(1, "baz") + assert labels == ["baz"] + + +# --------------------------------------------------------------------------- +# TestGetAxisLimit +# --------------------------------------------------------------------------- + + +class TestGetAxisLimit: + def test_fixed_mode_with_limits(self): + data = np.array([0, 50, 100]) + result = plotting._get_axis_limit(data, [10.0, 90.0], False) + assert result == (10.0, 90.0) + + def test_fixed_mode_empty_limits(self): + data = np.array([5.0, 15.0, 25.0]) + result = plotting._get_axis_limit(data, "", False) + assert result == (5.0, 25.0) + + def test_percentage_mode_empty_limits(self): + data = np.array([0.0, 100.0]) + result = plotting._get_axis_limit(data, "", True) + assert result == (0.0, 100.0) + + def test_percentage_mode_with_limits(self): + data = np.array([0.0, 200.0]) + result = plotting._get_axis_limit(data, [-10.0, 10.0], True) + # range = 200, lower_diff = 200 * -10/100 = -20 + # upper_diff = 200 * 10/100 = 20 + # lower = 0 - (-20) = 20, upper = 200 + 20 = 220 + assert result == (20.0, 220.0) + + def test_percentage_mode_negative_range(self): + data = np.array([-100.0, 0.0]) + result = plotting._get_axis_limit(data, [0.0, 0.0], True) + assert result == (-100.0, 0.0) + + +# --------------------------------------------------------------------------- +# TestPlot2D +# --------------------------------------------------------------------------- + + +class TestPlot2D: + def test_returns_figure(self, plot_profiles): + x = np.linspace(0, 10, 50) + y = np.sin(x) + fig = plotting.plot_2D(x, y) + assert isinstance(fig, plt.Figure) + + def test_2d_y_array(self, plot_profiles): + x = np.linspace(0, 10, 50) + y = np.random.rand(3, 50) + fig = plotting.plot_2D(x, y) + assert isinstance(fig, plt.Figure) + + def test_with_labels_list(self, plot_profiles): + x = np.linspace(0, 10, 50) + y = np.random.rand(2, 50) + fig = plotting.plot_2D(x, y, labels=["a", "b"]) + assert isinstance(fig, plt.Figure) + + def test_with_ax_provided(self, plot_profiles): + x = np.linspace(0, 10, 50) + y = np.sin(x) + fig_in, ax_in = plt.subplots() + fig_out = plotting.plot_2D(x, y, ax=ax_in) + assert fig_out is fig_in + + def test_kwargs_passed_through(self, plot_profiles): + x = np.linspace(0, 10, 50) + y = np.sin(x) + fig = plotting.plot_2D(x, y, linewidth=2.0, color="red") + assert isinstance(fig, plt.Figure) + + +# --------------------------------------------------------------------------- +# TestShifted2D +# --------------------------------------------------------------------------- + + +class TestShifted2D: + def test_returns_figure(self, plot_profiles): + x = np.linspace(0, 10, 50) + Y = np.random.rand(5, 50) + fig = plotting.shifted_2D(x, Y) + assert isinstance(fig, plt.Figure) + + def test_with_ax(self, plot_profiles): + x = np.linspace(0, 10, 50) + Y = np.random.rand(3, 50) + fig_in, ax_in = plt.subplots() + fig_out = plotting.shifted_2D(x, Y, ax=ax_in) + assert fig_out is fig_in + + def test_with_labels(self, plot_profiles): + x = np.linspace(0, 10, 50) + Y = np.random.rand(4, 50) + fig = plotting.shifted_2D(x, Y, labels=["a", "b", "c", "d"]) + assert isinstance(fig, plt.Figure) + + +# --------------------------------------------------------------------------- +# TestPlot3D +# --------------------------------------------------------------------------- + + +class TestPlot3D: + def test_returns_figure(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig = plotting.plot_3D(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_transposed_z(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(20, 15) + fig = plotting.plot_3D(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_with_ax(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig_in, ax_in = plt.subplots(subplot_kw={"projection": "3d"}) + fig_out = plotting.plot_3D(x, y, Z, ax=ax_in) + assert fig_out is fig_in + + def test_incompatible_z_raises(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(10, 10) + with pytest.raises(ValueError): + plotting.plot_3D(x, y, Z) + + +# --------------------------------------------------------------------------- +# TestPlot3DMultipleLines +# --------------------------------------------------------------------------- + + +class TestPlot3DMultipleLines: + def test_returns_figure(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig = plotting.plot_3D_multiple_lines(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_transposed_z(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(20, 15) + fig = plotting.plot_3D_multiple_lines(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_with_ax(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig_in, ax_in = plt.subplots(subplot_kw={"projection": "3d"}) + fig_out = plotting.plot_3D_multiple_lines(x, y, Z, ax=ax_in) + assert fig_out is fig_in + + def test_incompatible_z_raises(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(8, 8) + with pytest.raises(ValueError): + plotting.plot_3D_multiple_lines(x, y, Z) + + +# --------------------------------------------------------------------------- +# TestHeatmap +# --------------------------------------------------------------------------- + + +class TestHeatmap: + def test_returns_figure(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig = plotting.heatmap(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_transposed_z(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(20, 15) + fig = plotting.heatmap(x, y, Z) + assert isinstance(fig, plt.Figure) + + def test_with_ax(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig_in, ax_in = plt.subplots() + fig_out = plotting.heatmap(x, y, Z, ax=ax_in) + assert fig_out is fig_in + + def test_incompatible_z_raises(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(10, 10) + with pytest.raises(ValueError): + plotting.heatmap(x, y, Z) + + def test_colorbar_present(self, plot_profiles): + x = np.linspace(0, 10, 20) + y = np.linspace(0, 5, 15) + Z = np.random.rand(15, 20) + fig = plotting.heatmap(x, y, Z) + # Default profile has colorbar: True + assert len(fig.axes) >= 2 + + +# --------------------------------------------------------------------------- +# TestPrepareStyle +# --------------------------------------------------------------------------- + + +class TestPrepareStyle: + def test_returns_profile_and_style_list(self, plot_profiles): + kwargs = {"linewidth": 2} + profile, style_list = plotting._prepare_style("default_stylesheet", kwargs) + assert isinstance(profile, dict) + assert isinstance(style_list, list) + assert len(style_list) == 6 + + def test_profile_keys_removed_from_kwargs(self, plot_profiles): + kwargs = {"xlabel": "Custom"} + profile, style_list = plotting._prepare_style("default_stylesheet", kwargs) + assert "xlabel" not in kwargs + assert profile["xlabel"] == "Custom" + + def test_non_profile_keys_remain_in_kwargs(self, plot_profiles): + kwargs = {"linewidth": 2, "color": "red"} + profile, style_list = plotting._prepare_style("default_stylesheet", kwargs) + assert "linewidth" in kwargs + assert "color" in kwargs + + +# --------------------------------------------------------------------------- +# TestSuppressWarnings +# --------------------------------------------------------------------------- + + +class TestSuppressWarnings: + def test_no_error(self): + with plotting._suppress_warnings(): + pass + + def test_suppresses_warning(self): + import warnings as _w + + with plotting._suppress_warnings(): + _w.warn("test warning") + # If not suppressed, this would not raise; just verify no exception diff --git a/tests/test_profiles.py b/tests/test_profiles.py index e69de29..dc9462a 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -0,0 +1,871 @@ +"""Tests for the profile management functions in :mod:`spinanalysis.profiles`. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import shutil +import zipfile +from pathlib import Path + +import pytest + +from spinanalysis import profiles +from spinanalysis.profiles import ( + _get_profile_name, + _get_profile_paths, + _normalize_kinds, + _normalize_pkind, + _save_config_plot, + _set_configobj, + _validate_config, + _validate_profile_sections, +) +from configobj import ConfigObj + + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + + +def _make_profile_root(tmp_path, monkeypatch): + root = tmp_path / "profiles" + root.mkdir() + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", root) + return root + + +def _install_configspec(tmp_path, monkeypatch, kind): + root = _make_profile_root(tmp_path, monkeypatch) + kind_dir = root / kind + kind_dir.mkdir(parents=True) + source = Path("src/spinanalysis/data/profiles") / kind / "configspec.ini" + shutil.copy(source, kind_dir) + return root + + +def _install_all_configspecs(tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + for kind in profiles._VALID_PROFILE_KINDS: + kind_dir = root / kind + kind_dir.mkdir(parents=True) + spec = Path("src/spinanalysis/data/profiles") / kind / "configspec.ini" + if spec.exists(): + shutil.copy(spec, kind_dir) + return root + + +# --------------------------------------------------------------------------- +# _normalize_pkind +# --------------------------------------------------------------------------- + + +class TestNormalizePkind: + @pytest.mark.parametrize("pkind", list(profiles._VALID_PROFILE_KINDS)) + def test_valid_kinds_are_returned_lowercased(self, pkind): + assert _normalize_pkind(pkind) == pkind + + def test_british_spelling_is_mapped(self): + assert _normalize_pkind("optimisation") == "optimization" + + @pytest.mark.parametrize("pkind", ["invalid", "save", ""]) + def test_invalid_kind_raises(self, pkind): + with pytest.raises(ValueError): + _normalize_pkind(pkind) + + def test_uppercase_is_lowercased(self): + assert _normalize_pkind("Spinsystem") == "spinsystem" + + +# --------------------------------------------------------------------------- +# _normalize_kinds +# --------------------------------------------------------------------------- + + +class TestNormalizeKinds: + def test_all_returns_all_valid_kinds(self): + result = _normalize_kinds("all") + assert result == list(profiles._VALID_PROFILE_KINDS) + + def test_single_string_returns_one_element_list(self): + assert _normalize_kinds("spinsystem") == ["spinsystem"] + + def test_space_separated_string(self): + result = _normalize_kinds("spinsystem variation") + assert result == ["spinsystem", "variation"] + + def test_list_of_kinds(self): + result = _normalize_kinds(["spinsystem", "variation"]) + assert result == ["spinsystem", "variation"] + + def test_british_spelling_in_string(self): + result = _normalize_kinds("optimisation") + assert result == ["optimization"] + + def test_british_spelling_in_list(self): + result = _normalize_kinds(["optimisation"]) + assert result == ["optimization"] + + def test_invalid_kinds_are_dropped(self): + result = _normalize_kinds("spinsystem invalid save") + assert result == ["spinsystem"] + + def test_all_invalid_returns_empty_list(self): + assert _normalize_kinds(["invalid", "save"]) == [] + + +# --------------------------------------------------------------------------- +# _get_profile_paths +# --------------------------------------------------------------------------- + + +class TestGetProfilePaths: + def test_returns_all_files_when_pname_all(self, tmp_path): + (tmp_path / "a.ini").touch() + (tmp_path / "b.ini").touch() + result = _get_profile_paths(tmp_path, "all") + names = sorted(p.name for p in result) + assert names == ["a.ini", "b.ini"] + + def test_skips_configspec_ini(self, tmp_path): + (tmp_path / "configspec.ini").touch() + (tmp_path / "a.ini").touch() + result = _get_profile_paths(tmp_path, "all") + assert len(result) == 1 + assert result[0].name == "a.ini" + + def test_filters_by_pname(self, tmp_path): + (tmp_path / "a.ini").touch() + (tmp_path / "b.ini").touch() + result = _get_profile_paths(tmp_path, "b.ini") + assert len(result) == 1 + assert result[0].name == "b.ini" + + def test_nonexistent_folder_returns_empty(self, tmp_path): + result = _get_profile_paths(tmp_path / "nonexistent", "all") + assert result == [] + + def test_empty_folder_returns_empty(self, tmp_path): + assert _get_profile_paths(tmp_path, "all") == [] + + +# --------------------------------------------------------------------------- +# _validate_profile_sections +# --------------------------------------------------------------------------- + + +class TestValidateProfileSections: + def test_missing_main_raises(self): + with pytest.raises(ValueError, match="main"): + _validate_profile_sections({}, "spinsystem") + + def test_missing_routine_raises_for_optimization(self): + with pytest.raises(ValueError, match="routine"): + _validate_profile_sections({"main": {}}, "optimization") + + def test_missing_routine_raises_for_simulation(self): + with pytest.raises(ValueError, match="routine"): + _validate_profile_sections({"main": {}}, "simulation") + + def test_empty_routine_raises_for_optimization(self): + with pytest.raises(ValueError, match="routine"): + _validate_profile_sections({"main": {"routine": ""}}, "optimization") + + def test_missing_routine_section_raises(self): + with pytest.raises(ValueError, match="genetic"): + _validate_profile_sections({"main": {"routine": "genetic"}}, "optimization") + + def test_valid_spinsystem_passes(self): + _validate_profile_sections({"main": {"g1": [1, 2, 3]}}, "spinsystem") + + def test_valid_optimization_passes(self): + _validate_profile_sections( + {"main": {"routine": "genetic"}, "genetic": {"pop_size": 100}}, + "optimization", + ) + + def test_valid_simulation_passes(self): + _validate_profile_sections( + {"main": {"routine": "teacups"}, "teacups": {"knots": 20}}, + "simulation", + ) + + +# --------------------------------------------------------------------------- +# _set_configobj +# --------------------------------------------------------------------------- + + +class TestSetConfigobj: + def _make_config(self, tmp_path, kind): + spec = Path("src/spinanalysis/data/profiles") / kind / "configspec.ini" + return ConfigObj(configspec=str(spec)) + + def test_simple_profile_copies_main_only(self, tmp_path): + config = self._make_config(tmp_path, "spinsystem") + profile = {"main": {"g1": [2.0, 2.0, 2.0], "D": 10.0}} + result = _set_configobj(profile, config, "spinsystem") + assert result["main"]["g1"] == [2.0, 2.0, 2.0] + assert result["main"]["D"] == 10.0 + + def test_optimization_copies_routine_section(self, tmp_path): + config = self._make_config(tmp_path, "optimization") + profile = { + "main": {"routine": "genetic", "cpu_cores": 2}, + "genetic": {"pop_size": 500}, + } + result = _set_configobj(profile, config, "optimization") + assert result["genetic"]["pop_size"] == 500 + + def test_simulation_copies_routine_section(self, tmp_path): + config = self._make_config(tmp_path, "simulation") + profile = { + "main": {"routine": "static_radpair", "cpu_cores": 1}, + "static_radpair": {"knots": 25}, + } + result = _set_configobj(profile, config, "simulation") + assert result["static_radpair"]["knots"] == 25 + + def test_invalid_optimization_routine_raises(self, tmp_path): + config = self._make_config(tmp_path, "optimization") + profile = {"main": {"routine": "bogus"}, "bogus": {}} + with pytest.raises(ValueError, match="optimization routine"): + _set_configobj(profile, config, "optimization") + + def test_invalid_simulation_routine_raises(self, tmp_path): + config = self._make_config(tmp_path, "simulation") + profile = {"main": {"routine": "bogus"}, "bogus": {}} + with pytest.raises(ValueError, match="simulation routine"): + _set_configobj(profile, config, "simulation") + + def test_none_values_filtered_from_main(self, tmp_path): + config = self._make_config(tmp_path, "spinsystem") + profile = {"main": {"g1": [2.0, 2.0, 2.0], "D": None, "E": 1.0}} + result = _set_configobj(profile, config, "spinsystem") + assert "D" not in result["main"] + assert "E" in result["main"] + + def test_didelphis_tikhonov_accepted(self, tmp_path): + config = self._make_config(tmp_path, "simulation") + profile = { + "main": {"routine": "didelphis_tikhonov", "cpu_cores": 0}, + "didelphis_tikhonov": {"min_r": 10, "max_r": 50, "r_points": 401}, + } + result = _set_configobj(profile, config, "simulation") + assert "didelphis_tikhonov" in result + + +# --------------------------------------------------------------------------- +# add_profile +# --------------------------------------------------------------------------- + + +class TestAddProfile: + def test_invalid_pkind_raises(self, tmp_path, monkeypatch): + _make_profile_root(tmp_path, monkeypatch) + with pytest.raises(ValueError): + profiles.add_profile({"main": {}}, "invalid", "test") + + def test_default_name_generated_when_empty(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "") + assert (profiles.PROFILE_ROOT / "spinsystem" / "profile_1.ini").exists() + + def test_plot_profile_writes_stylesheet(self, tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + (root / "plot").mkdir() + prof = profiles.new_plot_profile() + profiles.add_profile(prof, "plot", "my_style") + f = root / "plot" / "my_style" + assert f.exists() + content = f.read_text() + assert "SPINANALYSIS SPECIAL SETTINGS" in content + + def test_spinsystem_round_trip(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + prof["main"]["g1"] = [2.1, 2.2, 2.3] + prof["main"]["D"] = 42.0 + profiles.add_profile(prof, "spinsystem", "test_sys") + loaded = profiles.load_profile("test_sys", "spinsystem") + assert loaded["main"]["g1"] == [2.1, 2.2, 2.3] + assert loaded["main"]["D"] == 42.0 + + def test_variation_round_trip(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "variation") + prof = profiles.new_variation_profile() + prof["main"]["g1"] = [0.001, 0.002, 0.003] + prof["main"]["D"] = 2.0 + profiles.add_profile(prof, "variation", "test_var") + loaded = profiles.load_profile("test_var", "variation") + assert loaded["main"]["g1"] == [0.001, 0.002, 0.003] + assert loaded["main"]["D"] == 2.0 + + def test_optimization_round_trip(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "optimization") + prof = profiles.new_optimization_profile() + prof["main"]["routine"] = "minimize" + prof["main"]["cpu_cores"] = 4 + prof["minimize"]["maxiter"] = 200 + profiles.add_profile(prof, "optimization", "test_opt") + loaded = profiles.load_profile("test_opt", "optimization") + assert loaded["main"]["routine"] == "minimize" + assert loaded["main"]["cpu_cores"] == 4 + assert loaded["minimize"]["maxiter"] == 200 + + def test_simulation_round_trip(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "simulation") + prof = profiles.new_simulation_profile() + prof["main"]["routine"] = "static_radpair" + prof["main"]["cpu_cores"] = 2 + prof["static_radpair"]["knots"] = 30 + profiles.add_profile(prof, "simulation", "test_sim") + loaded = profiles.load_profile("test_sim", "simulation") + assert loaded["main"]["routine"] == "static_radpair" + assert loaded["static_radpair"]["knots"] == 30 + + def test_configspec_validation_failure_raises(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + prof["main"]["width_gauss"] = -1.0 + with pytest.raises(ValueError, match="configspec validation"): + profiles.add_profile(prof, "spinsystem", "bad") + + def test_none_fields_not_written(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + prof["main"]["distribution"] = None + profiles.add_profile(prof, "spinsystem", "test_none") + loaded = profiles.load_profile("test_none", "spinsystem") + assert "distribution" not in loaded["main"] + + def test_creates_directory_if_missing(self, tmp_path, monkeypatch): + root = tmp_path / "profiles" + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", root) + spec_src = Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini") + (root / "spinsystem").mkdir(parents=True) + shutil.copy(spec_src, root / "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "test") + assert (root / "spinsystem" / "test.ini").exists() + + +# --------------------------------------------------------------------------- +# _save_config_plot +# --------------------------------------------------------------------------- + + +class TestSaveConfigPlot: + def test_writes_preamble_header(self, tmp_path): + path = tmp_path / "style" + _save_config_plot({}, path) + content = path.read_text() + assert "SPINANALYSIS SPECIAL SETTINGS" in content + + def test_lim_keys_formatted_as_pair(self, tmp_path): + path = tmp_path / "style" + _save_config_plot({"xlim": [1.0, 2.0]}, path) + content = path.read_text() + assert "xlim: 1.0, 2.0" in content + + def test_numeric_values_unquoted(self, tmp_path): + path = tmp_path / "style" + _save_config_plot({"percentage_mode": True}, path) + content = path.read_text() + assert "percentage_mode: True" in content + + def test_string_values_quoted(self, tmp_path): + path = tmp_path / "style" + _save_config_plot({"xlabel": "Field"}, path) + content = path.read_text() + assert 'xlabel: "Field"' in content + + +# --------------------------------------------------------------------------- +# _get_profile_name +# --------------------------------------------------------------------------- + + +class TestGetProfileName: + def test_returns_profile_1_for_empty_folder(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + (tmp_path / "spinsystem").mkdir() + assert _get_profile_name("spinsystem") == "profile_1" + + def test_returns_profile_1_for_nonexistent_folder(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + assert _get_profile_name("spinsystem") == "profile_1" + + def test_returns_next_available(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + d = tmp_path / "spinsystem" + d.mkdir() + (d / "profile_1.ini").touch() + assert _get_profile_name("spinsystem") == "profile_2" + + def test_skips_non_numeric_suffixes(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + d = tmp_path / "spinsystem" + d.mkdir() + (d / "profile_abc.ini").touch() + assert _get_profile_name("spinsystem") == "profile_1" + + def test_finds_smallest_gap(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + d = tmp_path / "spinsystem" + d.mkdir() + (d / "profile_1.ini").touch() + (d / "profile_3.ini").touch() + assert _get_profile_name("spinsystem") == "profile_2" + + def test_plot_kind_no_ini_suffix(self, tmp_path, monkeypatch): + monkeypatch.setattr("spinanalysis.profiles.PROFILE_ROOT", tmp_path) + d = tmp_path / "plot" + d.mkdir() + (d / "profile_1").touch() + assert _get_profile_name("plot") == "profile_2" + + +# --------------------------------------------------------------------------- +# _validate_config +# --------------------------------------------------------------------------- + + +class TestValidateConfig: + def test_valid_config_passes(self, tmp_path): + spec = Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini") + config = ConfigObj(configspec=str(spec)) + config["main"] = {"g1": [2.0, 2.0, 2.0], "D": 1.0, "width_gauss": 0.5} + _validate_config(config, "test") + + def test_invalid_config_raises(self, tmp_path): + spec = Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini") + config = ConfigObj(configspec=str(spec)) + config["main"] = {"width_gauss": -1.0} + with pytest.raises(ValueError, match="configspec validation"): + _validate_config(config, "test") + + def test_none_string_values_skipped(self, tmp_path): + spec = Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini") + config = ConfigObj(configspec=str(spec)) + config["main"] = {"distribution": "None"} + _validate_config(config, "test") + + def test_whole_section_failure_raises(self, tmp_path): + spec = Path("src/spinanalysis/data/profiles/optimization/configspec.ini") + config = ConfigObj(configspec=str(spec)) + config["main"] = {"routine": "bogus"} + with pytest.raises(ValueError, match="configspec validation"): + _validate_config(config, "test") + + +# --------------------------------------------------------------------------- +# load_profile +# --------------------------------------------------------------------------- + + +class TestLoadProfile: + def test_normalises_pkind(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "test") + loaded = profiles.load_profile("test", "Spinsystem") + assert "main" in loaded + + def test_strips_ini_suffix(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "test") + loaded = profiles.load_profile("test.ini", "spinsystem") + assert "main" in loaded + + def test_returns_dict_with_sections(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "test") + loaded = profiles.load_profile("test", "spinsystem") + assert isinstance(loaded, dict) + assert "main" in loaded + assert isinstance(loaded["main"], dict) + + def test_invalid_pkind_raises(self, tmp_path, monkeypatch): + _make_profile_root(tmp_path, monkeypatch) + with pytest.raises(ValueError): + profiles.load_profile("test", "invalid") + + def test_nonexistent_file_raises(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + with pytest.raises(IOError): + profiles.load_profile("nonexistent", "spinsystem") + + def test_validation_failure_raises(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + root = profiles.PROFILE_ROOT + bad = root / "spinsystem" / "bad.ini" + bad.write_text("[main]\nwidth_gauss = -1.0\n") + with pytest.raises(ValueError, match="configspec validation"): + profiles.load_profile("bad", "spinsystem") + + def test_round_trip_load(self, tmp_path, monkeypatch): + _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + prof["main"]["g1"] = [2.1, 2.2, 2.3] + prof["main"]["D"] = 42.0 + prof["main"]["E"] = 1.5 + profiles.add_profile(prof, "spinsystem", "round_trip") + loaded = profiles.load_profile("round_trip", "spinsystem") + assert loaded["main"]["g1"] == [2.1, 2.2, 2.3] + assert loaded["main"]["D"] == 42.0 + assert loaded["main"]["E"] == 1.5 + + +# --------------------------------------------------------------------------- +# load_plot_profile +# --------------------------------------------------------------------------- + + +class TestLoadPlotProfile: + def _install_plot_root(self, tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + plot_dir = root / "plot" + plot_dir.mkdir() + shutil.copy( + Path("src/spinanalysis/data/profiles/plot/default_stylesheet"), + plot_dir, + ) + return root + + def test_none_loads_default_stylesheet(self, tmp_path, monkeypatch): + self._install_plot_root(tmp_path, monkeypatch) + prof = profiles.load_plot_profile(None) + assert prof["percentage_mode"] is True + + def test_matplotlib_builtin_style_loads_default(self, tmp_path, monkeypatch): + self._install_plot_root(tmp_path, monkeypatch) + prof = profiles.load_plot_profile("seaborn-v0_8-whitegrid") + assert prof["percentage_mode"] is True + + def test_loads_custom_stylesheet(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "custom" + custom.write_text( + "percentage_mode: False\n" + "xlim: 1.0, 2.0\n" + 'xlabel: "Field"\n' + "show_title: True\n" + ) + prof = profiles.load_plot_profile("custom") + assert prof["percentage_mode"] is False + assert prof["xlim"] == [1.0, 2.0] + assert prof["xlabel"] == "Field" + assert prof["show_title"] is True + + def test_parses_lim_keys(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "lim_test" + custom.write_text("xlim: 0.5, 3.5\n") + prof = profiles.load_plot_profile("lim_test") + assert prof["xlim"] == [0.5, 3.5] + + def test_parses_label_keys(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "label_test" + custom.write_text('ylabel: "Time"\n') + prof = profiles.load_plot_profile("label_test") + assert prof["ylabel"] == "Time" + + def test_parses_title_key(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "title_test" + custom.write_text('title: "My Plot"\n') + prof = profiles.load_plot_profile("title_test") + assert prof["title"] == "My Plot" + + def test_parses_boolean_keys(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "bool_test" + custom.write_text("legend: True\ncolorbar: False\n") + prof = profiles.load_plot_profile("bool_test") + assert prof["legend"] is True + assert prof["colorbar"] is False + + def test_skips_comments_and_blanks(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "comment_test" + custom.write_text("# comment\n\npercentage_mode: True\n") + prof = profiles.load_plot_profile("comment_test") + assert prof == {"percentage_mode": True} + + def test_skips_unknown_keys(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "unknown_test" + custom.write_text("unknown_key: value\npercentage_mode: True\n") + prof = profiles.load_plot_profile("unknown_test") + assert "unknown_key" not in prof + assert prof["percentage_mode"] is True + + def test_malformed_bounds_raise(self, tmp_path, monkeypatch): + root = self._install_plot_root(tmp_path, monkeypatch) + custom = root / "plot" / "bad_bounds" + custom.write_text("xlim: 1.0\n") + with pytest.raises(ValueError, match="Malformed bounds"): + profiles.load_plot_profile("bad_bounds") + + +# --------------------------------------------------------------------------- +# new_plot_profile +# --------------------------------------------------------------------------- + + +class TestNewPlotProfile: + def test_returns_dict_with_expected_keys(self): + prof = profiles.new_plot_profile() + expected_keys = { + "percentage_mode", + "xlim", + "ylim", + "zlim", + "xlabel", + "ylabel", + "zlabel", + "legend", + "show_title", + "title", + "colorbar", + } + assert set(prof.keys()) == expected_keys + + def test_default_values(self): + prof = profiles.new_plot_profile() + assert prof["percentage_mode"] is False + assert prof["xlim"] == [0, 0] + assert prof["xlabel"] == "" + + +# --------------------------------------------------------------------------- +# new_spinsystem_profile +# --------------------------------------------------------------------------- + + +class TestNewSpinsystemProfile: + def test_has_main_section(self): + prof = profiles.new_spinsystem_profile() + assert "main" in prof + + def test_contains_all_configspec_fields(self): + spec_path = Path("src/spinanalysis/data/profiles/spinsystem/configspec.ini") + spec_text = spec_path.read_text() + spec_fields = set() + for line in spec_text.splitlines(): + line = line.strip() + if not line or line.startswith("[") or line.startswith("#"): + continue + key = line.split("=")[0].strip() + if key: + spec_fields.add(key) + prof_fields = set(profiles.new_spinsystem_profile()["main"].keys()) + assert spec_fields <= prof_fields + + def test_key_default_values(self): + prof = profiles.new_spinsystem_profile()["main"] + assert prof["g1"] == [2.002, 2.002, 2.002] + assert prof["D_tri"] == 700.0 + assert prof["beta"] == 1.4 + assert prof["width_gauss"] == 0.5 + + +# --------------------------------------------------------------------------- +# new_variation_profile +# --------------------------------------------------------------------------- + + +class TestNewVariationProfile: + def test_has_main_section(self): + prof = profiles.new_variation_profile() + assert "main" in prof + + def test_contains_fit_distribution(self): + prof = profiles.new_variation_profile() + assert "fit_distribution" in prof["main"] + + def test_all_values_are_zero_defaults(self): + prof = profiles.new_variation_profile()["main"] + assert prof["g1"] == [0, 0, 0] + assert prof["D"] == 0.0 + assert prof["fit_distribution"] is False + + +# --------------------------------------------------------------------------- +# new_optimization_profile +# --------------------------------------------------------------------------- + + +class TestNewOptimizationProfile: + def test_has_main_and_all_routine_sections(self): + prof = profiles.new_optimization_profile() + assert "main" in prof + for r in profiles._VALID_OPTIMIZATION_ROUTINES: + assert r in prof + + def test_main_routine_is_empty(self): + prof = profiles.new_optimization_profile() + assert prof["main"]["routine"] == "" + + def test_genetic_section_has_expected_keys(self): + prof = profiles.new_optimization_profile() + assert "pop_size" in prof["genetic"] + assert "p_c" in prof["genetic"] + + def test_minimize_section_has_expected_keys(self): + prof = profiles.new_optimization_profile() + assert "maxiter" in prof["minimize"] + + +# --------------------------------------------------------------------------- +# new_simulation_profile +# --------------------------------------------------------------------------- + + +class TestNewSimulationProfile: + def test_has_main_and_all_routine_sections(self): + prof = profiles.new_simulation_profile() + assert "main" in prof + for r in profiles._VALID_SIMULATION_ROUTINES: + assert r in prof + + def test_main_routine_is_empty(self): + prof = profiles.new_simulation_profile() + assert prof["main"]["routine"] == "" + + def test_static_radpair_has_knots(self): + prof = profiles.new_simulation_profile() + assert prof["static_radpair"]["knots"] == 20 + + def test_didelphis_tikhonov_has_force_cpu_and_regularization_mode(self): + prof = profiles.new_simulation_profile() + assert "force_cpu" in prof["didelphis_tikhonov"] + assert "regularization_mode" in prof["didelphis_tikhonov"] + + +# --------------------------------------------------------------------------- +# import_profiles +# --------------------------------------------------------------------------- + + +class TestImportProfiles: + def test_extracts_valid_zip(self, tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + zip_path = tmp_path / "import.zip" + with zipfile.ZipFile(str(zip_path), "w") as zf: + zf.writestr("spinsystem/my_profile.ini", "[main]\ng1 = 1.0, 1.0, 1.0\n") + profiles.import_profiles(str(zip_path)) + assert (root / "spinsystem" / "my_profile.ini").exists() + + def test_invalid_top_level_raises(self, tmp_path, monkeypatch): + _make_profile_root(tmp_path, monkeypatch) + zip_path = tmp_path / "bad.zip" + with zipfile.ZipFile(str(zip_path), "w") as zf: + zf.writestr("bogus/file.ini", "[main]\n") + with pytest.raises(ValueError, match="valid profile kind"): + profiles.import_profiles(str(zip_path)) + + def test_skip_existing_when_override_false(self, tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + existing = root / "spinsystem" / "my_profile.ini" + existing.parent.mkdir(parents=True, exist_ok=True) + existing.write_text("original") + zip_path = tmp_path / "import.zip" + with zipfile.ZipFile(str(zip_path), "w") as zf: + zf.writestr("spinsystem/my_profile.ini", "new") + profiles.import_profiles(str(zip_path), override=False) + assert existing.read_text() == "original" + + def test_overwrite_when_override_true(self, tmp_path, monkeypatch): + root = _make_profile_root(tmp_path, monkeypatch) + existing = root / "spinsystem" / "my_profile.ini" + existing.parent.mkdir(parents=True, exist_ok=True) + existing.write_text("original") + zip_path = tmp_path / "import.zip" + with zipfile.ZipFile(str(zip_path), "w") as zf: + zf.writestr("spinsystem/my_profile.ini", "new") + profiles.import_profiles(str(zip_path), override=True) + assert existing.read_text() == "new" + + +# --------------------------------------------------------------------------- +# export +# --------------------------------------------------------------------------- + + +class TestExport: + def _setup_profiles(self, tmp_path, monkeypatch): + root = _install_configspec(tmp_path, monkeypatch, "spinsystem") + prof = profiles.new_spinsystem_profile() + profiles.add_profile(prof, "spinsystem", "export_test") + return root + + def test_creates_zip_in_cwd_when_path_none(self, tmp_path, monkeypatch): + self._setup_profiles(tmp_path, monkeypatch) + monkeypatch.chdir(tmp_path) + profiles.export(path=None, pkind="spinsystem") + assert (tmp_path / "profiles_spinanalysis.zip").exists() + + def test_creates_zip_in_given_path(self, tmp_path, monkeypatch): + self._setup_profiles(tmp_path, monkeypatch) + out_dir = tmp_path / "output" + out_dir.mkdir() + profiles.export(path=str(out_dir), pkind="spinsystem") + assert (out_dir / "profiles_spinanalysis.zip").exists() + + def test_all_exports_all_kinds(self, tmp_path, monkeypatch): + _install_all_configspecs(tmp_path, monkeypatch) + sp = profiles.new_spinsystem_profile() + profiles.add_profile(sp, "spinsystem", "a") + vp = profiles.new_variation_profile() + profiles.add_profile(vp, "variation", "b") + out_dir = tmp_path / "out" + out_dir.mkdir() + profiles.export(path=str(out_dir), pkind="all") + with zipfile.ZipFile(str(out_dir / "profiles_spinanalysis.zip"), "r") as zf: + names = zf.namelist() + assert any("spinsystem/a.ini" in n for n in names) + assert any("variation/b.ini" in n for n in names) + + def test_specific_kind_exports_only_that_kind(self, tmp_path, monkeypatch): + _install_all_configspecs(tmp_path, monkeypatch) + sp = profiles.new_spinsystem_profile() + profiles.add_profile(sp, "spinsystem", "a") + vp = profiles.new_variation_profile() + profiles.add_profile(vp, "variation", "b") + out_dir = tmp_path / "out" + out_dir.mkdir() + profiles.export(path=str(out_dir), pkind="spinsystem") + with zipfile.ZipFile(str(out_dir / "profiles_spinanalysis.zip"), "r") as zf: + names = zf.namelist() + assert any("spinsystem/a.ini" in n for n in names) + assert not any("variation/b.ini" in n for n in names) + + def test_skips_configspec_ini(self, tmp_path, monkeypatch): + self._setup_profiles(tmp_path, monkeypatch) + out_dir = tmp_path / "out" + out_dir.mkdir() + profiles.export(path=str(out_dir), pkind="spinsystem") + with zipfile.ZipFile(str(out_dir / "profiles_spinanalysis.zip"), "r") as zf: + names = zf.namelist() + assert not any("configspec.ini" in n for n in names) + + def test_arcnames_are_relative(self, tmp_path, monkeypatch): + self._setup_profiles(tmp_path, monkeypatch) + out_dir = tmp_path / "out" + out_dir.mkdir() + profiles.export(path=str(out_dir), pkind="spinsystem") + with zipfile.ZipFile(str(out_dir / "profiles_spinanalysis.zip"), "r") as zf: + names = zf.namelist() + for n in names: + assert not n.startswith("/") + assert "spinsystem/export_test.ini" in n diff --git a/tests/test_saving.py b/tests/test_saving.py new file mode 100644 index 0000000..64dd009 --- /dev/null +++ b/tests/test_saving.py @@ -0,0 +1,336 @@ +"""Tests for the output and saving routines in :mod:`spinanalysis.saving`. + +© M. Sc. Florian Quintes, 2026 + +@contact: florian.quintes@pc.uni.freiburg.de + +@author: Florian Quintes +""" + +import datetime +from unittest.mock import patch + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from spinanalysis.epr import ( + Experimental, + FittingOptions, + SimulationOptions, + Spinsystem, +) +from spinanalysis.saving import ( + _get_authors, + _resolve_output_dir, + save_plot, + save_simulation, + write_out_file, +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def epr_objects(): + return Spinsystem(), Experimental(), SimulationOptions(), FittingOptions() + + +@pytest.fixture +def fig(): + f, ax = plt.subplots() + ax.plot([1, 2, 3], [4, 5, 6]) + yield f + plt.close(f) + + +@pytest.fixture +def fig_from_bes3t(test_loading_dir): + """Create a figure from real BES3T data.""" + from spinanalysis.loading import load_epr_bruker_bes3t + + with pytest.warns(UserWarning, match="No axis data"): + axis, data = load_epr_bruker_bes3t("cw_xband_100k") + f, ax = plt.subplots() + ax.plot(axis[0], data.real) + yield f + plt.close(f) + + +@pytest.fixture +def fig_from_transient(test_loading_dir): + """Create a figure from real transient data.""" + from spinanalysis.loading import load_epr_ESP_transient + + axis, data = load_epr_ESP_transient("transient_xband") + f, ax = plt.subplots() + ax.plot(axis[1], data[:, 0].real) + yield f + plt.close(f) + + +# --------------------------------------------------------------------------- +# TestGetAuthors +# --------------------------------------------------------------------------- + + +class TestGetAuthors: + def test_returns_non_empty(self): + result = _get_authors() + assert isinstance(result, str) + assert len(result) > 0 + + def test_contains_author_name(self): + result = _get_authors() + assert "Florian Quintes" in result + + def test_fallback_on_missing_metadata(self): + with patch( + "spinanalysis.saving._pkg_metadata", + side_effect=Exception("not found"), + ): + assert _get_authors() == "spinanalysis" + + +# --------------------------------------------------------------------------- +# TestResolveOutputDir +# --------------------------------------------------------------------------- + + +class TestResolveOutputDir: + def test_explicit_path(self, tmp_path): + out = _resolve_output_dir(tmp_path / "output") + assert out == tmp_path / "output" + assert out.is_dir() + + def test_explicit_path_already_exists(self, tmp_path): + existing = tmp_path / "existing" + existing.mkdir() + out = _resolve_output_dir(existing) + assert out == existing + + def test_nested_path(self, tmp_path): + out = _resolve_output_dir(tmp_path / "a" / "b" / "c") + assert out.is_dir() + + @patch("spinanalysis.saving.datetime") + def test_auto_naming_first(self, mock_dt, tmp_path, monkeypatch): + mock_dt.datetime.now.return_value = datetime.datetime(2026, 1, 15) + mock_dt.datetime.strftime = datetime.datetime.strftime + monkeypatch.chdir(tmp_path) + out = _resolve_output_dir(None) + assert out == tmp_path / "spinanalysis_2026-01-15_1" + assert out.is_dir() + + @patch("spinanalysis.saving.datetime") + def test_auto_naming_skips_existing(self, mock_dt, tmp_path, monkeypatch): + mock_dt.datetime.now.return_value = datetime.datetime(2026, 1, 15) + mock_dt.datetime.strftime = datetime.datetime.strftime + monkeypatch.chdir(tmp_path) + (tmp_path / "spinanalysis_2026-01-15_1").mkdir() + (tmp_path / "spinanalysis_2026-01-15_2").mkdir() + out = _resolve_output_dir(None) + assert out == tmp_path / "spinanalysis_2026-01-15_3" + assert out.is_dir() + + +# --------------------------------------------------------------------------- +# TestSavePlot +# --------------------------------------------------------------------------- + + +class TestSavePlot: + def test_single_figure(self, fig, tmp_path): + save_plot("test_plot.png", fig, path=tmp_path) + assert (tmp_path / "test_plot.png").is_file() + + def test_format_override(self, fig, tmp_path): + save_plot("test_plot.png", fig, path=tmp_path, format="pdf") + assert (tmp_path / "test_plot.pdf").is_file() + assert not (tmp_path / "test_plot.png").exists() + + def test_multiple_figures(self, tmp_path): + figs = [] + for i in range(3): + f, ax = plt.subplots() + ax.plot([1, 2, 3]) + figs.append(f) + try: + save_plot("multi.png", *figs, path=tmp_path) + finally: + for f in figs: + plt.close(f) + assert (tmp_path / "multi_1.png").is_file() + assert (tmp_path / "multi_2.png").is_file() + assert (tmp_path / "multi_3.png").is_file() + + def test_figures_as_list(self, fig, tmp_path): + save_plot("list_plot.png", [fig], path=tmp_path) + assert (tmp_path / "list_plot.png").is_file() + + def test_no_figures_raises(self, tmp_path): + with pytest.raises(ValueError, match="At least one figure"): + save_plot("empty.png", path=tmp_path) + + def test_plot_from_bes3t_data(self, fig_from_bes3t, tmp_path): + save_plot("bes3t_plot.png", fig_from_bes3t, path=tmp_path) + assert (tmp_path / "bes3t_plot.png").is_file() + + def test_plot_from_transient_data(self, fig_from_transient, tmp_path): + save_plot("transient_plot.png", fig_from_transient, path=tmp_path) + assert (tmp_path / "transient_plot.png").is_file() + + +# --------------------------------------------------------------------------- +# TestSaveSimulation +# --------------------------------------------------------------------------- + + +class TestSaveSimulation: + def test_2d_data(self, tmp_path): + x = np.linspace(0, 100, 50) + intensity = np.random.rand(50) + save_simulation("test_sim", x, intensity, path=tmp_path) + assert (tmp_path / "test_sim" / "x_axis.txt").is_file() + assert (tmp_path / "test_sim" / "intensity.txt").is_file() + assert not (tmp_path / "test_sim" / "y_axis.txt").exists() + + def test_3d_data(self, tmp_path): + x = np.linspace(0, 100, 50) + y = np.linspace(0, 50, 30) + intensity = np.random.rand(50, 30) + save_simulation("test_sim3d", x, y, intensity, path=tmp_path) + assert (tmp_path / "test_sim3d" / "x_axis.txt").is_file() + assert (tmp_path / "test_sim3d" / "y_axis.txt").is_file() + assert (tmp_path / "test_sim3d" / "intensity.txt").is_file() + + def test_strips_txt_extension(self, tmp_path): + x = np.linspace(0, 10, 5) + intensity = np.ones(5) + save_simulation("data.txt", x, intensity, path=tmp_path) + assert (tmp_path / "data" / "x_axis.txt").is_file() + assert not (tmp_path / "data.txt").exists() + + def test_wrong_array_count(self, tmp_path): + x = np.ones(5) + with pytest.raises(ValueError, match="Need 2D or 3D"): + save_simulation("bad", x, path=tmp_path) + with pytest.raises(ValueError, match="Need 2D or 3D"): + save_simulation("bad", x, x, x, x, path=tmp_path) + + def test_roundtrip_2d(self, tmp_path): + x = np.linspace(0, 100, 50) + intensity = np.random.rand(50) + 1j * np.random.rand(50) + save_simulation("roundtrip", x, intensity, path=tmp_path) + x_loaded = np.loadtxt(str(tmp_path / "roundtrip" / "x_axis.txt")) + assert np.allclose(x_loaded, x) + + def test_custom_path_created(self, tmp_path): + nested = tmp_path / "a" / "b" / "c" + x = np.ones(3) + intensity = np.ones(3) + save_simulation("sim", x, intensity, path=nested) + assert (nested / "sim" / "intensity.txt").is_file() + + +# --------------------------------------------------------------------------- +# TestWriteOutFile +# --------------------------------------------------------------------------- + + +class TestWriteOutFile: + def test_simulation_mode(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + assert out == tmp_path / "teacups_result.spinanalysis" + assert out.is_file() + + def test_fit_mode(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + FitOpt.routine = "scipy_optimize" + out = write_out_file(Sys, Exp, SimOpt, FitOpt, path=tmp_path) + assert out == tmp_path / "scipy_optimize_result.spinanalysis" + assert out.is_file() + + def test_current_best(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + FitOpt.routine = "scipy_optimize" + out = write_out_file(Sys, Exp, SimOpt, FitOpt, current_best=True, path=tmp_path) + assert "current_best" in out.name + assert out.is_file() + + def test_result_default(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + assert "result" in out.name + + def test_file_contains_section_headers(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + content = out.read_text() + assert "SPINSYSTEM" in content + assert "EXPERIMENTAL" in content + assert "SIMULATION OPTIONS" in content + + def test_fit_mode_contains_fitting_section(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + FitOpt.routine = "scipy_optimize" + out = write_out_file(Sys, Exp, SimOpt, FitOpt, path=tmp_path) + content = out.read_text() + assert "FITTING OPTIONS" in content + + def test_sim_mode_no_fitting_section(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + content = out.read_text() + assert "FITTING OPTIONS" not in content + + def test_file_contains_author(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + content = out.read_text() + assert "FLORIAN QUINTES" in content + + def test_file_contains_knots_not_grid_points(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt, path=tmp_path) + content = out.read_text() + assert "knots" in content + assert "grid_points" not in content + + def test_none_fitting_fields_skipped(self, epr_objects, tmp_path): + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + FitOpt.routine = "scipy_optimize" + out = write_out_file(Sys, Exp, SimOpt, FitOpt, path=tmp_path) + content = out.read_text() + fit_section = content.split("FITTING OPTIONS")[1] + assert "routine" in fit_section + assert "method" not in fit_section + assert "None" not in fit_section + + @patch("spinanalysis.saving.datetime") + def test_auto_path(self, mock_dt, epr_objects, tmp_path, monkeypatch): + mock_dt.datetime.now.return_value = datetime.datetime(2026, 1, 15) + mock_dt.datetime.strftime = datetime.datetime.strftime + monkeypatch.chdir(tmp_path) + Sys, Exp, SimOpt, FitOpt = epr_objects + SimOpt.routine = "teacups" + out = write_out_file(Sys, Exp, SimOpt) + assert out.parent == tmp_path / "spinanalysis_2026-01-15_1" + assert out.is_file() diff --git a/uv.lock b/uv.lock index 77fd2bb..34a3c9b 100644 --- a/uv.lock +++ b/uv.lock @@ -19,6 +19,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -694,6 +703,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1026,6 +1106,7 @@ dependencies = [ { name = "mkl" }, { name = "mkl-service" }, { name = "numpy" }, + { name = "pydantic" }, { name = "scipy" }, { name = "statsmodels" }, ] @@ -1048,6 +1129,7 @@ requires-dist = [ { name = "mkl", specifier = ">=2026.1.0" }, { name = "mkl-service", specifier = ">=2.7.2" }, { name = "numpy", specifier = ">=2.5.1" }, + { name = "pydantic", specifier = ">=2.13.4" }, { name = "scipy", specifier = ">=1.18.0" }, { name = "statsmodels", specifier = ">=0.14.6" }, ] @@ -1111,6 +1193,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/6d/9095c93326d0f8a5469ab22480d02795f24c08f1e7f383c73316ff106347/tcmlib-1.5.0-py2.py3-none-win_amd64.whl", hash = "sha256:f7b62787214083d490b39d7650f1e477eeacc875e7f86799feb9aa1fd34460d4", size = 366719, upload-time = "2026-04-24T14:09:45.977Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2026.3"