Source code for composition_variables

# Copyright 2025 Onera
# This file is part of the Noda package
# SPDX-License-Identifier: GPL-3.0-or-later

"""Gather and organize the variables that describe a system composition."""

import numpy as np
import scipy.interpolate as spi

[docs] def vec_to_nod(x, z): """ Evaluate `x` (defined on midpoints) on nodes by linear interpolation. Parameters ---------- x : 1D array Variable, shape (`nz` - 1). z : 1D array Node positions, shape (`nz`,). Returns ------- arr : 1D array Variable evaluated on nodes, shape (`nind`, `nz`). """ zm = (z[:-1] + z[1:])/2 f = spi.interp1d(zm, x, kind='linear', fill_value='extrapolate') return f(z)
[docs] def arr_to_nod(x, z, interpkind='cubic'): """ Evaluate `x` (defined on midpoints) on nodes by linear interpolation. Parameters ---------- x : 2D array Variable, shape (`nind`, `nz` - 1). z : 1D array Node positions, shape (`nz`,). Returns ------- arr : 2D array Variable evaluated on nodes, shape (`nind`, `nz`). """ nind = x.shape[0] arr = np.zeros((nind, z.size)) zm = (z[:-1] + z[1:])/2 for i, row in enumerate(x): f = spi.interp1d(zm, row, kind='linear', fill_value='extrapolate') arr[i] = f(z) return arr
[docs] class MultiVariable: """ Vector variable evaluated on midpoints `zm`. One row per independent constituent. """
[docs] def __init__(self, v_init): """ Class constructor. Parameters ---------- v_init : dict Initial value, dict of 1D arrays of shape (`nz` - 1,), with independent constituents as keys. Attributes ---------- mid : 2D array Variable evaluated on midpoints, shape (`ninds`, `nz` - 1) """ self.mid = np.array(list(v_init.values()))
[docs] def nod(self, z): """Evaluate on node, see :func:`arr_to_nod`.""" return arr_to_nod(self.mid, z)
[docs] class UniVariable: """Scalar variable evaluated on midpoints `zm`."""
[docs] def __init__(self, v_init): """ Class constructor. Parameters ---------- v_init : dict Initial value, 1D array of shape (`nz` - 1,). Attributes ---------- mid : 1D array Variable evaluated on midpoints, shape (`nz` - 1,) """ self.mid = v_init
[docs] def nod(self, z): """ Evaluate on nodes (`z`) by interpolation. Parameters ---------- z : 1D array Node positions, shape (`nz`,). Returns ------- 1D array Variable evaluated on nodes, shape (`nz`,). """ zm = (z[:-1] + z[1:])/2 f = spi.interp1d(zm, self.mid, fill_value='extrapolate') return f(z)
[docs] class CompositionVariables: """ Contain and organize composition variables. This describes the composition of a system with one metal phase and one pore phase. The system composition is initialized with metal atom fractions. Once all variables are built, the reference variable is the system concentration (in mol/m3). Only the system concentration will be updated in the calculations. The other variables will derive from the system concentrations --- the other variables are 'read-only'. Attributes ---------- x : 2D array Metal atom fractions. y : 2D array Metal site fractions. V : 1D array Average system molar volume. Vm : 1D array Average metal molar volume. fm : 1D array Metal volume fraction. fp : 1D array Pore volume fraction. c : 2D array System concentrations. """ # pylint: disable=too-many-instance-attributes
[docs] def __init__(self, comps, x_init, yVa_init, V_partial, fm): """ Class constructor. | Build metal atom fractions. | Build metal site fractions. | Build average molar volume of metal (`Vm`), then of system (`V`). | Build system concentration. Parameters ---------- comps : list of str System constituents, ordered: ['Va'] + inds + [dep]. x_init : dict Initial atom fractions, dict of 1D arrays of shape (`nz` - 1,). yVa_init : 1D array Initial vacancy site fraction, shape (`nz` - 1,). V_partial : dict Partial molar volumes. fm : 1D array Initial metal volume fraction, shape (`nz` - 1,). """ inds = comps[1:-1] dep = comps[-1] self.V_partial = V_partial x_dep = 1 - sum(x_init.values()) x_full = {k: x_init[k] for k in inds} x_full[dep] = x_dep y_full = {k: x_full[k]*(1 - yVa_init) for k in comps[1:]} y_full['Va'] = yVa_init if V_partial['Va'] == 'local': Vm = sum(x_full[k]*V_partial[k] for k in comps[1:]) Vk_arr = np.hstack((np.nan, np.array([V_partial[k] for k in comps[1:]]) )) else: Vm = sum(y_full[k]*V_partial[k] for k in comps) Vk_arr = np.array([V_partial[k] for k in comps]) self.Vk_arr = Vk_arr[np.newaxis].T V = Vm/fm c_full = {k: y_full[k]/V for k in comps} self.comps = comps self._x = MultiVariable(x_full) self._y = MultiVariable(y_full) self.c = MultiVariable(c_full) self._Vm = UniVariable(Vm) self._fm = UniVariable(fm) self._fp = UniVariable(1 - fm) self._V = UniVariable(V)
@property def x(self): """ Atom fractions evaluated on midpoints. Returns ------- 2D array Shape (`ninds` + 1, `nz` - 1). """ self._x.mid = self.y.mid[1:]/(1 - self.y.mid[0]) return self._x @property def y(self): """ Site fractions of all metal constituents evaluated on midpoints. Returns ------- 2D array Shape (`ninds` + 2, `nz` - 1). | Order of rows: | 0 : vacancies | 1.. : independent atom constituents | n : dependent constituent """ self._y.mid = self.c.mid/sum(self.c.mid) return self._y @property def V(self): """Average molar volume of the system, 1D array of shape (`nz` - 1).""" self._V.mid = 1/sum(self.c.mid) return self._V @property def Vm(self): """ Average molar volume of the metal, 1D array of shape (`nz` - 1). Choice of two definitions depending on whether the vacancy molar volume is independent or that of the local environment. """ if self.V_partial['Va'] == 'local': self._Vm.mid = sum(self.x.mid*self.Vk_arr[1:]) else: self._Vm.mid = sum(self.y.mid*self.Vk_arr) return self._Vm @property def fm(self): """Metal volume fraction, 1D array of shape (`nz` - 1).""" self._fm.mid = self.Vm.mid/self.V.mid return self._fm @property def fp(self): """Pore volume fraction, 1D array of shape (`nz` - 1).""" self._fp.mid = 1 - self.fm.mid return self._fp