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:
- Low-level discrete grids with integer coordinates
- 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 1-based.
We represent a positive integer X >= 1 as
$$ X = \sum_{i=1}^{R} (x_i - 1) B^{R-i} + 1, $$
where each digit x_i satisfies 1 <= 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 - 1) + B (y_i - 1) + B^2 (z_i - 1) + 1 $$
and therefore
$$ 1 \le \alpha_i \le B^3. $$
In fused ordering, the x digit runs fastest at each digit level. This matches
the convention used by the Julia package and 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![1; r];
let grididx = vec![1];
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![2; r];
let grididx = vec![2_i64.pow(r as u32)];
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(())
}
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(&[1])?[0] - 0.0).abs() < 1e-12);
assert!((grid.grididx_to_origcoord(&[2_i64.pow(r as u32)])?[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![1; r];
let grididx = vec![1, 1, 1];
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![1, 1, 1, 2];
let grididx = vec![2, 1, 1];
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![1; 3 * r];
let grididx = vec![1, 1, 1];
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![1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1];
let grididx = vec![2, 1, 1];
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![1; r];
let grididx = vec![1];
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![2; r];
let grididx = vec![2_i64.pow(r as u32)];
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 as i64; r];
let grididx = vec![(base as i64).pow(r as u32)];
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.