Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

quanticsgrids user guide

This library provides utilities for working with functions on quantics grids and for converting between quantics indices, grid indices, and original coordinates.

quanticsgrids provides two main layers:

  1. Low-level discrete grids with integer coordinates
  2. High-level discretized grids over continuous domains

Most users will want the high-level interface built around DiscretizedGrid. The following chapters mirror the Julia guide while using the Rust API and Rust idioms.

Installation

This crate is not yet published on crates.io. For now, depend on the GitHub repository directly.

Add this to Cargo.toml:

[dependencies]
quanticsgrids = { git = "https://github.com/tensor4all/quanticsgrids-rs" }

Or use cargo add:

cargo add quanticsgrids --git https://github.com/tensor4all/quanticsgrids-rs

Definition

We first introduce a base-B representation with B = 2, 3, 4, .... Throughout this guide, quantics digits and grid indices are 0-based (0-indexed), matching the Rust convention. QuanticsGrids.jl is 1-based; when porting Julia scripts, subtract 1 from grid indices and quantics digits at the call boundary.

We represent a non-negative integer X >= 0 as

$$ X = \sum_{i=1}^{R} x_i B^{R-i}, $$

where each digit x_i satisfies 0 <= x_i < B and R is the number of digits. In this crate, the base-B representation of X is stored as the vector

$$ [x_1, \ldots, x_R]. $$

For multiple variables, the crate supports fused and interleaved unfolding schemes. For three variables X, Y, and Z, suppose their base-B representations are

$$ [x_1, \ldots, x_R], \quad [y_1, \ldots, y_R], \quad [z_1, \ldots, z_R]. $$

The interleaved representation is

$$ [x_1, y_1, z_1, x_2, y_2, z_2, \ldots, x_R, y_R, z_R]. $$

The fused representation is

$$ [\alpha_1, \alpha_2, \ldots, \alpha_R], $$

where

$$ \alpha_i = x_i + B y_i + B^2 z_i $$

and therefore

$$ 0 \le \alpha_i < B^3. $$

In fused ordering, the x digit runs fastest at each digit level. This generalizes to any number of variables.

Discretized Grid

DiscretizedGrid discretizes a continuous d-dimensional domain and supports conversions between:

  • quantics indices
  • grid indices
  • original coordinates

Unlike the Julia package, the Rust API returns an error when original coordinates fall outside the configured domain.

Creating a one-dimensional grid

We can discretize the interval [0, 1) with R bits as follows:

use quanticsgrids::DiscretizedGrid;

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let grid = DiscretizedGrid::builder(&[r])
        .with_bounds(0.0, 1.0)
        .build()?;

    let quantics = vec![0; r];
    let grididx = vec![0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert!((grid.grididx_to_origcoord(&grididx)?[0] - 0.0).abs() < 1e-12);
    assert_eq!(grid.origcoord_to_grididx(&[0.0])?, grididx);

    let quantics = vec![1; r];
    let grididx = vec![2usize.pow(r as u32) - 1];
    let x = 1.0 - 1.0 / 2.0_f64.powi(r as i32);

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert!((grid.quantics_to_origcoord(&quantics)?[0] - x).abs() < 1e-12);
    assert!((grid.grididx_to_origcoord(&grididx)?[0] - x).abs() < 1e-12);
    assert_eq!(grid.origcoord_to_grididx(&[x])?, grididx);
    assert_eq!(grid.origcoord_to_quantics(&[x])?, quantics);

    Ok(())
}

Supported resolutions

Coordinates are f64, so grid points must stay distinguishable: the rounding error of a conversion is about one ulp of the bounds, and a grid whose step falls below that limit cannot map coordinates to indices exactly. The limit therefore depends on the bounds, not on R alone:

BoundsHighest supported R
[0, 1)50
symmetric O(1), e.g. [-1.5, 1.5)51
[0.3, 10.3)50
[1e10, 1e10 + 1)17
[1e12, 1e12 + 1e6)30

build() returns QuanticsGridError::GridNotRepresentable above these, instead of producing a grid whose conversions silently return a neighbouring index. The check is conservative (step of at least four ulps of the bounds, plus the last two grid points checked), so it can also reject a grid that would happen to be exact. Within the accepted range, origcoord_to_grididx(&grididx_to_origcoord(&[g])?) returns g exactly.

Mapping coordinates to indices

A coordinate c maps to the index round((c - lower_bound) / grid_step), clamped to 0..=N-1; a normalized index exactly halfway between two indices rounds to the larger one. The rounding applies to that computed ratio, so the result is not promised to be the geometrically nearest emitted coordinate in every case.

The grid does not wrap around: in the half-open case a coordinate in the last step below the upper bound keeps the last grid point. Coordinates outside the domain ([lower, upper), or [lower, upper] with include_endpoint(true)) return QuanticsGridError::CoordinateOutOfBounds.

The Rust builder also supports including the upper endpoint:

use quanticsgrids::DiscretizedGrid;

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let grid = DiscretizedGrid::builder(&[r])
        .with_bounds(0.0, 1.0)
        .include_endpoint(true)
        .build()?;

    assert!((grid.grididx_to_origcoord(&[0])?[0] - 0.0).abs() < 1e-12);
    assert!((grid.grididx_to_origcoord(&[2usize.pow(r as u32) - 1])?[0] - 1.0).abs() < 1e-12);

    Ok(())
}

Creating a d-dimensional grid

A d-dimensional grid is created in the same way by supplying one resolution per dimension. You can choose either fused or interleaved unfolding.

Fused representation

use quanticsgrids::{DiscretizedGrid, UnfoldingScheme};

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let grid = DiscretizedGrid::builder(&[r, r, r])
        .with_lower_bound(&[0.0, 0.0, 0.0])
        .with_upper_bound(&[1.0, 1.0, 1.0])
        .with_unfolding_scheme(UnfoldingScheme::Fused)
        .build()?;

    let quantics = vec![0; r];
    let grididx = vec![0, 0, 0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert_eq!(grid.origcoord_to_grididx(&[0.0, 0.0, 0.0])?, grididx);

    let quantics = vec![0, 0, 0, 1];
    let grididx = vec![1, 0, 0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);

    let coord = grid.quantics_to_origcoord(&quantics)?;
    assert!((coord[0] - 1.0 / 16.0).abs() < 1e-12);
    assert!((coord[1] - 0.0).abs() < 1e-12);
    assert!((coord[2] - 0.0).abs() < 1e-12);

    Ok(())
}

Incrementing the least significant fused index increments the x coordinate first.

Interleaved representation

use quanticsgrids::{DiscretizedGrid, UnfoldingScheme};

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let grid = DiscretizedGrid::builder(&[r, r, r])
        .with_lower_bound(&[0.0, 0.0, 0.0])
        .with_upper_bound(&[1.0, 1.0, 1.0])
        .with_unfolding_scheme(UnfoldingScheme::Interleaved)
        .build()?;

    let quantics = vec![0; 3 * r];
    let grididx = vec![0, 0, 0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert_eq!(grid.origcoord_to_grididx(&[0.0, 0.0, 0.0])?, grididx);

    let quantics = vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0];
    let grididx = vec![1, 0, 0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);

    let coord = grid.quantics_to_origcoord(&quantics)?;
    assert!((coord[0] - 1.0 / 16.0).abs() < 1e-12);
    assert!((coord[1] - 0.0).abs() < 1e-12);
    assert!((coord[2] - 0.0).abs() < 1e-12);

    Ok(())
}

Inherent Discrete Grid

InherentDiscreteGrid is the low-level grid type for discrete target spaces. It has an interface parallel to DiscretizedGrid, but original coordinates are integer-valued and are defined by an origin and a step size.

use quanticsgrids::InherentDiscreteGrid;

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let grid = InherentDiscreteGrid::builder(&[r])
        .with_origin(&[0])
        .with_step(&[1])
        .build()?;

    let quantics = vec![0; r];
    let grididx = vec![0];
    let origcoord = vec![0];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert_eq!(grid.grididx_to_origcoord(&grididx)?, origcoord);
    assert_eq!(grid.origcoord_to_grididx(&origcoord)?, grididx);
    assert_eq!(grid.origcoord_to_quantics(&origcoord)?, quantics);

    let quantics = vec![1; r];
    let grididx = vec![2usize.pow(r as u32) - 1];
    let origcoord = vec![2_i64.pow(r as u32) - 1];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert_eq!(grid.grididx_to_origcoord(&grididx)?, origcoord);
    assert_eq!(grid.origcoord_to_grididx(&origcoord)?, grididx);
    assert_eq!(grid.origcoord_to_quantics(&origcoord)?, quantics);

    Ok(())
}

As with DiscretizedGrid, the Rust API returns an error when original coordinates fall outside the representable range.

Use a Base Other Than 2

You can choose a base other than 2 when building a grid. The example below uses base 10.

use quanticsgrids::InherentDiscreteGrid;

fn main() -> quanticsgrids::Result<()> {
    let r = 4;
    let base = 10;
    let grid = InherentDiscreteGrid::builder(&[r])
        .with_origin(&[0])
        .with_step(&[1])
        .with_base(base)
        .build()?;

    let quantics = vec![base - 1; r];
    let grididx = vec![base.pow(r as u32) - 1];
    let origcoord = vec![(base as i64).pow(r as u32) - 1];

    assert_eq!(grid.quantics_to_grididx(&quantics)?, grididx);
    assert_eq!(grid.grididx_to_quantics(&grididx)?, quantics);
    assert_eq!(grid.grididx_to_origcoord(&grididx)?, origcoord);
    assert_eq!(grid.origcoord_to_grididx(&origcoord)?, grididx);
    assert_eq!(grid.origcoord_to_quantics(&origcoord)?, quantics);

    Ok(())
}

Quantics Function

When working with algorithms that expect a function of quantics indices, you can wrap a coordinate-space function with quantics_function.

use quanticsgrids::{DiscretizedGrid, quantics_function};

fn main() -> quanticsgrids::Result<()> {
    let grid = DiscretizedGrid::builder(&[4, 4]).build()?;

    let f = |coords: &[f64]| coords[0].sin() + coords[1];
    let fq = quantics_function(&grid, f);

    let quantics = grid.origcoord_to_quantics(&[0.0, 0.5])?;
    let coords = grid.quantics_to_origcoord(&quantics)?;
    let value = fq(&quantics)?;

    assert!((value - (coords[0].sin() + coords[1])).abs() < 1e-12);

    Ok(())
}

This is the Rust equivalent of first defining a function in the original coordinate space and then obtaining a quantics-index wrapper around it.