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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file crienet-0.2.0.tar.gz.
File metadata
- Download URL: crienet-0.2.0.tar.gz
- Upload date:
- Size: 32.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a7dc61c30688ec5a425fd2d6887b5b59c8d8bca61db83dafaace30a21efb7179
|
|
| MD5 |
561faad399431c49dfff2f7a20ef4646
|
|
| BLAKE2b-256 |
7989f0adbca1eab064c7b89564d7152a8b15783c32237d392e169d52a2c84820
|
Provenance
The following attestation bundles were made for crienet-0.2.0.tar.gz:
Publisher:
publish.yml on bongiornoc/CrossRIEnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crienet-0.2.0.tar.gz -
Subject digest:
a7dc61c30688ec5a425fd2d6887b5b59c8d8bca61db83dafaace30a21efb7179 - Sigstore transparency entry: 2310106575
- Sigstore integration time:
-
Permalink:
bongiornoc/CrossRIEnet@f8984cdae7e145b9332b2a2157a790c2af0f3fd8 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bongiornoc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8984cdae7e145b9332b2a2157a790c2af0f3fd8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file crienet-0.2.0-py3-none-any.whl.
File metadata
- Download URL: crienet-0.2.0-py3-none-any.whl
- Upload date:
- Size: 23.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04574f77c176e4d1c5dc9c581b658ae4d8735d895050da2c886c913ac9a749bd
|
|
| MD5 |
88d1c308e5a21b39551f24ec0f448782
|
|
| BLAKE2b-256 |
9209a2e7fbab9cbbe3f25351afd20f1a93ad32cb473baf0eb3b319be48192d66
|
Provenance
The following attestation bundles were made for crienet-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on bongiornoc/CrossRIEnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
crienet-0.2.0-py3-none-any.whl -
Subject digest:
04574f77c176e4d1c5dc9c581b658ae4d8735d895050da2c886c913ac9a749bd - Sigstore transparency entry: 2310106591
- Sigstore integration time:
-
Permalink:
bongiornoc/CrossRIEnet@f8984cdae7e145b9332b2a2157a790c2af0f3fd8 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bongiornoc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8984cdae7e145b9332b2a2157a790c2af0f3fd8 -
Trigger Event:
release
-
Statement type: