enthalpy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. enthalpy/__init__.py +55 -0
  2. enthalpy/backend/__init__.py +78 -0
  3. enthalpy/backend/_base.py +144 -0
  4. enthalpy/backend/_jax.py +149 -0
  5. enthalpy/backend/_numpy.py +83 -0
  6. enthalpy/chemistry/__init__.py +34 -0
  7. enthalpy/chemistry/equilibrium.py +426 -0
  8. enthalpy/chemistry/reactions.py +185 -0
  9. enthalpy/chemistry/species.py +41 -0
  10. enthalpy/chemistry/system.py +471 -0
  11. enthalpy/chemistry/thermodynamics.py +144 -0
  12. enthalpy/cli.py +94 -0
  13. enthalpy/constants.py +21 -0
  14. enthalpy/exceptions.py +83 -0
  15. enthalpy/experiments/__init__.py +29 -0
  16. enthalpy/experiments/base.py +183 -0
  17. enthalpy/experiments/conditions.py +56 -0
  18. enthalpy/experiments/dataset.py +112 -0
  19. enthalpy/experiments/experiment.py +177 -0
  20. enthalpy/experiments/itc.py +90 -0
  21. enthalpy/experiments/nmr.py +115 -0
  22. enthalpy/experiments/protocols.py +221 -0
  23. enthalpy/experiments/stocks.py +126 -0
  24. enthalpy/experiments/synthetic.py +51 -0
  25. enthalpy/experiments/uvvis.py +124 -0
  26. enthalpy/inference/__init__.py +23 -0
  27. enthalpy/inference/fitting.py +321 -0
  28. enthalpy/inference/optimizers/__init__.py +6 -0
  29. enthalpy/inference/optimizers/base.py +63 -0
  30. enthalpy/inference/optimizers/scipy.py +71 -0
  31. enthalpy/inference/parameters.py +207 -0
  32. enthalpy/inference/problem.py +318 -0
  33. enthalpy/inference/result.py +320 -0
  34. enthalpy/inference/varpro.py +183 -0
  35. enthalpy/io/__init__.py +20 -0
  36. enthalpy/io/export.py +91 -0
  37. enthalpy/io/importers/__init__.py +5 -0
  38. enthalpy/io/importers/csv.py +89 -0
  39. enthalpy/io/records.py +215 -0
  40. enthalpy/io/schema.py +421 -0
  41. enthalpy/io/store.py +79 -0
  42. enthalpy/project.py +137 -0
  43. enthalpy/specs.py +148 -0
  44. enthalpy/statistics/__init__.py +30 -0
  45. enthalpy/statistics/bootstrap.py +98 -0
  46. enthalpy/statistics/comparison.py +134 -0
  47. enthalpy/statistics/covariance.py +102 -0
  48. enthalpy/statistics/profile.py +231 -0
  49. enthalpy/units.py +170 -0
  50. enthalpy/visualization/__init__.py +18 -0
  51. enthalpy/visualization/_axis.py +64 -0
  52. enthalpy/visualization/_style.py +150 -0
  53. enthalpy/visualization/diagnostics.py +120 -0
  54. enthalpy/visualization/fits.py +221 -0
  55. enthalpy/visualization/species.py +91 -0
  56. enthalpy-0.1.0.dist-info/METADATA +101 -0
  57. enthalpy-0.1.0.dist-info/RECORD +61 -0
  58. enthalpy-0.1.0.dist-info/WHEEL +5 -0
  59. enthalpy-0.1.0.dist-info/entry_points.txt +2 -0
  60. enthalpy-0.1.0.dist-info/licenses/LICENSE +674 -0
  61. enthalpy-0.1.0.dist-info/top_level.txt +1 -0
enthalpy/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """EnthalPy: global analysis of molecular binding equilibria from titration experiments."""
2
+
3
+ from enthalpy import io
4
+ from enthalpy.chemistry import ChemicalSystem, GibbsEnthalpy
5
+ from enthalpy.experiments import (
6
+ Conditions,
7
+ Experiment,
8
+ ITCHeat,
9
+ ITCInjectionProtocol,
10
+ NMRShift,
11
+ Stock,
12
+ TitrationProtocol,
13
+ UVVisAbsorbance,
14
+ synthesize,
15
+ )
16
+ from enthalpy.inference import FitResult, ScipyLeastSquares
17
+ from enthalpy.project import GlobalFit
18
+ from enthalpy.statistics import (
19
+ bootstrap,
20
+ compare_models,
21
+ information_criteria,
22
+ likelihood_ratio_test,
23
+ linear_standard_errors,
24
+ profile_likelihood,
25
+ )
26
+ from enthalpy.units import Q_, ureg
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "ChemicalSystem",
32
+ "Conditions",
33
+ "Experiment",
34
+ "FitResult",
35
+ "GibbsEnthalpy",
36
+ "GlobalFit",
37
+ "ITCHeat",
38
+ "ITCInjectionProtocol",
39
+ "NMRShift",
40
+ "Q_",
41
+ "ScipyLeastSquares",
42
+ "Stock",
43
+ "TitrationProtocol",
44
+ "UVVisAbsorbance",
45
+ "__version__",
46
+ "bootstrap",
47
+ "io",
48
+ "compare_models",
49
+ "information_criteria",
50
+ "likelihood_ratio_test",
51
+ "linear_standard_errors",
52
+ "profile_likelihood",
53
+ "synthesize",
54
+ "ureg",
55
+ ]
@@ -0,0 +1,78 @@
1
+ """Array backends for the numerical kernels (decision D3).
2
+
3
+ Examples
4
+ --------
5
+ >>> from enthalpy.backend import get_backend
6
+ >>> backend = get_backend("numpy")
7
+ >>> backend.xp.exp(backend.asarray([0.0]))
8
+ array([1.])
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+
15
+ from enthalpy.backend._base import Array, Backend
16
+ from enthalpy.backend._jax import jax_unavailable_reason
17
+ from enthalpy.backend._numpy import NumpyBackend
18
+ from enthalpy.exceptions import BackendError
19
+
20
+ __all__ = ["Array", "Backend", "available_backends", "get_backend"]
21
+
22
+ _BACKEND_NAMES = ("numpy", "jax")
23
+ _instances: dict[str, Backend] = {}
24
+
25
+
26
+ def available_backends() -> list[str]:
27
+ """Return the names of backends that can be used on this machine.
28
+
29
+ JAX counts as available only if it is installed *and* imports successfully (jaxlib can fail at
30
+ import time, e.g. on CPUs without AVX). Checking this imports JAX, which takes about a second the
31
+ first time.
32
+ """
33
+ names = ["numpy"]
34
+ if importlib.util.find_spec("jax") is not None and jax_unavailable_reason() is None:
35
+ names.append("jax")
36
+ return names
37
+
38
+
39
+ def get_backend(backend: str | Backend = "numpy", *, enable_x64: bool = False) -> Backend:
40
+ """Return a backend instance.
41
+
42
+ Parameters
43
+ ----------
44
+ backend
45
+ Backend name (``"numpy"`` or ``"jax"``) or an existing :class:`Backend`, which is
46
+ validated and returned unchanged.
47
+ enable_x64
48
+ JAX only: enable 64-bit floats globally if they are not enabled yet.
49
+
50
+ Raises
51
+ ------
52
+ BackendError
53
+ If the backend is unknown, its dependencies are missing, or it is misconfigured.
54
+ """
55
+ if isinstance(backend, Backend):
56
+ backend.validate()
57
+ return backend
58
+
59
+ name = backend.lower()
60
+ if name not in _BACKEND_NAMES:
61
+ raise BackendError(f"Unknown backend {backend!r}. Available backends: {', '.join(_BACKEND_NAMES)}.")
62
+
63
+ instance = _instances.get(name)
64
+ if instance is None:
65
+ if name == "numpy":
66
+ instance = NumpyBackend()
67
+ else:
68
+ from enthalpy.backend._jax import JaxBackend
69
+
70
+ instance = JaxBackend(enable_x64=enable_x64)
71
+ _instances[name] = instance
72
+ elif name == "jax" and enable_x64:
73
+ import jax
74
+
75
+ jax.config.update("jax_enable_x64", True)
76
+
77
+ instance.validate()
78
+ return instance
@@ -0,0 +1,144 @@
1
+ """Abstract array backend (decision D3).
2
+
3
+ Numerical kernels (θ → thermodynamics → equilibrium → observables → log-likelihood) are written
4
+ against a :class:`Backend`. It provides
5
+
6
+ - an Array-API namespace (:attr:`Backend.xp`) for elementwise operations and linear algebra,
7
+ - functional control flow (:meth:`Backend.while_loop`, :meth:`Backend.fori_loop`) that stays
8
+ traceable under ``jax.jit``,
9
+ - functional updates (:meth:`Backend.at_set`, :meth:`Backend.at_add`) because JAX arrays are
10
+ immutable,
11
+ - root finding with implicit differentiation (:meth:`Backend.custom_root`).
12
+
13
+ Kernel rules: no in-place assignment, no Python ``if``/``while`` on array values, and float64
14
+ throughout.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from abc import ABC, abstractmethod
20
+ from collections.abc import Callable
21
+ from types import ModuleType
22
+ from typing import Any, ClassVar, TypeAlias, TypeVar
23
+
24
+ import numpy as np
25
+ from numpy.typing import NDArray
26
+
27
+ __all__ = ["Array", "Backend"]
28
+
29
+ Array: TypeAlias = Any
30
+ """Backend array type (``numpy.ndarray`` or ``jax.Array``)."""
31
+
32
+ T = TypeVar("T")
33
+ F = TypeVar("F", bound=Callable[..., Any])
34
+
35
+
36
+ class Backend(ABC):
37
+ """Interface that every array backend implements."""
38
+
39
+ name: ClassVar[str]
40
+
41
+ @property
42
+ @abstractmethod
43
+ def xp(self) -> ModuleType:
44
+ """Array-API compatible namespace (use like ``numpy``)."""
45
+
46
+ def validate(self) -> None: # noqa: B027 - optional hook, deliberately not abstract
47
+ """Check that the backend is usable in its current configuration.
48
+
49
+ Raises
50
+ ------
51
+ BackendError
52
+ If the backend is misconfigured.
53
+ """
54
+
55
+ def asarray(self, x: Any) -> Array:
56
+ """Convert ``x`` to a ``float64`` array of this backend."""
57
+ return self.xp.asarray(x, dtype=self.xp.float64)
58
+
59
+ @abstractmethod
60
+ def to_numpy(self, x: Array) -> NDArray[np.float64]:
61
+ """Convert a backend array to a NumPy ``float64`` array."""
62
+
63
+ @abstractmethod
64
+ def while_loop(self, cond_fun: Callable[[T], Any], body_fun: Callable[[T], T], init_val: T) -> T:
65
+ """Run ``body_fun`` while ``cond_fun`` is true (``jax.lax.while_loop`` semantics).
66
+
67
+ Reverse-mode differentiation through this loop is not supported. Differentiate through
68
+ :meth:`custom_root` instead.
69
+ """
70
+
71
+ @abstractmethod
72
+ def fori_loop(self, lower: int, upper: int, body_fun: Callable[[int, T], T], init_val: T) -> T:
73
+ """Apply ``val = body_fun(i, val)`` for ``i`` in ``range(lower, upper)``."""
74
+
75
+ @abstractmethod
76
+ def at_set(self, x: Array, index: Any, values: Any) -> Array:
77
+ """Return a copy of ``x`` with ``x[index] = values`` (``x`` is not modified)."""
78
+
79
+ @abstractmethod
80
+ def at_add(self, x: Array, index: Any, values: Any) -> Array:
81
+ """Return a copy of ``x`` with ``values`` added at ``index``; repeated indices accumulate."""
82
+
83
+ @abstractmethod
84
+ def custom_root(
85
+ self,
86
+ f: Callable[[Array], Array],
87
+ initial_guess: Array,
88
+ solve: Callable[[Callable[[Array], Array], Array], Any],
89
+ tangent_solve: Callable[[Callable[[Array], Array], Array], Array],
90
+ *,
91
+ has_aux: bool = False,
92
+ ) -> Any:
93
+ """Find a root of ``f`` and make it differentiable by the implicit-function theorem.
94
+
95
+ Parameters
96
+ ----------
97
+ f
98
+ Residual function. Differentiable parameters are the variables ``f`` closes over.
99
+ initial_guess
100
+ Starting point passed to ``solve``.
101
+ solve
102
+ ``solve(f, initial_guess) -> x`` with ``f(x) ≈ 0``.
103
+ tangent_solve
104
+ ``tangent_solve(g, y) -> x`` solving the linear system ``g(x) = y``, where ``g`` is the
105
+ Jacobian-vector product of ``f`` at the root. :meth:`dense_tangent_solve` is a default.
106
+ has_aux
107
+ If true, ``solve`` returns ``(x, aux)``. The auxiliary output (e.g. convergence flags)
108
+ is passed through without being differentiated.
109
+
110
+ Notes
111
+ -----
112
+ On backends without automatic differentiation this simply returns
113
+ ``solve(f, initial_guess)``. Derivatives are then computed explicitly by the caller with
114
+ the same implicit-function formula.
115
+ """
116
+
117
+ @abstractmethod
118
+ def dense_tangent_solve(self, g: Callable[[Array], Array], y: Array) -> Array:
119
+ """Solve the linear system ``g(x) = y`` by materialising the matrix of the linear map ``g``."""
120
+
121
+ def block_tangent_solve(self, g: Callable[[Array], Array], y: Array) -> Array:
122
+ """Solve ``g(x) = y`` for a linear map ``g`` that is block-diagonal over the leading axes.
123
+
124
+ ``y`` has shape ``(..., n)``, and ``g`` must act independently on every leading index (e.g.
125
+ independent titration points). The ``n × n`` blocks are obtained from ``n`` evaluations
126
+ of ``g``, so the cost is linear in the number of blocks.
127
+ """
128
+ xp = self.xp
129
+ n = y.shape[-1]
130
+ identity = xp.eye(n, dtype=xp.float64)
131
+ columns = [g(xp.zeros(y.shape, dtype=xp.float64) + identity[k]) for k in range(n)]
132
+ blocks = xp.stack(columns, axis=-1) # blocks[..., i, k] = g(e_k)[..., i]
133
+ return xp.linalg.solve(blocks, y[..., None])[..., 0]
134
+
135
+ @abstractmethod
136
+ def stop_gradient(self, x: T) -> T:
137
+ """Treat ``x`` as a constant for differentiation."""
138
+
139
+ @abstractmethod
140
+ def jit(self, fun: F) -> F:
141
+ """Compile ``fun`` if the backend supports it; otherwise return it unchanged."""
142
+
143
+ def __repr__(self) -> str:
144
+ return f"<{type(self).__name__} {self.name!r}>"
@@ -0,0 +1,149 @@
1
+ """JAX backend (automatic differentiation, JIT compilation, accelerators).
2
+
3
+ JAX is an optional dependency (``pip install enthalpy[jax]``). Equilibrium concentrations span
4
+ many orders of magnitude, so float64 is mandatory. The backend refuses to run while JAX is in its
5
+ default float32 mode.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from types import ModuleType
12
+ from typing import Any, TypeVar
13
+
14
+ import numpy as np
15
+ from numpy.typing import NDArray
16
+
17
+ from enthalpy.backend._base import Array, Backend
18
+ from enthalpy.exceptions import BackendError
19
+
20
+ __all__ = ["JaxBackend", "import_jax", "jax_unavailable_reason"]
21
+
22
+ T = TypeVar("T")
23
+ F = TypeVar("F", bound=Callable[..., Any])
24
+
25
+ _X64_HELP = (
26
+ "The JAX backend requires 64-bit floats. Enable them before creating any arrays with "
27
+ "`jax.config.update('jax_enable_x64', True)`, by setting the environment variable "
28
+ "JAX_ENABLE_X64=1, or by calling `enthalpy.backend.get_backend('jax', enable_x64=True)`."
29
+ )
30
+
31
+
32
+ def import_jax() -> tuple[Any, Any]:
33
+ """Import ``jax`` and ``jax.numpy``.
34
+
35
+ Raises
36
+ ------
37
+ BackendError
38
+ If JAX is not installed, or if it is installed but cannot be imported. The latter happens,
39
+ for example, when jaxlib requires CPU instructions (AVX) that the machine does not provide.
40
+ """
41
+ try:
42
+ import jax
43
+ import jax.numpy as jnp
44
+ except ImportError as exc:
45
+ raise BackendError(f"The JAX backend requires the optional dependency 'jax' (enthalpy[jax]): {exc}") from exc
46
+ except Exception as exc: # e.g. RuntimeError from jaxlib's CPU feature check
47
+ raise BackendError(f"JAX is installed but cannot be imported: {exc}") from exc
48
+ return jax, jnp
49
+
50
+
51
+ def jax_unavailable_reason() -> str | None:
52
+ """Return why the JAX backend cannot be used, or ``None`` if JAX imports successfully."""
53
+ try:
54
+ import_jax()
55
+ except BackendError as exc:
56
+ return str(exc)
57
+ return None
58
+
59
+
60
+ class JaxBackend(Backend):
61
+ """Backend based on ``jax.numpy`` and ``jax.lax``.
62
+
63
+ Parameters
64
+ ----------
65
+ enable_x64
66
+ If true, switch JAX to 64-bit mode globally. Otherwise 64-bit mode must already be enabled.
67
+ """
68
+
69
+ name = "jax"
70
+
71
+ def __init__(self, *, enable_x64: bool = False) -> None:
72
+ jax, jnp = import_jax()
73
+ self._jax: Any = jax
74
+ self._jnp: Any = jnp
75
+ if enable_x64:
76
+ jax.config.update("jax_enable_x64", True)
77
+ self.validate()
78
+
79
+ def validate(self) -> None:
80
+ if not bool(self._jax.config.jax_enable_x64):
81
+ raise BackendError(_X64_HELP)
82
+
83
+ @property
84
+ def xp(self) -> ModuleType:
85
+ return self._jnp
86
+
87
+ def to_numpy(self, x: Array) -> NDArray[np.float64]:
88
+ return np.asarray(x, dtype=np.float64)
89
+
90
+ def while_loop(self, cond_fun: Callable[[T], Any], body_fun: Callable[[T], T], init_val: T) -> T:
91
+ return self._jax.lax.while_loop(cond_fun, body_fun, init_val)
92
+
93
+ def fori_loop(self, lower: int, upper: int, body_fun: Callable[[int, T], T], init_val: T) -> T:
94
+ return self._jax.lax.fori_loop(lower, upper, body_fun, init_val)
95
+
96
+ def at_set(self, x: Array, index: Any, values: Any) -> Array:
97
+ return self._jnp.asarray(x).at[index].set(values)
98
+
99
+ def at_add(self, x: Array, index: Any, values: Any) -> Array:
100
+ return self._jnp.asarray(x).at[index].add(values)
101
+
102
+ def custom_root(
103
+ self,
104
+ f: Callable[[Array], Array],
105
+ initial_guess: Array,
106
+ solve: Callable[[Callable[[Array], Array], Array], Any],
107
+ tangent_solve: Callable[[Callable[[Array], Array], Array], Array],
108
+ *,
109
+ has_aux: bool = False,
110
+ ) -> Any:
111
+ lax = self._jax.lax
112
+ if not has_aux:
113
+ return lax.custom_root(f, initial_guess, solve, tangent_solve)
114
+
115
+ # Some JAX versions (e.g. 0.10.x) produce tangents of the wrong dtype for integer or boolean
116
+ # auxiliary outputs of custom_root. Carry all auxiliary leaves as float64 and restore their
117
+ # dtypes afterwards; the auxiliary output is never differentiated.
118
+ tree_util = self._jax.tree_util
119
+ jnp = self._jnp
120
+ meta: dict[str, Any] = {}
121
+
122
+ def solve_with_float_aux(fun: Callable[[Array], Array], guess: Array) -> Any:
123
+ solution, aux = solve(fun, guess)
124
+ leaves, treedef = tree_util.tree_flatten(aux)
125
+ meta["treedef"], meta["dtypes"] = treedef, [jnp.asarray(leaf).dtype for leaf in leaves]
126
+ return solution, tree_util.tree_unflatten(
127
+ treedef, [jnp.asarray(leaf, dtype=jnp.float64) for leaf in leaves]
128
+ )
129
+
130
+ solution, aux = lax.custom_root(f, initial_guess, solve_with_float_aux, tangent_solve, has_aux=True)
131
+ leaves = [
132
+ lax.stop_gradient(leaf).astype(dtype)
133
+ for leaf, dtype in zip(tree_util.tree_leaves(aux), meta["dtypes"], strict=True)
134
+ ]
135
+ return solution, tree_util.tree_unflatten(meta["treedef"], leaves)
136
+
137
+ def dense_tangent_solve(self, g: Callable[[Array], Array], y: Array) -> Array:
138
+ jnp = self._jnp
139
+ y = jnp.asarray(y)
140
+ if y.ndim == 0:
141
+ return y / g(jnp.ones_like(y))
142
+ matrix = self._jax.jacfwd(g)(y).reshape(y.size, y.size)
143
+ return jnp.linalg.solve(matrix, y.ravel()).reshape(y.shape)
144
+
145
+ def stop_gradient(self, x: T) -> T:
146
+ return self._jax.lax.stop_gradient(x)
147
+
148
+ def jit(self, fun: F) -> F:
149
+ return self._jax.jit(fun)
@@ -0,0 +1,83 @@
1
+ """NumPy backend (reference implementation, no automatic differentiation)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from types import ModuleType
7
+ from typing import Any, TypeVar
8
+
9
+ import array_api_compat.numpy as xp_numpy
10
+ import numpy as np
11
+ from numpy.typing import NDArray
12
+
13
+ from enthalpy.backend._base import Array, Backend
14
+
15
+ __all__ = ["NumpyBackend"]
16
+
17
+ T = TypeVar("T")
18
+ F = TypeVar("F", bound=Callable[..., Any])
19
+
20
+
21
+ class NumpyBackend(Backend):
22
+ """Backend based on NumPy, using the Array-API compatible ``array_api_compat.numpy`` namespace."""
23
+
24
+ name = "numpy"
25
+
26
+ @property
27
+ def xp(self) -> ModuleType:
28
+ return xp_numpy
29
+
30
+ def to_numpy(self, x: Array) -> NDArray[np.float64]:
31
+ return np.asarray(x, dtype=np.float64)
32
+
33
+ def while_loop(self, cond_fun: Callable[[T], Any], body_fun: Callable[[T], T], init_val: T) -> T:
34
+ val = init_val
35
+ while bool(cond_fun(val)):
36
+ val = body_fun(val)
37
+ return val
38
+
39
+ def fori_loop(self, lower: int, upper: int, body_fun: Callable[[int, T], T], init_val: T) -> T:
40
+ val = init_val
41
+ for i in range(lower, upper):
42
+ val = body_fun(i, val)
43
+ return val
44
+
45
+ def at_set(self, x: Array, index: Any, values: Any) -> Array:
46
+ out = np.array(x, copy=True)
47
+ out[index] = values
48
+ return out
49
+
50
+ def at_add(self, x: Array, index: Any, values: Any) -> Array:
51
+ out = np.array(x, copy=True)
52
+ np.add.at(out, index, values) # pyright: ignore[reportUnknownMemberType] (incomplete NumPy stub)
53
+ return out
54
+
55
+ def custom_root(
56
+ self,
57
+ f: Callable[[Array], Array],
58
+ initial_guess: Array,
59
+ solve: Callable[[Callable[[Array], Array], Array], Any],
60
+ tangent_solve: Callable[[Callable[[Array], Array], Array], Array],
61
+ *,
62
+ has_aux: bool = False,
63
+ ) -> Any:
64
+ return solve(f, initial_guess)
65
+
66
+ def dense_tangent_solve(self, g: Callable[[Array], Array], y: Array) -> Array:
67
+ rhs: NDArray[np.float64] = np.asarray(y, dtype=np.float64)
68
+ if rhs.ndim == 0:
69
+ return rhs / np.asarray(g(np.ones_like(rhs)), dtype=np.float64)
70
+ # g is linear, so the columns of its matrix are the images of the unit vectors.
71
+ matrix = np.empty((rhs.size, rhs.size), dtype=np.float64)
72
+ for i in range(rhs.size):
73
+ unit = np.zeros(rhs.size, dtype=np.float64)
74
+ unit[i] = 1.0
75
+ matrix[:, i] = np.asarray(g(unit.reshape(rhs.shape)), dtype=np.float64).ravel()
76
+ solution = np.asarray(np.linalg.solve(matrix, rhs.ravel()), dtype=np.float64)
77
+ return solution.reshape(rhs.shape)
78
+
79
+ def stop_gradient(self, x: T) -> T:
80
+ return x
81
+
82
+ def jit(self, fun: F) -> F:
83
+ return fun
@@ -0,0 +1,34 @@
1
+ """Chemical model: species, reactions, thermodynamics and equilibrium (independent of techniques)."""
2
+
3
+ from enthalpy.chemistry.equilibrium import (
4
+ EquilibriumSensitivities,
5
+ EquilibriumSolver,
6
+ EquilibriumState,
7
+ NewtonSolver,
8
+ OneToOneSolver,
9
+ default_solver,
10
+ equilibrium_sensitivities,
11
+ )
12
+ from enthalpy.chemistry.reactions import Reaction, format_reaction, parse_reaction
13
+ from enthalpy.chemistry.species import Species
14
+ from enthalpy.chemistry.system import ChemicalSystem, FormationModel
15
+ from enthalpy.chemistry.thermodynamics import GibbsEnthalpy, ThermodynamicModel, ln_k
16
+
17
+ __all__ = [
18
+ "ChemicalSystem",
19
+ "EquilibriumSensitivities",
20
+ "EquilibriumSolver",
21
+ "EquilibriumState",
22
+ "FormationModel",
23
+ "GibbsEnthalpy",
24
+ "NewtonSolver",
25
+ "OneToOneSolver",
26
+ "Reaction",
27
+ "Species",
28
+ "ThermodynamicModel",
29
+ "default_solver",
30
+ "equilibrium_sensitivities",
31
+ "format_reaction",
32
+ "ln_k",
33
+ "parse_reaction",
34
+ ]