CRIENet: CrossRIEnet for Rectangular Cross-Correlation Cleaning
This library implements the neural estimator introduced in:
- Manolakis, E., Bongiorno, C., & Mantegna, R. N. (2026). Physics-Informed Singular-Value Learning for Cross-Covariances Forecasting in Financial Markets. arXiv:2601.07687v3.
CRIENet is a TensorFlow/Keras research implementation for cleaning rectangular cross-correlation matrices. Given two marginal correlation matrices, their empirical cross-correlation and the corresponding sample size, the model learns corrections to the empirical spectral coefficients and reconstructs a cross-correlation matrix in the empirical singular-vector basis.
The package operates on correlation matrices. Estimation of marginal volatilities, conversion to cross-covariances and preprocessing of raw returns are outside the core API.
Version 0.2 does not provide compatibility aliases for the earlier crossrie
package.
Relationship to RIEnet
RIEnet applies learned spectral corrections to square covariance and correlation matrices through an eigendecomposition. CrossRIEnet applies a related construction to rectangular cross-correlation matrices through a singular-value decomposition.
CRIENet is currently an independent package. It does not import RIEnet or rely on RIEnet private symbols. Matrix outputs from the two packages can be composed in an external workflow when marginal covariance or precision estimates are also required.
What this package provides
- A Keras layer for learned cleaning of rectangular cross-correlations
- Additive, bounded-multiplicative and positive-multiplicative corrections
- Access to the reconstructed matrix, spectral coefficients and intermediate spectral quantities
- Support for varying batch size and matrix dimensions in one traced function
- Float32, float64 and Keras mixed-precision policies
- Structural validation and optional validation of the full correlation block
- Keras
.kerasserialization and training throughModel.fit
Module organization
crienet.layer: the publicCrossRIEnetLayer.crienet.trainable_layers: the shared encoder, recurrent aggregator and correction head.crienet.ops_layers: deterministic projection, padding and reconstruction layers.crienet.spectral: the full-basis spectral backend.crienet.validation: input and correlation-domain validation.crienet.diagnostics: non-mutating feasibility diagnostics.
Installation
Install from PyPI:
python -m pip install crienet
Or install from source:
git clone https://github.com/bongiornoc/CrossRIEnet.git
cd CrossRIEnet
python -m pip install -e .
For development:
python -m pip install -e ".[dev]"
pytest
The supplied Conda environment can be created with:
conda env update --file environment.yml --prune
conda activate crienet_env
python -m pip install -e ".[dev]"
Quick start
The examples below construct the three input matrices from the same joint return sample. This ensures that the marginal matrices and cross-correlation belong to one valid correlation block.
import tensorflow as tf
from crienet import CrossRIEnetLayer
def correlation_blocks(returns, n_x):
"""Compute correlation blocks from returns shaped (batch, time, variables)."""
centered = returns - tf.reduce_mean(returns, axis=1, keepdims=True)
standardized = centered / tf.math.reduce_std(
centered,
axis=1,
keepdims=True,
)
sample_size = tf.shape(standardized)[1]
correlation = tf.matmul(
standardized,
standardized,
transpose_a=True,
) / tf.cast(sample_size, standardized.dtype)
return (
correlation[:, :n_x, :n_x],
correlation[:, n_x:, n_x:],
correlation[:, :n_x, n_x:],
)
batch_size = 32
sample_size = 60
n_x = 10
n_y = 8
returns = tf.random.stateless_normal(
(batch_size, sample_size, n_x + n_y),
seed=(1, 2),
)
correlation_x, correlation_y, cross_correlation = correlation_blocks(
returns,
n_x,
)
inputs = {
"correlation_x": correlation_x,
"correlation_y": correlation_y,
"cross_correlation": cross_correlation,
"sample_size": tf.fill((batch_size,), tf.cast(sample_size, tf.float32)),
}
cleaned = CrossRIEnetLayer(output_type="cross_correlation")(
inputs,
training=False,
)
print(cleaned.shape) # (32, 10, 8)
The input may also be supplied as a four-element sequence in this order:
cleaned = CrossRIEnetLayer()(
[
correlation_x,
correlation_y,
cross_correlation,
inputs["sample_size"],
],
training=False,
)
sample_size accepts shape (batch,) or (batch, 1). It may use an integer
or floating dtype, must be strictly positive, and may differ between samples in
the same batch.
Training
The example below generates one regularized Wishart correlation population per
training example. Empirical blocks are estimated from finite Gaussian samples;
the corresponding population cross-correlation is the target. This illustrates
the Model.fit interface, not the training protocol used in the paper.
import tensorflow as tf
from crienet import CrossRIEnetLayer
def generate_populations(count, dimension, seed=(1, 2)):
factors = tf.random.stateless_normal((count, dimension, dimension), seed)
covariance = tf.matmul(factors, factors, transpose_b=True)
covariance += 0.5 * tf.eye(dimension)
scale = tf.sqrt(tf.linalg.diag_part(covariance))
return covariance / scale[..., :, None] / scale[..., None, :]
def make_dataset(populations, n_x, sample_size, batch_size, seed=(3, 4)):
count, dimension = tf.shape(populations)[0], tf.shape(populations)[1]
noise = tf.random.stateless_normal(tf.stack([count, sample_size, dimension]), seed)
returns = tf.matmul(noise, tf.linalg.cholesky(populations), transpose_b=True)
returns -= tf.reduce_mean(returns, axis=1, keepdims=True)
returns /= tf.math.reduce_std(returns, axis=1, keepdims=True)
empirical = tf.matmul(returns, returns, transpose_a=True) / sample_size
inputs = {
"correlation_x": empirical[:, :n_x, :n_x],
"correlation_y": empirical[:, n_x:, n_x:],
"cross_correlation": empirical[:, :n_x, n_x:],
"sample_size": tf.fill([count], tf.cast(sample_size, tf.float32)),
}
targets = populations[:, :n_x, n_x:]
return tf.data.Dataset.from_tensor_slices((inputs, targets)).batch(batch_size)
n_x, n_y = 6, 8
dataset = make_dataset(
generate_populations(256, n_x + n_y),
n_x=n_x,
sample_size=64,
batch_size=32,
)
inputs = {
"correlation_x": tf.keras.Input((None, None), name="correlation_x"),
"correlation_y": tf.keras.Input((None, None), name="correlation_y"),
"cross_correlation": tf.keras.Input((None, None), name="cross_correlation"),
"sample_size": tf.keras.Input((), name="sample_size"),
}
model = tf.keras.Model(inputs, CrossRIEnetLayer()(inputs))
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4, clipnorm=1.0),
loss="mse",
)
model.fit(dataset, epochs=10)
Dense tensors in one call to fit have fixed n_x and n_y. Training across
different matrix dimensions requires a tf.data.Dataset that yields batches
with the corresponding dynamic TensorSpec objects. One batch must still
contain matrices with a common shape.
Selecting outputs
output_type accepts one output name, a sequence of names or "all".
# One output name returns a tensor.
cleaned = CrossRIEnetLayer(
output_type="cross_correlation",
)(inputs)
# A sequence returns a dictionary.
outputs = CrossRIEnetLayer(
output_type=("cross_correlation", "spectral_coefficients"),
)(inputs)
cleaned = outputs["cross_correlation"]
coefficients = outputs["spectral_coefficients"]
# Intermediate quantities for inspection.
spectral = CrossRIEnetLayer(
output_type=(
"empirical_singular_values",
"correction",
"left_singular_vectors",
"right_singular_vectors",
"projected_variance_x",
"projected_variance_y",
),
)(inputs)
Stable outputs are:
cross_correlation;spectral_coefficients.
The remaining outputs expose internal quantities for diagnostics and research. With additive correction, spectral coefficients can be negative and are not strict mathematical singular values.
Correction modes
additive = CrossRIEnetLayer(
correction_mode="additive",
additive_activation="linear",
)
bounded = CrossRIEnetLayer(
correction_mode="bounded_multiplicative",
)
positive = CrossRIEnetLayer(
correction_mode="positive_multiplicative",
)
The corresponding definitions are:
additive:
coefficient = empirical + activation(delta)
bounded_multiplicative:
coefficient = empirical * sigmoid(delta)
positive_multiplicative:
coefficient = empirical * softplus(delta)
Feasibility diagnostics
feasibility_diagnostics evaluates compatibility of a proposed
cross-correlation with fixed marginal correlation matrices. It reports values;
it does not modify or project the matrix.
from crienet.diagnostics import feasibility_diagnostics
diagnostics = feasibility_diagnostics(
correlation_x,
correlation_y,
cleaned,
)
maximum_canonical_value = diagnostics["max_canonical_singular_value"]
violation_count = diagnostics["violation_count"]
Dtype and mixed precision
The Keras dtype policy controls computation, variables and public outputs:
| Policy | Spectral work dtype | Variables | Public output |
|---|---|---|---|
float64 |
float64 |
float64 |
float64 |
float32 |
float32 |
float32 |
float32 |
mixed_float16 |
float32 |
float32 |
float16 |
mixed_bfloat16 |
float32 |
float32 |
bfloat16 |
Passing float64 inputs does not override a float32 layer policy. Use
dtype="float64" on the layer or select the global float64 policy when that
precision is required.
Version 0.2 does not claim jit_compile=True support. With TensorFlow 2.20
and Keras 3.12, Keras disables JIT on GPU for the cuDNN-backed recurrent
layers, while CPU XLA compilation fails in the dynamic rectangular spectral
branch. Standard Model.fit training works on CPU and GPU without JIT.
The mixed_bfloat16 row specifies the dtype contract, not unconditional
gradient stability. An intermittent non-finite backward pass was observed on
an NVIDIA RTX A2000 for some initialization histories; use float32 when
finite-gradient guarantees are required.
Requirements
- Python >=3.10
- TensorFlow >=2.16.1
- Keras >=3.0
These are lower bounds, not upper compatibility limits. The CI matrix tests selected Python and TensorFlow versions; newer compatible releases are allowed by the package metadata.
Development
Run the complete test suite from the repository root:
pytest
The suite covers public API validation, dynamic tracing, float32, float64,
mixed precision, reconstruction, gradients, valid correlation blocks,
non-degenerate equivariance, diagnostics and .keras serialization.
Citation
Use print_citation() or the repository CITATION.cff:
import crienet
crienet.print_citation()
@article{manolakis2026crossrienet,
title = {Physics-Informed Singular-Value Learning for Cross-Covariances
Forecasting in Financial Markets},
author = {Manolakis, Efstratios and Bongiorno, Christian and
Mantegna, Rosario N.},
year = {2026},
eprint = {2601.07687},
archivePrefix = {arXiv}
}
Support
For questions, issues, or contributions,
- Open an issue on GitHub
- Check the documentation
- Contact Efstratios Manolakis (stratomanolaki@gmail.com)
- Contact Prof. Christian Bongiorno (christian.bongiorno@centralesupelec.fr) for calibrated model weights or collaboration requests
Metadata
Release files for crienet 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| crienet-0.2.0.tar.gz | 32.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| crienet-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.7 kB
Release files / crienet-0.2.0.tar.gz
| Download URL | crienet-0.2.0.tar.gz |
|---|---|
| Size | 32.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a7dc61c30688ec5a425fd2d6887b5b59c8d8bca61db83dafaace30a21efb7179
|
|
BLAKE2b-256 checksum How to use checksums |
7989f0adbca1eab064c7b89564d7152a8b15783c32237d392e169d52a2c84820
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logRelease files / crienet-0.2.0-py3-none-any.whl
| Download URL | crienet-0.2.0-py3-none-any.whl |
|---|---|
| Size | 23.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
04574f77c176e4d1c5dc9c581b658ae4d8735d895050da2c886c913ac9a749bd
|
|
BLAKE2b-256 checksum How to use checksums |
9209a2e7fbab9cbbe3f25351afd20f1a93ad32cb473baf0eb3b319be48192d66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency log