landau.interpolate

class landau.interpolate.CalphadFittedSurface(f0_model, df_model, L_models)[source]

Bases: FittedSurface

Fitted surface for CalphadSurface2DInterpolator.

Holds SGTE models for the terminal free energies and polynomial models for each Redlich-Kister interaction coefficient. slice_at() evaluates them at T to produce a RedlichKisterInterpolation with an analytic c-derivative.

slice_at(T)[source]

Return H(c) = f(T,c) + T·S(c) — the entropy-removed free energy — as a 1-D interpolation.

Return type:

RedlichKisterInterpolation

class landau.interpolate.CalphadSurface2DInterpolator(num_coeffs=5, terminal_sgte_order=4, coeff_poly_order=2)[source]

Bases: SurfaceInterpolator

CALPHAD-style 2-D surface: SGTE terminals + polynomial-in-T Redlich-Kister coefficients.

Fits the entropy-removed free energy

H(T, c) = (1-c) f0(T) + c (f0(T)+df(T)) + c(1-c) Σ_v L_v(T) (1-2c)^v

in two stages:

  1. Fit RedlichKister independently at each T in the incoming grid, extracting terminals f0(T), df(T) and interaction coefficients L_v(T).

  2. Fit the T-dependence: terminals via SGTE, interaction coefficients via PolyFit.

slice_at() evaluates these models at T and returns a RedlichKisterInterpolation with an analytic c-derivative, so the parent phase’s logit-Newton solver uses exact gradients (~1 ms solves).

Requires terminal concentrations at c=0 and c=1 in the training data.

coeff_poly_order: int = 2

Polynomial order in T for each interaction coefficient L_v(T).

fit(T, c, f)[source]

Fit a surface from flat arrays of (temperature, concentration, entropy-removed free energy).

Parameters:
  • T – 1-D array of temperatures.

  • c – 1-D array of concentrations (same length as T).

  • f – 1-D array of entropy-removed free energies H = f_raw + T·S(c).

Return type:

CalphadFittedSurface

Returns:

A FittedSurface whose slice_at() yields a 1-D Interpolation at any requested temperature.

num_coeffs: int = 5

Number of Redlich-Kister interaction coefficients L_0..L_{n-1}.

terminal_sgte_order: int = 4

SGTE order for the terminal free energies f0(T) and df(T).

class landau.interpolate.ConcentrationInterpolator[source]

Bases: Interpolator

class landau.interpolate.FittedSurface[source]

Bases: ABC

A fitted 2-D free-energy surface; slices to a 1-D Interpolation at fixed T.

abstractmethod slice_at(T)[source]

Return H(c) = f(T,c) + T·S(c) — the entropy-removed free energy — as a 1-D interpolation.

Return type:

Interpolation

landau.interpolate.G_calphad(T, pl, *p)[source]
class landau.interpolate.Interpolation[source]

Bases: ABC

A fitted 1-D interpolation f(x).

Subclasses must be callable. deriv() returns f' as another Interpolation; the default is a vectorised central difference, and subclasses with a closed form (polynomials, SGTE, Redlich-Kister) override it with the exact derivative.

deriv()[source]

Return f' as another Interpolation (numerical by default).

Return type:

Interpolation

class landau.interpolate.Interpolator[source]

Bases: ABC

This class acts as a factory for an interplation.

Call fit() to obtain a specific interpolation.

Implementations should be hashable and immutable to allow caching in the thermodynamics module.

abstractmethod fit(x, y)[source]
Return type:

Interpolation

class landau.interpolate.NumericalDerivative(func, step=1e-06)[source]

Bases: Interpolation

f' by a scale-aware central difference; the generic fallback derivative.

A single fixed-step central difference (vectorised, no Python-level loop) rather than scipy.differentiate.derivative(), which only exists from SciPy 1.15 and would lift the package’s scipy>=1.11.2 floor.

Parameters:
  • func (Callable) – the interpolation to differentiate.

  • step (float) – relative step; the absolute step is step * max(1, |x|).

class landau.interpolate.PolyFit(nparam, regularizer_strength=1e-08, enforce_curvature=False)[source]

Bases: TemperatureInterpolator, ConcentrationInterpolator

enforce_curvature: bool = False

Ensure that the interpolation has negative curvature as expected for thermodynamic potentials.

fit(x, y)[source]
nparam: int | Literal['auto']

Number of parameters, if “auto” fit a 10 parameter polynomial under L1 and discard parameters <1e-10, then refit.

regularizer_strength: float = 1e-08

Strength of L2-norm regularization.

class landau.interpolate.PolynomialInterpolation(poly)[source]

Bases: Interpolation

A polynomial fit with an analytic (polynomial) derivative.

Wraps a numpy.poly1d so the poly1d representation (and its descending-power coefficient order) stays an implementation detail; coefficients exposes them in ascending power order.

property coefficients: ndarray

c0 + c1*x + c2*x**2 + ....

Type:

Coefficients in ascending power order

deriv()[source]

The exact derivative polynomial.

Return type:

Interpolation

class landau.interpolate.RedlichKister(nparam)[source]

Bases: ConcentrationInterpolator

Fits the “enthalpic” part of a Redlich-Kister expansion, i.e. without the ideal configuration entropy.

fit(c, f)[source]

Beware: You need to manually remove the entropy if included in f.

nparam: int
class landau.interpolate.RedlichKisterInterpolation(df, f0, rk_parameters)[source]

Bases: Interpolation

deriv()[source]

The analytic d/dc from _eval_mix_derivative().

Return type:

Interpolation

df: float

Change in mixing “enthalpy” across composition range.

f0: float

Absolute “enthalpy” at concentration 0

rk_parameters: ndarray[float]

Redlich-Kister parameters.

class landau.interpolate.SGTE(nparam)[source]

Bases: TemperatureInterpolator

fit(x, y)[source]
nparam: int
class landau.interpolate.SGTEInterpolation(parameters)[source]

Bases: Interpolation

G(T) = T*ln(T)*pl + sum_i p_i*T^i with the analytic dG/dT.

deriv()[source]

The analytic dG/dT as a callable interpolation.

Return type:

Interpolation

parameters: tuple[float, ...]
class landau.interpolate.SoftplusFit(n_softplus=2, loss='soft_l1', max_nfev=100, f_scale=1.0)[source]

Bases: ConcentrationInterpolator, TemperatureInterpolator

Fits data using a sum of softplus functions for smooth, flexible interpolation.

The softplus function provides numerically stable, smooth approximations suitable for thermodynamic data. This interpolator uses multiple softplus terms to capture complex behavior.

fit() runs a local Levenberg–Marquardt / trust-region fit with analytical Jacobian.

f_scale: float = 1.0

Soft margin between inlier and outlier residuals for the robust loss (see scipy.optimize.least_squares()). Larger values weaken the influence of the robust loss — at the limit it behaves quadratically over the full residual range, equivalent to loss='linear'. Has no effect when loss='linear'.

fit(x, y)[source]

Local nonlinear least-squares fit with analytical Jacobian.

Return type:

Interpolation

loss: Literal['linear', 'soft_l1', 'huber', 'cauchy', 'arctan'] = 'soft_l1'

Loss function for robust fitting.

max_nfev: int = 100

Maximum number of function evaluations for the local fit.

n_softplus: int = 2

Number of softplus terms to fit.

class landau.interpolate.SoftplusFittedSurface(A, B, C, O, Tm, Ts, wm, ws, cm, cs, monotone_slope=False, signs=None, wmin=0.0)[source]

Bases: FittedSurface

Fitted surface for SoftplusSurface2DInterpolator.

Holds the per-term coefficient polynomials A / B / C (amplitude pre-activation / slope / knee) and the offset polynomial O, plus the T, 1/T and c normalisations. The amplitude, knee and offset polynomials are in normalised T; the slope ``B`` is a polynomial in normalised 1/T (well sharpness scales with inverse temperature). slice_at() evaluates them at T to produce a _SoftplusSlice with an analytic c-derivative; the amplitude passes through the softplus link a_i(T) = softplus(polyval(Tn, A[i])) so it is non-negative and the slice is convex in c.

Under SoftplusSurface2DInterpolator.monotone_slope, B holds the pre-link slope coefficients instead: the slope is b_i(T) = signs[i] * sum_k softplus(B[i,k]) * w^k with w = max(Wn - wmin, 0), so |b_i(T)| is non-increasing in T. signs and wmin are stored for slice_at(); both are unused for the default free polynomial slope.

slice_at(T)[source]

Return H(c) = f(T,c) + T·S(c) — the entropy-removed free energy — as a 1-D interpolation.

Return type:

_SoftplusSlice

class landau.interpolate.SoftplusSurface2DInterpolator(n_softplus=2, a_order=1, b_order=2, c_order=1, shared_knee=False, offset_order=2, loss='linear', f_scale=1.0, max_nfev=4000, method=None, monotone_slope=False)[source]

Bases: SurfaceInterpolator

Temperature-aware 2-D softplus surface f(T, c).

Fits the entropy-removed free energy

H(T, c) = off(T) + sum_i a_i(T) softplus( b_i(T) (cn + c_i(T)) )

as a single nonlinear least-squares problem with an analytic Jacobian. c carries the shape; T only modulates the coefficients. The amplitude a_i, knee c_i and offset polynomials are in normalised T; the slope ``b_i`` is a polynomial in normalised 1/T – the well sharpens as the temperature drops, so its inverse-temperature dependence is far better approximated by a low-order polynomial in 1/T than in T (a polynomial in T cannot track the strong sharpening at the cold end over a wide range). Set monotone_slope to additionally constrain |b_i(T)| to never reverse – a physically convex well only ever sharpens as it cools.

Convexity. The amplitudes are reparametrised through a non-negative link, a_i(T) = softplus(alpha_i(T)) with alpha_i(T) the fitted polynomial, so a_i(T) >= 0 at every temperature (training and extrapolated). Each term a_i * softplus(b_i*(cn + c_i)) is then convex in c (softplus is convex, affine argument, non-negative weight), so the fitted H(c) is convex by construction – no spurious miscibility gaps, matching the guarantee the bounded 1-D SoftplusFit gives. A plain unconstrained polynomial amplitude could dip negative between knots and break this.

Shared knee. Set shared_knee to tie every term to one knee polynomial c(T) instead of fitting n_softplus independent ones – the knee still moves with T, it’s just the same function for every term. This matches a single well built from an opposite-slope softplus pair, which is already what the per-slice seed assumes (see _smoothv_seed()); turning it on removes degrees of freedom the seed was not using anyway.

Avoiding bad minima. The coupled fit is multimodal – the sharp V-wells that motivate softplus sit in a flat-gradient landscape where a single optimisation easily stalls with the knee or branch slopes badly placed. Two slices – the coldest (sharpest, most structure) and the central – are each given a convex-well-aware per-slice seed (the knee taken from the data minimum, an opposite-slope softplus pair seeding the V; see _fit_slice()) rather than a generic quantile guess. The two seeds are then raced: each gets a short coupled polish and only the better-converging one is finished (a full solve restarted from that seed), so a bad basin is avoided without paying to converge both. Seeding cost is independent of how many temperatures are sampled (hundreds in practice).

Solver. By default the coupled solve picks its least-squares method by scipy version (see _LM_HAS_INTERNAL_SCALING); set method to 'lm' or 'trf' to override. scipy 1.16 switched the default variable scaling of least_squares(method='lm') to MINPACK’s internal scaling (mode=1), matching what plain leastsq always did – see gh-22790 and the bug it fixed, gh-19459 over on gh/scipy. Before 1.16, lm silently disabled that scaling (mode=2 with an all-ones diag) and converged poorly on the badly conditioned sharp-knee Jacobian. So on scipy >= 1.16 the solve defaults to lm (no per-iteration SVD, faster); on older scipy it defaults to the trust-region solver with an explicit x_scale='jac'. A robust loss always uses trf (lm supports only the linear loss). The residual and Jacobian are evaluated together (_model_and_jac()) so the per-term softplus/sigmoid are computed once per point. Most of the speed-up is from doing less redundant work – racing the seeds instead of converging both, capping the per-slice seed, and the shared evaluation.

Unlike CalphadSurface2DInterpolator there is no terminal-phase requirement, so this works for narrow intermetallics as well as solution phases; and unlike WhitneySurface2DInterpolator the fixed-T slice carries an analytic c-derivative, so the parent phase’s logit-Newton solver uses exact gradients.

a_order: int = 1

Polynomial order in T for the amplitudes a_i(T).

b_order: int = 2

Polynomial order in 1/T for the slopes b_i(T) (the strongest T-dependence).

c_order: int = 1

Polynomial order in T for the knee positions c_i(T).

f_scale: float = 1.0

Soft margin between inlier and outlier residuals for a robust loss; ignored when loss='linear'.

fit(T, c, f)[source]

Fit a surface from flat arrays of (temperature, concentration, entropy-removed free energy).

Parameters:
  • T – 1-D array of temperatures.

  • c – 1-D array of concentrations (same length as T).

  • f – 1-D array of entropy-removed free energies H = f_raw + T·S(c).

Return type:

SoftplusFittedSurface

Returns:

A FittedSurface whose slice_at() yields a 1-D Interpolation at any requested temperature.

loss: Literal['linear', 'soft_l1', 'huber', 'cauchy', 'arctan'] = 'linear'

Loss for the surface fit (see scipy.optimize.least_squares()). The default is linear – the well bottom is the signal, not an outlier, so a robust loss that down-weights large residuals is counter-productive here. Set a robust loss only for data with genuine outlier points.

max_nfev: int = 4000

Maximum function evaluations per start of the surface fit.

method: Literal['lm', 'trf'] | None = None

Least-squares solver for the coupled fit. None (default) picks by scipy version: 'lm' on scipy >= 1.16 (faster, no per-iteration SVD) and 'trf' on older scipy, where 'lm' lacks variable scaling and converges poorly on stiff data (see _LM_HAS_INTERNAL_SCALING). Force 'lm' or 'trf' to override the default; 'lm' requires loss='linear' and, on scipy < 1.16, still converges poorly on sharp-knee data.

monotone_slope: bool = False

Constrain every term’s slope magnitude |b_i(T)| to be non-increasing in T – a convex well only ever sharpens as it cools, so the fitted sharpness should never reverse. When True the slope is reparametrised as

b_i(T) = s_i * [ softplus(B_i0) + sum_{k>=1} softplus(B_ik) * w^k ], w = max((1/T) - min_train(1/T), 0),

with the branch sign s_i fixed from the per-slice seed. Each softplus(B_ik) >= 0 and w^k is non-decreasing in 1/T, so |b_i| is non-decreasing in 1/T (= non-increasing in T) by construction at every temperature, training and extrapolated – the max(., 0) holds the magnitude flat for T hotter than the training range rather than letting an even-power term turn it back up. Softplus (not sigmoid) is the positivity link: it is unbounded above and keeps the solve unconstrained, so the default lm fast path is retained and the slope is free to grow at the cold end (a bounded sigmoid link caps the slope and converges worse on sharp wells). The branch sign s_i is frozen from the per-slice seed and not refined by the coupled solve – reliable for the intended convex-V wells (the opposite-slope seed yields [+, -]), but a near-degenerate seed slice could lock in a wrong sign. The default False leaves b_i(T) a free polynomial in 1/T.

n_softplus: int = 2

Number of softplus terms.

offset_order: int = 2

Polynomial order in T for the vertical offset off(T).

shared_knee: bool = False

When True, every softplus term shares a single knee polynomial c(T) instead of fitting one independently per term – the knee can still vary with T, it is just the same function for all n_softplus terms. This is the constraint the smooth-V seed already assumes for an opposite-slope term pair (_smoothv_seed() seeds identical knees), so turning it on only removes degrees of freedom the seed was not using anyway. Reduces the knee parameter count from n_softplus * (c_order + 1) to c_order + 1.

class landau.interpolate.SplineFit(degree=3, smoothing=0.0)[source]

Bases: ConcentrationInterpolator

Fits data with a univariate B-spline of degree degree.

Wraps scipy.interpolate.UnivariateSpline. smoothing selects between strict interpolation (0.0, the default — the spline passes through every sample) and least-squares smoothing (> 0 — the spline may miss the samples by a total squared residual of smoothing in exchange for a smoother curve). None defers to scipy’s own default (s equal to the number of samples).

Outside [min(x), max(x)] the fit continues the boundary spline segment (scipy’s default ext=0): degree=1 extends linearly, while higher degrees follow the edge polynomial and can diverge quickly, so extrapolated values should be treated with care.

degree: int = 3

Spline degree k (3 = cubic). Clamped to len(x) - 1 so a fit with fewer samples than degree + 1 drops to the highest degree the data supports.

fit(x, y)[source]
smoothing: float | None = 0.0

Smoothing factor s for UnivariateSpline. 0.0 interpolates strictly; larger values trade fidelity for smoothness; None uses scipy’s default.

class landau.interpolate.StitchedFit(interpolating=SGTE(nparam=4), low=None, upp=PolyFit(nparam=2, regularizer_strength=1e-08, enforce_curvature=False), edge=10)[source]

Bases: TemperatureInterpolator

An interpolator with more control over the extrapolation regions.

edge: int = 10
fit(t, f)[source]
interpolating: TemperatureInterpolator = SGTE(nparam=4)
low: TemperatureInterpolator | None = None
upp: TemperatureInterpolator | None = PolyFit(nparam=2, regularizer_strength=1e-08, enforce_curvature=False)

How many samples near the edges to use to fit the extrapolating interpolator.

class landau.interpolate.SurfaceInterpolator[source]

Bases: ABC

Fits a 2-D free-energy surface f(T, c) from flat scattered (T, c, H) samples.

H = f_raw + T·S(c) is the entropy-removed free energy (the quantity the parent phase solver operates on). Implementations should be hashable (frozen dataclasses satisfy this) so they can be stored as fields in Surface2DInterpolatingPhase.

abstractmethod fit(T, c, f)[source]

Fit a surface from flat arrays of (temperature, concentration, entropy-removed free energy).

Parameters:
  • T (ArrayLike) – 1-D array of temperatures.

  • c (ArrayLike) – 1-D array of concentrations (same length as T).

  • f (ArrayLike) – 1-D array of entropy-removed free energies H = f_raw + T·S(c).

Return type:

FittedSurface

Returns:

A FittedSurface whose slice_at() yields a 1-D Interpolation at any requested temperature.

class landau.interpolate.TemperatureInterpolator[source]

Bases: Interpolator

class landau.interpolate.WhitneyFittedSurface(rbf)[source]

Bases: FittedSurface

Fitted surface for WhitneySurface2DInterpolator.

Wraps a WhitneyRBFInterpolator trained over the 2-D (T, c) space. slice_at() returns a closure at fixed T wrapped in _CallableInterpolation; the c-derivative is a numerical central difference (inherited from Interpolation).

slice_at(T)[source]

Return H(c) = f(T,c) + T·S(c) — the entropy-removed free energy — as a 1-D interpolation.

Return type:

_CallableInterpolation

class landau.interpolate.WhitneySurface2DInterpolator(kernel='thin_plate_spline', smoothing=0.001, degree=2, epsilon=1.0, grad_eps=0.0001)[source]

Bases: SurfaceInterpolator

2-D free-energy surface backed by a Whitney-extended RBF.

Trains a single WhitneyRBFInterpolator over all scattered (T, c, H) samples at once; slicing at a fixed T returns a closure with numerical c-derivatives (via the inherited deriv()).

Unlike CalphadSurface2DInterpolator, there is no terminal-phase requirement, so this works for narrow intermetallics as well as solution phases. The trade-off is slower per-(T, mu) solves (numerical derivative) and a need for temperature_range to span the solve grid to keep query points inside the RBF’s convex hull (fast batched path).

degree: int = 2
epsilon: float = 1.0
fit(T, c, f)[source]

Fit a surface from flat arrays of (temperature, concentration, entropy-removed free energy).

Parameters:
  • T – 1-D array of temperatures.

  • c – 1-D array of concentrations (same length as T).

  • f – 1-D array of entropy-removed free energies H = f_raw + T·S(c).

Return type:

WhitneyFittedSurface

Returns:

A FittedSurface whose slice_at() yields a 1-D Interpolation at any requested temperature.

grad_eps: float = 0.0001
kernel: str = 'thin_plate_spline'
smoothing: float = 0.001
class landau.interpolate.WhitneyTemperatureInterpolator(kernel='thin_plate_spline', smoothing=0.0, degree=2, epsilon=1.0, grad_eps=0.0001)[source]

Bases: TemperatureInterpolator

A TemperatureInterpolator backed by WhitneyRBFInterpolator.

Fits a 1-D Whitney RBF on temperature data and returns a callable that extrapolates smoothly outside the training range.

Warning

Fitting this interpolator can be very slow, especially when taking data e.g. from calphy as is. It’s better to pre-smooth raw free energies with a plain interpolator or sub set data and then feeding it to this class.

Parameters:
  • kernel (str)

  • smoothing (float)

  • degree (int)

  • epsilon (float)

  • grad_eps (float)

degree: int = 2
epsilon: float = 1.0
fit(T, y)[source]

Fit the interpolator.

Parameters:
  • T ((N,) array-like — temperature values)

  • y ((N,) array-like — target values)

Returns:

callable

Return type:

(M,) ndarray -> (M,) ndarray

grad_eps: float = 0.0001
kernel: str = 'thin_plate_spline'
smoothing: float = 0.0