Skip to main content

CRIENet: CrossRIEnet for Rectangular Cross-Correlation Cleaning

Python >=3.10 License: MIT

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 .keras serialization and training through Model.fit

Module organization

  • crienet.layer: the public CrossRIEnetLayer.
  • 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,

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

crienet-0.2.0.tar.gz (32.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

crienet-0.2.0-py3-none-any.whl (23.9 kB view details)

Uploaded Python 3

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

Hashes for crienet-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a7dc61c30688ec5a425fd2d6887b5b59c8d8bca61db83dafaace30a21efb7179
MD5 561faad399431c49dfff2f7a20ef4646
BLAKE2b-256 7989f0adbca1eab064c7b89564d7152a8b15783c32237d392e169d52a2c84820

See more details on using hashes here.

Provenance

The following attestation bundles were made for crienet-0.2.0.tar.gz:

Publisher: publish.yml on bongiornoc/CrossRIEnet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for crienet-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 04574f77c176e4d1c5dc9c581b658ae4d8735d895050da2c886c913ac9a749bd
MD5 88d1c308e5a21b39551f24ec0f448782
BLAKE2b-256 9209a2e7fbab9cbbe3f25351afd20f1a93ad32cb473baf0eb3b319be48192d66

See more details on using hashes here.

Provenance

The following attestation bundles were made for crienet-0.2.0-py3-none-any.whl:

Publisher: publish.yml on bongiornoc/CrossRIEnet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page