Skip to main content

geographdb_core/algorithms/
tnet4d.rs

1//! 4D Tensor Network backed by the spatiotemporal graph.
2//!
3//! Sites occupy a (nx × ny × nz) spatial grid. Each site exists at `depth`
4//! time layers. The full structure has `nx * ny * nz * depth` nodes.
5//!
6//! Two edge types share the same `TemporalEdge` struct — distinguished by the
7//! destination node's layer:
8//!
9//!   Spatial edge   — dst.begin_ts == src.begin_ts  (same time layer, adjacent space)
10//!   Temporal edge  — dst.begin_ts >  src.begin_ts  (same space, next time layer)
11//!
12//! Tensor shape per node: `[χ_t_prev, d, χ_t_next]`
13//!   - χ_t_prev = temporal bond to previous layer (1 for layer 0)
14//!   - d        = physical dimension (2 for qubits)
15//!   - χ_t_next = temporal bond to next layer (1 for final layer)
16//!
17//! Spatial bonds are recorded as edges with weight = χ_spatial and do not
18//! change the tensor rank in this model. They carry entanglement information
19//! but full spatial contraction requires a separate PEPS engine (future work).
20//!
21//! Node id scheme: id = tl * nz*ny*nx  +  sz * ny*nx  +  sy * nx  +  sx
22
23use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
24use crate::algorithms::mps::{get_tensor, mps_apply_gate_2site, mps_norm_sq, set_tensor};
25
26/// Spatial grid dimensions passed as a single argument to avoid clippy `too_many_arguments`.
27#[derive(Clone, Copy, Debug)]
28pub struct GridDims {
29    pub nx: usize,
30    pub ny: usize,
31    pub nz: usize,
32}
33
34/// A single spatial coordinate `(sx, sy, sz)` within a [`GridDims`] grid.
35#[derive(Clone, Copy, Debug)]
36pub struct SiteCoord {
37    pub sx: usize,
38    pub sy: usize,
39    pub sz: usize,
40}
41
42// ── Identity helpers ──────────────────────────────────────────────────────────
43
44/// Compute the node id for site (sx, sy, sz) at time layer tl.
45pub fn site_id(sx: usize, sy: usize, sz: usize, tl: usize, nx: usize, ny: usize, nz: usize) -> u64 {
46    (tl * nz * ny * nx + sz * ny * nx + sy * nx + sx) as u64
47}
48
49// ── Construction ──────────────────────────────────────────────────────────────
50
51/// Build a (nx × ny × nz) × depth tensor network, all sites initialised to |0⟩.
52///
53/// Physical dimension is 2 (qubit). All bond dimensions are 1 (product state).
54/// Spatial edges connect nearest neighbours within each layer; temporal edges
55/// connect the same site across adjacent layers.
56pub fn build_tnet4d(nx: usize, ny: usize, nz: usize, depth: usize) -> Vec<GraphNode4D> {
57    let total = nx * ny * nz * depth;
58    let mut nodes: Vec<GraphNode4D> = Vec::with_capacity(total);
59
60    // Phase 1: create all nodes with |0⟩ tensors.
61    for tl in 0..depth {
62        for sz in 0..nz {
63            for sy in 0..ny {
64                for sx in 0..nx {
65                    let id = site_id(sx, sy, sz, tl, nx, ny, nz);
66                    let mut node = GraphNode4D {
67                        id,
68                        x: sx as f32,
69                        y: sy as f32,
70                        z: sz as f32,
71                        begin_ts: tl as u64,
72                        end_ts: tl as u64 + 1,
73                        properties: GraphProperties::new(),
74                        successors: Vec::new(),
75                    };
76                    // |0⟩: shape [1, 2, 1], data [1.0, 0.0]
77                    set_tensor(&mut node, [1, 2, 1], vec![1.0, 0.0]);
78                    nodes.push(node);
79                }
80            }
81        }
82    }
83
84    // Phase 2: wire spatial edges (same layer, adjacent positions).
85    let directions: &[(i32, i32, i32)] = &[
86        (1, 0, 0),
87        (-1, 0, 0),
88        (0, 1, 0),
89        (0, -1, 0),
90        (0, 0, 1),
91        (0, 0, -1),
92    ];
93    for tl in 0..depth {
94        for sz in 0..nz {
95            for sy in 0..ny {
96                for sx in 0..nx {
97                    let src_id = site_id(sx, sy, sz, tl, nx, ny, nz);
98                    for &(dx, dy, dz) in directions {
99                        let nx2 = sx as i32 + dx;
100                        let ny2 = sy as i32 + dy;
101                        let nz2 = sz as i32 + dz;
102                        if nx2 < 0 || nx2 >= nx as i32 {
103                            continue;
104                        }
105                        if ny2 < 0 || ny2 >= ny as i32 {
106                            continue;
107                        }
108                        if nz2 < 0 || nz2 >= nz as i32 {
109                            continue;
110                        }
111                        let dst_id =
112                            site_id(nx2 as usize, ny2 as usize, nz2 as usize, tl, nx, ny, nz);
113                        let src_idx = src_id as usize;
114                        nodes[src_idx].successors.push(TemporalEdge {
115                            dst: dst_id,
116                            weight: 1.0, // spatial bond dim = 1
117                            begin_ts: tl as u64,
118                            end_ts: tl as u64, // begin_ts == end_ts marks spatial
119                        });
120                    }
121                }
122            }
123        }
124    }
125
126    // Phase 3: wire temporal edges (same position, next layer).
127    for tl in 0..depth.saturating_sub(1) {
128        for sz in 0..nz {
129            for sy in 0..ny {
130                for sx in 0..nx {
131                    let src_id = site_id(sx, sy, sz, tl, nx, ny, nz);
132                    let dst_id = site_id(sx, sy, sz, tl + 1, nx, ny, nz);
133                    let src_idx = src_id as usize;
134                    nodes[src_idx].successors.push(TemporalEdge {
135                        dst: dst_id,
136                        weight: 1.0, // temporal bond dim = 1
137                        begin_ts: tl as u64,
138                        end_ts: tl as u64 + 1, // begin_ts < end_ts marks temporal
139                    });
140                }
141            }
142        }
143    }
144
145    nodes
146}
147
148// ── Lookup ────────────────────────────────────────────────────────────────────
149
150/// Return the slice index of the node at (sx, sy, sz, tl), or `None` if out of bounds.
151pub fn tnet4d_find(
152    nodes: &[GraphNode4D],
153    sx: usize,
154    sy: usize,
155    sz: usize,
156    tl: usize,
157    dims: GridDims,
158) -> Option<usize> {
159    if sx >= dims.nx || sy >= dims.ny || sz >= dims.nz {
160        return None;
161    }
162    let target_id = site_id(sx, sy, sz, tl, dims.nx, dims.ny, dims.nz);
163    nodes.iter().position(|n| n.id == target_id)
164}
165
166// ── Gates ─────────────────────────────────────────────────────────────────────
167
168/// Apply a d×d single-site gate at (sx, sy, sz) in layer tl.
169///
170/// The gate is a d×d matrix in row-major order. Does nothing if the site does
171/// not exist.
172pub fn tnet4d_apply_gate_1site(
173    nodes: &mut [GraphNode4D],
174    sx: usize,
175    sy: usize,
176    sz: usize,
177    tl: usize,
178    dims: GridDims,
179    gate: &[f32],
180) {
181    if let Some(idx) = tnet4d_find(nodes, sx, sy, sz, tl, dims) {
182        let (shape, data) = match get_tensor(&nodes[idx]) {
183            Some(t) => t,
184            None => return,
185        };
186        let (chi_l, d, chi_r) = (shape[0], shape[1], shape[2]);
187        let mut new_data = vec![0.0f32; chi_l * d * chi_r];
188        for a in 0..chi_l {
189            for s_out in 0..d {
190                for b in 0..chi_r {
191                    let mut val = 0.0f32;
192                    for s_in in 0..d {
193                        val += gate[s_out * d + s_in] * data[a * d * chi_r + s_in * chi_r + b];
194                    }
195                    new_data[a * d * chi_r + s_out * chi_r + b] = val;
196                }
197            }
198        }
199        set_tensor(&mut nodes[idx], shape, new_data);
200    }
201}
202
203/// Apply a two-site gate to two spatially adjacent sites at time layer `tl`.
204///
205/// `site0` and `site1` must be neighbors in the grid (differ by one step in one
206/// axis). The gate is a `d²×d²` unitary; see [`mps_apply_gate_2site`] for the
207/// index convention. Only works for χ_in = 1 (product state) and d = 2.
208///
209/// Returns the actual bond dimension kept after SVD truncation.
210pub fn tnet4d_apply_gate_2site(
211    nodes: &mut [GraphNode4D],
212    site0: SiteCoord,
213    site1: SiteCoord,
214    tl: usize,
215    dims: GridDims,
216    gate: &[f32],
217    chi_max: usize,
218) -> usize {
219    let idx_a = match tnet4d_find(nodes, site0.sx, site0.sy, site0.sz, tl, dims) {
220        Some(i) => i,
221        None => return 0,
222    };
223    let idx_b = match tnet4d_find(nodes, site1.sx, site1.sy, site1.sz, tl, dims) {
224        Some(i) => i,
225        None => return 0,
226    };
227    mps_apply_gate_2site(nodes, idx_a, idx_b, gate, chi_max)
228}
229
230// ── Norm ──────────────────────────────────────────────────────────────────────
231
232/// Compute ⟨ψ|ψ⟩ for a product-state 4D network (all spatial bonds χ=1).
233///
234/// Contracts each spatial column (fixed x,y,z through all layers) as a 1D MPS
235/// using `mps_norm_sq`, then multiplies all column norms together.
236///
237/// For entangled states (spatial bond dim > 1) a full PEPS contraction is
238/// required — this function returns the correct answer only for χ_spatial = 1.
239pub fn tnet4d_norm_sq(nodes: &[GraphNode4D], nx: usize, ny: usize, nz: usize, depth: usize) -> f64 {
240    // For product states (all spatial bonds χ=1), the norm factors into
241    // independent temporal columns. Contract each column as a 1D MPS.
242    let mut total_norm = 1.0f64;
243    for sz in 0..nz {
244        for sy in 0..ny {
245            for sx in 0..nx {
246                let dims = GridDims { nx, ny, nz };
247                let column: Vec<GraphNode4D> = (0..depth)
248                    .filter_map(|tl| {
249                        tnet4d_find(nodes, sx, sy, sz, tl, dims).map(|idx| nodes[idx].clone())
250                    })
251                    .collect();
252                total_norm *= mps_norm_sq(&column);
253            }
254        }
255    }
256    total_norm
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    // ── helpers ──────────────────────────────────────────────────────────────
263
264    fn hadamard() -> Vec<f32> {
265        let s = std::f32::consts::FRAC_1_SQRT_2;
266        vec![s, s, s, -s]
267    }
268
269    // ── test 1: node count 2×2×1×1 ───────────────────────────────────────────
270
271    #[test]
272    fn test_build_node_count_single_layer() {
273        let nodes = build_tnet4d(2, 2, 1, 1);
274        assert_eq!(nodes.len(), 4, "2×2×1 grid × 1 layer = 4 nodes");
275    }
276
277    // ── test 2: node count 2×2×1×2 ───────────────────────────────────────────
278
279    #[test]
280    fn test_build_node_count_two_layers() {
281        let nodes = build_tnet4d(2, 2, 1, 2);
282        assert_eq!(nodes.len(), 8, "2×2×1 grid × 2 layers = 8 nodes");
283    }
284
285    // ── test 3: temporal edges connect layers ─────────────────────────────────
286
287    #[test]
288    fn test_temporal_edges_connect_layers() {
289        let nodes = build_tnet4d(2, 2, 1, 2);
290        // Node at (0,0,0,tl=0) should have a temporal edge to (0,0,0,tl=1).
291        let src_id = site_id(0, 0, 0, 0, 2, 2, 1);
292        let dst_id = site_id(0, 0, 0, 1, 2, 2, 1);
293        let src = nodes.iter().find(|n| n.id == src_id).expect("src node");
294        let has_temporal = src.successors.iter().any(|e| e.dst == dst_id);
295        assert!(has_temporal, "no temporal edge from layer 0 to layer 1");
296        // Temporal edge: dst node is at a later layer (begin_ts differs).
297        let dst = nodes.iter().find(|n| n.id == dst_id).expect("dst node");
298        assert!(
299            dst.begin_ts > src.begin_ts,
300            "temporal dst should be at a later time layer"
301        );
302    }
303
304    // ── test 4: spatial edges connect grid neighbours ────────────────────────
305
306    #[test]
307    fn test_spatial_edges_connect_neighbours() {
308        let nodes = build_tnet4d(2, 2, 1, 1);
309        // Node at (0,0,0,tl=0) should have spatial edges to (1,0,0) and (0,1,0).
310        let src_id = site_id(0, 0, 0, 0, 2, 2, 1);
311        let right_id = site_id(1, 0, 0, 0, 2, 2, 1);
312        let up_id = site_id(0, 1, 0, 0, 2, 2, 1);
313        let src = nodes.iter().find(|n| n.id == src_id).expect("src");
314        let has_right = src.successors.iter().any(|e| e.dst == right_id);
315        let has_up = src.successors.iter().any(|e| e.dst == up_id);
316        assert!(has_right, "missing spatial edge to (1,0,0)");
317        assert!(has_up, "missing spatial edge to (0,1,0)");
318        // Spatial edges: dst node is at the same layer (same begin_ts as src).
319        let right = nodes.iter().find(|n| n.id == right_id).expect("right");
320        assert_eq!(
321            right.begin_ts, src.begin_ts,
322            "spatial neighbour must be same layer"
323        );
324    }
325
326    // ── test 5: tnet4d_find locates nodes correctly ───────────────────────────
327
328    #[test]
329    fn test_find_locates_correct_node() {
330        let nodes = build_tnet4d(2, 2, 1, 2);
331        let dims = GridDims {
332            nx: 2,
333            ny: 2,
334            nz: 1,
335        };
336        // Site (1,1,0) at layer 0 should be index 3 in a 2×2×1×1 layout.
337        let idx = tnet4d_find(&nodes, 1, 1, 0, 0, dims).expect("node must exist");
338        let expected_id = site_id(1, 1, 0, 0, 2, 2, 1);
339        assert_eq!(nodes[idx].id, expected_id);
340        // Out of bounds returns None.
341        assert!(tnet4d_find(&nodes, 5, 0, 0, 0, dims).is_none());
342    }
343
344    // ── test 6: all-|0⟩ product state norm = 1.0 ─────────────────────────────
345
346    #[test]
347    fn test_product_state_norm_is_one() {
348        let nodes = build_tnet4d(2, 2, 1, 1);
349        let norm = tnet4d_norm_sq(&nodes, 2, 2, 1, 1);
350        assert!((norm - 1.0).abs() < 1e-5, "norm = {norm}");
351    }
352
353    fn cnot_gate() -> Vec<f32> {
354        vec![
355            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
356        ]
357    }
358
359    // ── test 7: Hadamard on all sites preserves norm ──────────────────────────
360
361    #[test]
362    fn test_hadamard_all_sites_preserves_norm() {
363        let mut nodes = build_tnet4d(2, 2, 1, 1);
364        let h = hadamard();
365        let dims = GridDims {
366            nx: 2,
367            ny: 2,
368            nz: 1,
369        };
370        for sx in 0..2 {
371            for sy in 0..2 {
372                tnet4d_apply_gate_1site(&mut nodes, sx, sy, 0, 0, dims, &h);
373            }
374        }
375        let norm = tnet4d_norm_sq(&nodes, 2, 2, 1, 1);
376        assert!((norm - 1.0).abs() < 1e-5, "norm after H = {norm}");
377    }
378
379    // ── test 8: 2-site CNOT creates entanglement between spatial neighbors ────
380
381    #[test]
382    fn test_tnet4d_2site_gate_creates_entanglement() {
383        let mut nodes = build_tnet4d(2, 2, 1, 1);
384        let dims = GridDims {
385            nx: 2,
386            ny: 2,
387            nz: 1,
388        };
389        // H on (0,0,0): |+⟩|0⟩
390        tnet4d_apply_gate_1site(&mut nodes, 0, 0, 0, 0, dims, &hadamard());
391        // CNOT (0,0,0)↔(1,0,0) → Bell pair
392        let site0 = SiteCoord {
393            sx: 0,
394            sy: 0,
395            sz: 0,
396        };
397        let site1 = SiteCoord {
398            sx: 1,
399            sy: 0,
400            sz: 0,
401        };
402        let chi_new = tnet4d_apply_gate_2site(&mut nodes, site0, site1, 0, dims, &cnot_gate(), 2);
403        assert_eq!(
404            chi_new, 2,
405            "Bell pair between spatial neighbours: bond must be 2"
406        );
407    }
408}