Qu

Measurement & instrumentation

Impedance (rLKK)

regularized linear Kramers-Kronig (rLKK) validation and reconstruction of an impedance spectrum.

catalog/qu_impedance_rlkk.qu

rLKK checks whether a measured impedance spectrum is itself physically consistent (linear, causal, time-invariant) by fitting a distribution of relaxation times (DRT) and comparing the reconstructed spectrum back against the raw data -- independent of any assumed equivalent circuit. This demo builds a synthetic Randles-circuit spectrum (Rs + Rct parallel Cdl), validates the clean data, corrupts a single point, and shows rLKK flagging exactly that corruption.

What it exercises

How it works

Synthesize a Randles-circuit impedance spectrum

j = 0 + 1i
Rs = 0.2
Rct = 0.01
Cdl = 100e-6

randles(freqs, Rs, Rct, Cdl) := Rs + Rct ./ (1 + j .* (2*pi*freqs) .* Rct .* Cdl)

freqs = logspace(0, 4, 40)
Z = randles(freqs, Rs, Rct, Cdl)

Reconstruct via rLKK and check the fit quality

fit = rlkk_reconstruct(Z, freqs, lambda=1e-6)
recon_err = max(abs(fit.z - Z))
print("rLKK reconstruction of clean data: max |error| = {recon_err:.2e}")

Validate the clean spectrum, then corrupt one point and validate again

clean_check = rlkk_validate(Z, freqs, lambda=1e-4, threshold=2.0)
print("clean spectrum: valid={clean_check.valid}, max residual={clean_check.max_residual:.3f}%")

mask = ones(length(freqs))
mask[20] = 1.5
Z_corrupted = Z .* mask
corrupted_check = rlkk_validate(Z_corrupted, freqs, lambda=1e-4, threshold=2.0)
print("corrupted spectrum (point 20 off by 50%): valid={corrupted_check.valid}, max residual={corrupted_check.max_residual:.3f}%")

Nyquist plot: clean vs corrupted

nyquist(Z)
nyquist(Z_corrupted)
legend()
title("Randles impedance: clean vs corrupted")
show plot

Results

printed
rLKK reconstruction of clean data: max |error| = 3.17e-9
clean spectrum: valid=true, max residual=0.000%
corrupted spectrum (point 20 off by 50%): valid=false, max residual=29.729%
  note: legend() had nothing to show -- label the series first, e.g. `plot(x, y, label="model")`, or pass the labels directly: `legend("model", "data")`
-3.0 -2.0 -1.0 0 1.0 2.0 3.0 ×10−2 0.210 0.220 0.230 0.240 0.250 0.260 0.270 0.280 0.290 0.300 0.310 0.320 x=0.21, y=$6.3\times10^{-8}$ x=0.21, y=$8\times10^{-8}$ x=0.21, y=$10^{-7}$ x=0.21, y=$1.3\times10^{-7}$ x=0.21, y=$1.6\times10^{-7}$ x=0.21, y=$2\times10^{-7}$ x=0.21, y=$2.6\times10^{-7}$ x=0.21, y=$3.3\times10^{-7}$ x=0.21, y=$4.2\times10^{-7}$ x=0.21, y=$5.3\times10^{-7}$ x=0.21, y=$6.7\times10^{-7}$ x=0.21, y=$8.4\times10^{-7}$ x=0.21, y=$1.1\times10^{-6}$ x=0.21, y=$1.4\times10^{-6}$ x=0.21, y=$1.7\times10^{-6}$ x=0.21, y=$2.2\times10^{-6}$ x=0.21, y=$2.7\times10^{-6}$ x=0.21, y=$3.5\times10^{-6}$ x=0.21, y=$4.4\times10^{-6}$ x=0.21, y=$5.6\times10^{-6}$ x=0.21, y=$7.1\times10^{-6}$ x=0.21, y=$9\times10^{-6}$ x=0.21, y=$1.1\times10^{-5}$ x=0.21, y=$1.4\times10^{-5}$ x=0.21, y=$1.8\times10^{-5}$ x=0.21, y=$2.3\times10^{-5}$ x=0.21, y=$2.9\times10^{-5}$ x=0.21, y=$3.7\times10^{-5}$ x=0.21, y=$4.7\times10^{-5}$ x=0.21, y=$5.9\times10^{-5}$ x=0.21, y=$7.5\times10^{-5}$ x=0.21, y=$9.5\times10^{-5}$ x=0.21, y=$1.2\times10^{-4}$ x=0.21, y=$1.5\times10^{-4}$ x=0.21, y=$1.9\times10^{-4}$ x=0.21, y=$2.4\times10^{-4}$ x=0.21, y=$3.1\times10^{-4}$ x=0.21, y=$3.9\times10^{-4}$ x=0.21, y=$4.9\times10^{-4}$ x=0.21, y=$6.3\times10^{-4}$ x=0.21, y=$6.3\times10^{-8}$ x=0.21, y=$8\times10^{-8}$ x=0.21, y=$10^{-7}$ x=0.21, y=$1.3\times10^{-7}$ x=0.21, y=$1.6\times10^{-7}$ x=0.21, y=$2\times10^{-7}$ x=0.21, y=$2.6\times10^{-7}$ x=0.21, y=$3.3\times10^{-7}$ x=0.21, y=$4.2\times10^{-7}$ x=0.21, y=$5.3\times10^{-7}$ x=0.21, y=$6.7\times10^{-7}$ x=0.21, y=$8.4\times10^{-7}$ x=0.21, y=$1.1\times10^{-6}$ x=0.21, y=$1.4\times10^{-6}$ x=0.21, y=$1.7\times10^{-6}$ x=0.21, y=$2.2\times10^{-6}$ x=0.21, y=$2.7\times10^{-6}$ x=0.21, y=$3.5\times10^{-6}$ x=0.21, y=$4.4\times10^{-6}$ x=0.21, y=$5.6\times10^{-6}$ x=0.315, y=$1.1\times10^{-5}$ x=0.21, y=$9\times10^{-6}$ x=0.21, y=$1.1\times10^{-5}$ x=0.21, y=$1.4\times10^{-5}$ x=0.21, y=$1.8\times10^{-5}$ x=0.21, y=$2.3\times10^{-5}$ x=0.21, y=$2.9\times10^{-5}$ x=0.21, y=$3.7\times10^{-5}$ x=0.21, y=$4.7\times10^{-5}$ x=0.21, y=$5.9\times10^{-5}$ x=0.21, y=$7.5\times10^{-5}$ x=0.21, y=$9.5\times10^{-5}$ x=0.21, y=$1.2\times10^{-4}$ x=0.21, y=$1.5\times10^{-4}$ x=0.21, y=$1.9\times10^{-4}$ x=0.21, y=$2.4\times10^{-4}$ x=0.21, y=$3.1\times10^{-4}$ x=0.21, y=$3.9\times10^{-4}$ x=0.21, y=$4.9\times10^{-4}$ x=0.21, y=$6.3\times10^{-4}$ Randles impedance: clean vs corrupted
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_impedance_rlkk.qu

# qu_impedance_rlkk.qu -- regularized linear Kramers-Kronig (rLKK)

# validation and reconstruction of an impedance spectrum.

#

# rLKK checks whether a measured impedance spectrum is itself physically

# consistent (linear, causal, time-invariant) by fitting a distribution of

# relaxation times (DRT) and comparing the reconstructed spectrum back

# against the raw data -- independent of any assumed equivalent circuit.

# This demo builds a synthetic Randles-circuit spectrum (Rs + Rct parallel

# Cdl), validates the clean data, corrupts a single point, and shows rLKK

# flagging exactly that corruption.

#

# Constructs exercised (all real, verified-working builtins):

#   logspace                                     -- log-spaced frequency sweep

#   complex arithmetic (0 + 1i, ./ , .*)          -- the Randles impedance model

#   rlkk_reconstruct(Z, freqs, lambda=)           -- Model with .z / .gamma

#   rlkk_validate(Z, freqs, lambda=, threshold=)  -- Model with .valid/.residuals/.max_residual

#   nyquist(Z)                                    -- the standard EIS plot


backend auto

#%% Synthesize a Randles-circuit impedance spectrum

j = 0 + 1i
Rs = 0.2
Rct = 0.01
Cdl = 100e-6

randles(freqs, Rs, Rct, Cdl) := Rs + Rct ./ (1 + j .* (2*pi*freqs) .* Rct .* Cdl)

freqs = logspace(0, 4, 40)
Z = randles(freqs, Rs, Rct, Cdl)

#%% Reconstruct via rLKK and check the fit quality

fit = rlkk_reconstruct(Z, freqs, lambda=1e-6)
recon_err = max(abs(fit.z - Z))
print("rLKK reconstruction of clean data: max |error| = {recon_err:.2e}")

#%% Validate the clean spectrum, then corrupt one point and validate again

clean_check = rlkk_validate(Z, freqs, lambda=1e-4, threshold=2.0)
print("clean spectrum: valid={clean_check.valid}, max residual={clean_check.max_residual:.3f}%")

mask = ones(length(freqs))
mask[20] = 1.5
Z_corrupted = Z .* mask
corrupted_check = rlkk_validate(Z_corrupted, freqs, lambda=1e-4, threshold=2.0)
print("corrupted spectrum (point 20 off by 50%): valid={corrupted_check.valid}, max residual={corrupted_check.max_residual:.3f}%")

#%% Nyquist plot: clean vs corrupted

nyquist(Z)
nyquist(Z_corrupted)
legend()
title("Randles impedance: clean vs corrupted")
show plot