Free Energy Principle (FEP) on Quantum Substrate¶
1. Mathematical Formalism¶
Variational Free Energy¶
The Free Energy Principle (Friston 2010) defines variational free energy as:
Since \(D_\text{KL} \geq 0\), free energy upper-bounds surprise: \(F \geq -\log p(x)\). Minimising \(F\) thus minimises surprise (self-information of observations).
For Gaussian distributions:
the free energy decomposes as:
where \(\Pi\) is prior precision, \(\Gamma\) is sensory precision, and \(g(\mu)\) is the generative model.
KL Divergence Between Gaussians¶
This is exact (not an approximation) for multivariate Gaussians. The
implementation uses numpy.linalg.inv and numpy.linalg.slogdet for
numerical stability.
Free Energy Gradient (Belief Dynamics)¶
The gradient drives belief updates via gradient descent:
where \(J = \partial g / \partial \mu\) is the Jacobian of the generative model. With the identity generative model (\(g(\mu) = \mu\), \(J = I\)):
At the MAP point: \(\mu_\text{MAP} = (\Pi + \Gamma)^{-1}\Gamma x\). For \(\Pi = \Gamma = I\): \(\mu_\text{MAP} = x/2\).
SCPN Mapping¶
The FEP maps onto the SCPN hierarchy as follows:
| FEP Concept | SCPN Entity | Module |
|---|---|---|
| Belief mean \(\mu\) | Oscillator phases \(\theta_i\) | phase/phase_vqe.py |
| Prior precision \(\Pi\) | K_nm coupling matrix, when positive definite | bridge/knm_hamiltonian.py |
| Sensory precision \(\Gamma\) | Measurement confidence | Identity (simulation) |
| Generative model \(g\) | Forward prediction | Coupling-weighted mean |
| Prediction error \(\varepsilon\) | Phase mismatch | Hierarchical PC |
| Free energy \(F\) | Total prediction cost | variational_free_energy() |
ELBO Interpretation¶
The Evidence Lower BOund (ELBO) is \(-F\):
Maximising ELBO is equivalent to: 1. Maximising the model evidence \(p(x)\) — the model explains the data well 2. Minimising the posterior gap \(D_\text{KL}[q \| p(z|x)]\) — the approximate posterior \(q\) is close to the true posterior
In the SCPN context: high ELBO means the oscillator phases (beliefs) are well-matched to the observed phases, and the K_nm prior is consistent with the data.
MAP Point Derivation¶
Setting \(\partial F / \partial \mu = 0\) with identity generative model:
For \(\Pi = K_\text{nm}\) and \(\Gamma = I\):
This is a regularised least-squares solution: the K_nm prior pulls the beliefs toward zero (prior mean), while observations pull toward \(x\). Stronger coupling (larger K_nm eigenvalues) means stronger regularisation — beliefs deviate less from the prior.
Predictive Coding Algorithm¶
Algorithm: Hierarchical Predictive Coding on SCPN
Input: observations x, initial beliefs μ, coupling K, learning_rate lr, n_steps
Output: converged beliefs μ*, free energy trajectory F[t]
1. For t = 1 to n_steps:
a. For each layer i:
Π_i ← Σ_j K[i,j] (local precision)
x̂_i ← Σ_j K[i,j] μ_j / Π_i (coupling-weighted prediction)
ε_i ← Π_i (x_i − x̂_i) (precision-weighted error)
b. Compute gradient:
∂F/∂μ = K_reg μ − Γ(x − μ) (Rust-accelerated)
c. Update beliefs:
μ ← μ − lr × ∂F/∂μ
d. Record F[t] = complexity(μ) + accuracy(μ, x)
2. Return μ, F
The algorithm is equivalent to gradient descent on the variational free energy surface. Convergence is guaranteed for convex F (which holds when K_nm is positive semi-definite and the generative model is the identity).
Hierarchical Predictive Coding¶
For each SCPN layer \(i\), the prediction from connected layers is:
The precision-weighted prediction error is:
Layers with stronger K_nm coupling (higher precision) produce larger prediction errors — they are more "surprised" by mismatches. This maps to the predictive coding hierarchy (Friston 2005, Bastos et al. 2012) where higher layers predict lower layers' activity.
2. Theoretical Context¶
Why FEP in the SCPN?¶
The SCPN dynamics can be cast as optimisation across the hierarchy of layers. The FEP provides the mathematical framework: the UPDE dynamics (Kuramoto coupling) can be reinterpreted as belief updating under the FEP — oscillator phases are sufficient statistics, and synchronisation is free energy minimisation.
This is not a metaphor. The Kuramoto model with heterogeneous frequencies and distance-dependent coupling IS a predictive coding scheme where: - Coupling K_nm = precision (how much to trust the neighbour) - Phase difference = prediction error - Synchronisation = free energy minimum
Kuramoto-FEP Equivalence¶
The Kuramoto model with coupling \(K_{ij}\) and natural frequencies \(\omega_i\):
can be rewritten as gradient descent on an energy function:
This is equivalent to the FEP gradient descent \(d\mu/dt = -\partial F / \partial \mu\) where:
- \(\mu = \theta\) (phases are beliefs)
- The prior energy is \(\frac{1}{2}\mu^\top K \mu\) (quadratic approximation of the cosine coupling near synchronisation)
- The accuracy term encodes the observation that each oscillator's phase should match its natural frequency: \(\frac{1}{2}(\omega - \dot{\theta})^\top \Gamma (\omega - \dot{\theta})\)
The linearised Kuramoto around the synchronised state IS the FEP gradient with K_nm as prior precision. The nonlinear \(\sin\) terms provide additional structure not captured by the Gaussian FEP, but near synchronisation (small phase differences), the equivalence is exact.
Precision-Weighted Message Passing¶
In biological predictive coding (Bastos et al. 2012), cortical layers communicate via two message types:
| Message | Direction | Content | SCPN Equivalent |
|---|---|---|---|
| Top-down prediction | Higher → Lower | \(\hat{x}_i = g(\mu_{i+1})\) | K_nm-weighted mean |
| Bottom-up error | Lower → Higher | \(\varepsilon_i = \Pi_i(x_i - \hat{x}_i)\) | Precision-weighted mismatch |
The precision \(\Pi_i = \sum_j K_{ij}\) determines how much weight each layer's error signal carries. Layers with strong coupling (high precision) dominate the belief update — their errors are treated as more informative.
This maps to cortical anatomy where layer-specific precision corresponds to neuromodulatory gain (dopamine, acetylcholine). In the SCPN, the analogous modulator is the K_nm coupling strength.
Connection to Active Inference¶
Active inference extends the FEP to include action: the system not only updates beliefs but also acts on the world to reduce prediction errors. In the SCPN context, this maps to: - Perception: updating phase beliefs from quantum measurements - Action: modulating coupling strengths (DynamicCouplingEngine)
This module implements the perception side. The action side is handled
by qsnn/dynamic_coupling.py (Hebbian learning from XY correlations).
What This Module Does NOT Claim¶
- It does NOT claim that the brain literally performs quantum FEP
- It does NOT derive the FEP from quantum mechanics
- The mapping SCPN → FEP is a formal analogy, not a proof of identity
- The identity generative model (\(g(\mu) = \mu\)) is a simplification — the real SCPN generative model would be the full UPDE
Relationship to Other SCPN Modules¶
| Module | FEP Role | Connection |
|---|---|---|
phase/phase_vqe.py |
Provides oscillator phases (beliefs) | VQE output → FEP input |
bridge/knm_hamiltonian.py |
Provides prior precision | K_nm → Π matrix |
qsnn/dynamic_coupling.py |
Active inference (action) | Hebbian update ← XY correlations |
analysis/sync_witness.py |
Measures synchronisation | Sync order ↔ low free energy |
qec/multiscale_qec.py |
Error correction | QEC protects belief states |
The FEP provides the theoretical glue: synchronisation (low F) is the system's way of minimising surprise about its environment.
3. Pipeline Position¶
bridge/knm_hamiltonian.py → K_nm (prior precision Π)
↓
fep/variational_free_energy.py
├─ _complexity_term() → KL[q || prior]
├─ _accuracy_term() → prediction error energy
├─ variational_free_energy() → FreeEnergyResult
├─ free_energy_gradient() → ∂F/∂μ (Rust-accelerated)
└─ evidence_lower_bound() → ELBO = −F
↓
fep/predictive_coding.py
├─ hierarchical_prediction_error() → ε_i (Rust-accelerated)
└─ predictive_coding_step() → PredictiveCodingResult
↓
phase/phase_vqe.py → updated beliefs → circuit parameters
Inputs: oscillator phases (beliefs), observed phases, K_nm coupling.
Outputs: free energy decomposition, gradient, updated beliefs.
4. Features¶
- Variational free energy: exact Gaussian computation with complexity/accuracy decomposition
- KL divergence: exact multivariate Gaussian KL via matrix inversion and log-determinant
- Evidence lower bound (ELBO): \(-F\) for variational inference optimisation
- Free energy gradient: analytical \(\partial F / \partial \mu\) for gradient descent belief update
- Hierarchical predictive coding: coupling-weighted predictions across SCPN layers
- Predictive coding step: single gradient descent update with error tracking
- Rust acceleration: gradient and prediction error use Rust engine (110 μs and 40 μs per call)
- Custom generative models: supports arbitrary \(g(\mu)\) and Jacobian \(J(\mu)\) with Python fallback
- Convergence tracking: prediction error norm monitored across PC iterations
5. Usage Examples¶
Compute Free Energy¶
import numpy as np
from scpn_quantum_control.bridge.knm_hamiltonian import build_knm_paper27
from scpn_quantum_control.fep.variational_free_energy import variational_free_energy
K = build_knm_paper27()
n = K.shape[0] # 16
mu = np.zeros(n) # initial belief: all phases zero
sigma = 0.1 * np.eye(n) # belief covariance
x = np.random.default_rng(42).standard_normal(n) * 0.1 # observations
result = variational_free_energy(mu, sigma, x, K)
print(f"F = {result.free_energy:.4f}")
print(f" complexity = {result.complexity:.4f}")
print(f" accuracy = {result.accuracy:.4f}")
print(f" ELBO = {result.elbo:.4f}")
Predictive Coding Convergence¶
from scpn_quantum_control.fep.predictive_coding import predictive_coding_step
K = build_knm_paper27(L=4)
x = np.array([0.5, 0.3, -0.2, 0.1])
beliefs = np.zeros(4)
for step in range(100):
result = predictive_coding_step(x, beliefs, K, learning_rate=0.001)
beliefs = result.beliefs
if step % 20 == 0:
print(f"Step {step}: F={result.free_energy:.4f}, "
f"||ε||={result.total_error_norm:.4f}")
Custom Generative Model¶
from scpn_quantum_control.fep.variational_free_energy import free_energy_gradient
# Nonlinear generative model: g(μ) = sin(μ)
def g(mu):
return np.sin(mu)
def J(mu):
return np.diag(np.cos(mu))
grad = free_energy_gradient(
mu=np.zeros(4), sigma=np.eye(4),
x_observed=np.ones(4) * 0.5, K_precision=np.eye(4),
generative_fn=g, generative_jac=J,
)
Gradient Descent on Free Energy¶
from scpn_quantum_control.fep.variational_free_energy import (
variational_free_energy, free_energy_gradient,
)
K = build_knm_paper27()
n = K.shape[0]
mu = np.zeros(n)
sigma = 0.1 * np.eye(n)
x = np.random.default_rng(42).standard_normal(n) * 0.1
# Manual gradient descent (equivalent to predictive_coding_step)
lr = 0.001
for step in range(200):
grad = free_energy_gradient(mu, sigma, x, K)
mu = mu - lr * grad
result = variational_free_energy(mu, sigma, x, K)
print(f"Final F = {result.free_energy:.6f}")
print(f"Final ||grad|| = {np.linalg.norm(grad):.6f}")
KL Divergence Verification¶
from scpn_quantum_control.fep.variational_free_energy import kl_divergence_gaussian
# KL[q || p] = 0 when q = p
mu = np.array([1.0, 2.0, 3.0])
sigma = np.eye(3) * 0.5
kl = kl_divergence_gaussian(mu, sigma, mu, sigma)
assert abs(kl) < 1e-10, "KL[q||q] must be 0"
# KL is always non-negative (Gibbs' inequality)
for _ in range(100):
mu_q = np.random.randn(3)
mu_p = np.random.randn(3)
A = np.random.randn(3, 3)
sigma_q = A @ A.T + 0.1 * np.eye(3)
B = np.random.randn(3, 3)
sigma_p = B @ B.T + 0.1 * np.eye(3)
assert kl_divergence_gaussian(mu_q, sigma_q, mu_p, sigma_p) >= -1e-10
Both covariances must be finite, symmetric and positive definite, and the two
means must be finite and share their dimension. Anything else is rejected with a
ValueError naming the offending argument, rather than producing a number that
cannot be a divergence — a negative-definite covariance has no log-determinant
of the required sign, and silently taking its magnitude yields a negative "KL".
The divergence is evaluated through Cholesky factors of both covariances: no
explicit inverse is formed, and log|Σ| is taken as 2 Σ log L_ii, which needs
no sign correction. Accuracy therefore follows the conditioning of the
covariances; KL[p || p] returns a residual bounded by machine epsilon times
the condition number rather than exactly zero, and that residual is never
clamped. Where a precision matrix is inverted to form a prior covariance, the
ridge added first is the documented constant PRECISION_RIDGE, and the
regularised matrix must still be positive definite.
6. Technical Reference¶
Classes¶
FreeEnergyResult¶
| Field | Type | Description |
|---|---|---|
free_energy |
float |
\(F = \text{complexity} + \text{accuracy}\) |
complexity |
float |
\(D_\text{KL}[q \| \text{prior}]\) |
accuracy |
float |
\(\frac{1}{2}(x - g(\mu))^\top \Gamma (x - g(\mu))\) |
elbo |
float |
\(-F\) |
surprise_bound |
float |
\(F\) (upper bound on \(-\log p(x)\)) |
PredictiveCodingResult¶
| Field | Type | Description |
|---|---|---|
prediction_errors |
ndarray |
\(\varepsilon_i\) per layer |
beliefs |
ndarray |
Updated \(\mu_i\) |
free_energy |
float |
\(F\) after update |
total_error_norm |
float |
\(\|\varepsilon\|\) |
Functions¶
variational_free_energy(mu, sigma, x_observed, K_precision, sensory_precision, generative_fn)¶
Compute \(F\) with full decomposition. Returns FreeEnergyResult.
kl_divergence_gaussian(mu_q, sigma_q, mu_p, sigma_p)¶
Exact KL divergence between multivariate Gaussians.
free_energy_gradient(mu, sigma, x_observed, K_precision, sensory_precision, generative_fn, generative_jac)¶
Analytical gradient \(\partial F / \partial \mu\). Rust-accelerated for identity generative model. A custom model and its Jacobian are one contract and must be supplied together; see below.
hierarchical_prediction_error(observations, beliefs, K)¶
Precision-weighted prediction errors across SCPN layers. Rust-accelerated.
predictive_coding_step(observations, beliefs, K, learning_rate, sigma)¶
Single gradient descent step. Returns PredictiveCodingResult.
| Parameter | Type | Default | Description |
|---|---|---|---|
observations |
ndarray |
required | Measured phases \(x_i\) |
beliefs |
ndarray |
required | Current beliefs \(\mu_i\) |
K |
ndarray |
required | K_nm coupling, used for the prediction errors |
learning_rate |
float |
0.01 |
Gradient step size |
sigma |
ndarray \| None |
0.1 × I |
Belief covariance |
prior_precision |
ndarray \| None |
K |
Prior precision \(\Pi\), positive definite |
The coupling matrix and the prior precision are separate objects. A K_nm
matrix with a zero diagonal is a valid coupling — a layer is not coupled to
itself — but it is not a precision, because the prior covariance \(\Pi^{-1}\)
then does not exist. Passing K for both roles is correct only when K is
positive definite; otherwise supply prior_precision, for example the graph
Laplacian of K with a positive shift, which is what the bundled FEP
application plugin uses.
variational_free_energy — Full Signature¶
| Parameter | Type | Default | Description |
|---|---|---|---|
mu |
ndarray |
required | Belief mean |
sigma |
ndarray |
required | Belief covariance (\(n × n\)) |
x_observed |
ndarray |
required | Observed data |
K_precision |
ndarray |
required | Prior precision (K_nm) |
sensory_precision |
ndarray \| None |
I |
Likelihood precision \(\Gamma\) |
generative_fn |
Callable \| None |
identity | Forward model \(g(\mu)\), returning a finite length-\(m\) vector |
free_energy_gradient — Full Signature¶
| Parameter | Type | Default | Description |
|---|---|---|---|
mu |
ndarray |
required | Belief mean |
sigma |
ndarray |
required | Belief covariance |
x_observed |
ndarray |
required | Observed data |
K_precision |
ndarray |
required | Prior precision |
sensory_precision |
ndarray \| None |
I |
Likelihood precision |
generative_fn |
Callable \| None |
identity | Forward model; requires generative_jac |
generative_jac |
Callable \| None |
— | Jacobian \(\partial g / \partial \mu\); requires generative_fn |
When generative_fn and generative_jac are both None, uses Rust engine.
Arguments are validated before that dispatch, so the Python and Rust tiers
admit the same domain.
A model and its Jacobian are supplied together or not at all. Neither is
inferred from the other: substituting the identity for an absent Jacobian
returns the gradient of a different model — for \(g(\mu) = 2\mu\) at \(\mu = 0.4\)
with \(x = 0\) and \(K = \Gamma = I\) it returns \(1.2\) where the derivative of \(F\)
is \(2.0\) — and a Jacobian without its model differentiates the identity
prediction. Either half alone raises ValueError before the callables run, so
no belief state is updated from an incomplete contract.
Models need not be square. For \(g: \mathbb{R}^n \to \mathbb{R}^m\) the Jacobian
is \((m, n)\), x_observed and the model output have length \(m\), the sensory
precision is \((m, m)\), and the gradient has length \(n\). The model must return a
finite one-dimensional vector of length \(m\): a scalar or an \((m, 1)\) column is
rejected rather than broadcast against the observation.
sigma is accepted for symmetry with variational_free_energy and does not
enter the gradient; the terms of \(F\) carrying the belief covariance are constant
in \(\mu\).
Rust Engine API¶
Available via import scpn_quantum_engine:
free_energy_gradient_rust(mu, x_observed, k_precision, sensory_precision, ridge)¶
Computes \(\partial F / \partial \mu\) for identity generative model.
All parameters are numpy arrays. Returns ndarray[f64].
The dimension \(n\) comes from mu. x_observed must have length \(n\), and both
matrices must be exactly \(n \times n\); all four arguments and ridge must be
finite. Violations raise ValueError naming the argument. Non-contiguous views
are read through their strides and are not rejected.
Symmetry is not required here: the gradient uses k_precision as a general
linear map. The free-energy export, which needs a log-determinant, does require
it.
hierarchical_prediction_error_rust(observations, beliefs, k)¶
Computes precision-weighted prediction errors. Returns ndarray[f64].
variational_free_energy_rust(mu, x_observed, k_precision, sensory_precision, sigma_diag, ridge)¶
Computes \((F, \text{complexity}, \text{accuracy})\) tuple for diagonal \(\Sigma\).
Same length, shape and finiteness contract as the gradient export, plus
sigma_diag positive and finite. k_precision must additionally be symmetric
within \(10^{-10}\) — the same absolute tolerance as COVARIANCE_SYMMETRY_ATOL
in the Python tier — and positive definite once the ridge is added. The
symmetry check exists because the log-determinant is taken from a Cholesky
factor, which reads only the lower triangle: without it, an asymmetric matrix
returns the determinant of its symmetrised triangle instead of an error.
Both exports validate the whole contract before any indexing. Before this, an
undersized argument reached an ndarray bounds panic that crosses PyO3 as
pyo3_runtime.PanicException, a BaseException that ordinary Python error
handling does not catch, and a merely mis-shaped argument — a \(2 \times 3\) read
as \(2 \times 2\) — produced a silently wrong number. The contract is exercised
against a freshly built extension by the fep-ffi-contract-quality job in
.github/workflows/ci-native-integration.yml.
Internal Functions¶
| Function | Module | Description |
|---|---|---|
_complexity_term(mu, sigma, K) |
variational_free_energy.py |
KL divergence to prior |
_accuracy_term(mu, x, Γ, g) |
variational_free_energy.py |
Prediction error energy |
Numerical Considerations¶
- Ridge regularisation: K_nm + \(10^{-10} I\) ensures invertibility. Without this, zero eigenvalues in K_nm cause singular matrix errors.
- Log-determinant: taken from a Cholesky factor as \(2 \sum_i \log L_{ii}\), which needs no separate sign and cannot silently return the magnitude of a negative determinant.
- Gradient at MAP: residual \(\|\nabla F\| \sim 10^{-10}\) due to ridge regularisation. This is not a bug — it is the price of numerical stability.
- Convergence criterion: PC step uses fixed learning rate. Adaptive step sizes (e.g., line search) would improve convergence but add complexity. The current implementation prioritises simplicity.
- Custom generative models: when
generative_fnis not None, Rust engine is bypassed. The Python path supports arbitrary callables but is ~10× slower for the gradient computation. The gradient requires the pairedgenerative_jac; it is not approximated, because a numerical derivative would change the accuracy of the result without saying so. A caller who wants one builds it explicitly and passes it as the pair:generative_jac=lambda mu: jacobian(g, mu)withfrom scpn_quantum_control.diff import jacobian, whose return shape \((m, n)\) is already the one this contract requires. Against the analytic Jacobian of \(g = \sin\) on three coordinates the two agree to \(4×10^{-11}\).
7. Performance Benchmarks¶
Measured on Intel i5-11600K, Python 3.12, \(n = 16\) oscillators.
| Function | Time | Engine |
|---|---|---|
variational_free_energy |
88 μs | Python (numpy linalg) |
free_energy_gradient |
110 μs | Rust |
hierarchical_prediction_error |
40 μs | Rust |
predictive_coding_step |
260 μs | Rust (inner) |
kl_divergence_gaussian |
~50 μs | Python (numpy linalg) |
These figures predate the argument validation that free_energy_gradient and
variational_free_energy now perform before dispatch, and the Cholesky
factorisation that replaced the explicit inverse in kl_divergence_gaussian.
They have not been re-measured on an isolated host, so they are retained as the
last qualified measurement rather than refreshed with a loaded-workstation
number. The validation is deliberate cost: it is what keeps an inadmissible
argument from reaching either the Python or the native tier.
Convergence Rate¶
For \(n = 4\), \(\text{lr} = 0.001\), K = build_knm_paper27(L=4), starting from \(\mu = 0\):
- 50 steps: error norm decreases (verified in test)
- 200 steps: belief change \(< 0.01\) per step (convergence verified)
Scaling with System Size¶
| \(n\) | variational_free_energy |
free_energy_gradient |
predictive_coding_step |
|---|---|---|---|
| 4 | ~20 μs | ~30 μs | ~60 μs |
| 8 | ~40 μs | ~50 μs | ~110 μs |
| 16 | ~88 μs | ~110 μs | ~260 μs |
Scaling is \(O(n^2)\) for gradient (matrix-vector) and \(O(n^3)\) for
variational_free_energy (matrix inversion in KL computation).
Test Coverage¶
Two owners cover this surface:
| Test file | Scope |
|---|---|
tests/test_fep.py |
Free energy, KL contract, predictive coding, performance budgets |
tests/test_fep_generative_model_contract.py |
Generative model and Jacobian contract for the gradient |
fep/predictive_coding.py carries an exact 100% statement and branch coverage
gate, measured from tests/test_fep.py alone; see
tools/predictive_coding_quality_gates.py. fep/variational_free_energy.py is
at 100% statement and branch coverage across the two owners together.
8. Citations¶
-
Friston, K. "The free-energy principle: a unified brain theory?" Nature Reviews Neuroscience 11, 127–138 (2010). DOI: 10.1038/nrn2787
-
Friston, K. "Life as we know it." J. R. Soc. Interface 10, 20130475 (2013). DOI: 10.1098/rsif.2013.0475
-
Friston, K. "A theory of cortical responses." Phil. Trans. R. Soc. B 360, 1249–1266 (2005). DOI: 10.1098/rstb.2005.1622
-
Bastos, A. M. et al. "Canonical microcircuits for predictive coding." Neuron 76, 695–711 (2012). DOI: 10.1016/j.neuron.2012.10.038
-
Bogacz, R. "A tutorial on the free-energy framework for modelling perception and learning." J. Math. Psych. 76, 198–211 (2017). DOI: 10.1016/j.jmp.2015.11.003
-
Buckley, C. L. et al. "The free energy principle for action and perception: a mathematical review." Entropy 19, 318 (2017). DOI: 10.3390/e19070318
-
Fields, C. & Glazebrook, J. F. "A mosaic of Chu spaces and Channel Theory I: Category-theoretic concepts and tools." J. Exp. Theor. Artif. Intell. 34, 1–38 (2022). DOI: 10.1080/0952813X.2019.1689909
-
Šotek, M. "God of the Math — The SCPN Master Publications." Zenodo (2025). DOI: 10.5281/zenodo.17419678