From f0860f410fd1421b5a40b7dd0ad5deacb58a35f1 Mon Sep 17 00:00:00 2001 From: Changkai Zhang Date: Mon, 28 Sep 2026 23:32:48 +0800 Subject: [PATCH 1/3] Add interaction map for the LLL-projected C4 moire model on a cylinder chain Enumerated every one-body slot and every ordered pair admitted by momentum conservation and the orbital bounds, in closed form on the index differences rather than by generating and rejecting candidates over all four indices. Truncation is deliberately left to the model builder, the only stage that sees the interaction parameters and can judge which couplings are negligible; a cutoff imposed here would also be invisible in the logs, since the excluded terms would never be enumerated in the first place. --- intrcmaps/intrcmap_moire_c4.py | 126 +++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 intrcmaps/intrcmap_moire_c4.py diff --git a/intrcmaps/intrcmap_moire_c4.py b/intrcmaps/intrcmap_moire_c4.py new file mode 100644 index 0000000..188919b --- /dev/null +++ b/intrcmaps/intrcmap_moire_c4.py @@ -0,0 +1,126 @@ +# Copyright (C) 2026 Changkai Zhang. +# +# This file is part of IntraKnot. +# +# IntraKnot is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. +# +# IntraKnot is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with IntraKnot. If not, see . + + +"""Interaction map for the LLL-projected C4 moire model on a cylinder chain.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, List + +from alice.network.interaction import ( + Interaction, + Interaction1Site, + Interaction2Site, + InteractionNSite, +) + +if TYPE_CHECKING: + from alice.physics.geometry import Geometry + +logger = logging.getLogger(__name__) + + +def intrcmap_moire_c4(geo: Geometry) -> List[Interaction]: + """Enumerate one-body and ordered-pair interaction slots for moire_c4. + + Reads from `geo.cfg`: + + - `lx` — number of retained Landau-gauge orbitals `N` + - `ny` — cylinder periods per flux cell (hopping distance for C4) + + The ordered-pair enumeration is complete: every `(m, n, k, l)` admitted by + momentum conservation and the orbital bounds is emitted. Truncation belongs + to the model builder, where the coefficients are known — see `cpl_rtol` in + `build_moire_c4`. Do not reintroduce index cutoffs here: this stage only + receives `geo`, so it cannot see the interaction parameters that decide + which terms are negligible, and a cutoff imposed here is invisible in the + logs because the excluded terms are never enumerated at all. + + All returned interactions have `cpl = 0.0` and unset tensors; the model + builder fills them. + """ + N = int(geo.lx) + ny = int(geo.cfg.get('ny', 2)) + + interactions: List[Interaction] = [] + + logger.info("─" * 60) + logger.info("Moire C4 Interaction Map".center(60)) + logger.info("─" * 60) + logger.info("") + logger.info(f" Orbitals N = {N}, ny = {ny}") + logger.info("") + + # --- One-body diagonal potential slots --- + for site in range(N): + interactions.append(Interaction1Site( + label=['B', 'DIAG'], + site=site, + )) + + # --- One-body hopping at separation ny (C4 y-harmonics) --- + n_hop = 0 + for n in range(N - ny): + interactions.append(Interaction2Site( + label=['B', 'HOP'], + leading_site=n, + terminal_site=n + ny, + )) + n_hop += 1 + + # --- Ordered-pair interaction: (m, p, r) with p = m-l, r = m-k --- + # The ordered-pair constraints are r < p and p + r < 0. For integer p, r + # these are jointly equivalent to r < 0 together with |r| >= |p| + 1, so + # writing q = -r > 0 the admissible triples can be enumerated directly from + # the orbital bounds without generating and rejecting candidates: + # + # l = m - p in [0, N) -> m - N < p <= m + # k = m + q in [0, N) -> q <= N - 1 - m + # n = m - p + q in [0, N) -> q <= N - 1 - m + p (binds only for p < 0) + # + # m < n and l < k both reduce to q > p and so need no separate check. + # When p == 0 the term is density-density n_m n_n with n = m + q. + n_pair = 0 + n_dens = 0 + for m in range(N): + for p in range(m - N + 1, m + 1): + q_hi = N - 1 - m + min(p, 0) + for q in range(abs(p) + 1, q_hi + 1): + l = m - p + k = m + q + n = m - p + q + if p == 0: + label = ['V', 'DENS'] + n_dens += 1 + else: + label = ['V', 'PAIR'] + n_pair += 1 + interactions.append(InteractionNSite( + label=label, + sites=[m, n, k, l], + )) + + logger.info(f" Diagonal one-body: {N}") + logger.info(f" Hopping (sep={ny}): {n_hop}") + logger.info(f" Density-density: {n_dens}") + logger.info(f" Ordered-pair terms: {n_pair}") + logger.info(f" Total interactions: {len(interactions)}") + logger.info("") + + return interactions From 2214ec7f2264ff95ca5cc15edb555ee6a4408daa Mon Sep 17 00:00:00 2001 From: Changkai Zhang Date: Mon, 28 Sep 2026 23:32:55 +0800 Subject: [PATCH 2/3] Add LLL-projected C4 moire model builder with screened Coulomb interaction table Built the cylinder one-body matrix from the four C4 reciprocal harmonics, and precomputed the screened-Coulomb table through a nonoscillatory form of the momentum integral, reached by substituting the oscillatory cosine away so that Gauss-Legendre converges without a blowup in node count. The two-body term is carried in the half-weighted convention, summed over ordered pairs alone, which is what fixes the interaction scale relative to the potential amplitude and is set out on `ordered_pair_W`. Coupling truncation is relative to a per-channel reference rather than absolute, so that a scan over either scale keeps the same accuracy throughout, and the discarded weight is both logged and exposed for programmatic audit. --- models/model_moire_c4.py | 606 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 606 insertions(+) create mode 100644 models/model_moire_c4.py diff --git a/models/model_moire_c4.py b/models/model_moire_c4.py new file mode 100644 index 0000000..19da233 --- /dev/null +++ b/models/model_moire_c4.py @@ -0,0 +1,606 @@ +# Copyright (C) 2026 Changkai Zhang. +# +# This file is part of IntraKnot. +# +# IntraKnot is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. +# +# IntraKnot is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with IntraKnot. If not, see . + + +"""LLL-projected C4 moire model builder for Alice. + +Implements the cylinder one-body matrix B from reciprocal harmonics and the +Yukawa interaction table V_{p,r}, then populates Interaction1Site / +Interaction2Site / InteractionNSite objects for AutoMPO construction. +""" + +from __future__ import annotations + +import logging +from typing import Callable, Dict, List, Optional, Sequence, Tuple + +import numpy as np +from numpy.polynomial.legendre import leggauss + +from nicole import Direction, Index, Tensor, einsum, identity + +from alice.network.interaction import ( + Interaction, + Interaction1Site, + Interaction2Site, + InteractionNSite, +) +from alice.physics.system import build_fermionic + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Local 4-order MPO helpers (mirrors alice.physics.system private helpers) +# --------------------------------------------------------------------------- + +def _make_leading4(op3: Tensor) -> Tensor: + op4 = op3.clone() + op4.insert_index(0, direction=Direction.IN, itag='_aux_') + return op4.permute([0, 3, 1, 2]) + + +def _make_terminal4(op3: Tensor) -> Tensor: + op4 = op3.clone() + op4.insert_index(3, direction=Direction.OUT, itag='_aux_') + return op4.permute([2, 3, 0, 1]) + + +def _make_intermid4(string_op: Tensor, leading_tnsr: Tensor) -> Tensor: + op_idx_out = leading_tnsr.indices[1] + op_id = identity(op_idx_out.flip()) + return einsum('op,rs->oprs', op_id, string_op) + + +def _channel_templates(ops: Dict[str, Tensor]) -> Dict[str, Tensor]: + """Derive single-channel fermionic templates from build_fermionic ops.""" + C4 = _make_leading4(ops['C']) + C4dag = _make_terminal4(ops['Cd']) + F4 = _make_leading4(ops['F']) + F4dag = _make_terminal4(ops['Fd']) + return { + 'C4': C4, + 'C4dag': C4dag, + 'C4mid': _make_intermid4(ops['Z'], C4), + 'F4': F4, + 'F4dag': F4dag, + 'F4mid': _make_intermid4(ops['Z'], F4), + 'I4': ops['I4'], + 'N4': ops['N4'], + 'G4': ops['G4'], + 'G4dag': ops['G4dag'], + 'Z4mid': ops['Z4mid'], + } + + +def _onebody_window(i: int, j: int, ch: Dict[str, Tensor]) -> List[Tensor]: + """MPO window for c†_j c_i on the contiguous span [min(i,j), max(i,j)]. + + Alice's JW-dressed channels match G4 with positive coupling for both + directions, so no extra minus is applied. + """ + if i == j: + return [ch['N4'].clone()] + left, right = (i, j) if i < j else (j, i) + span = right - left + 1 + if j > i: + leading, mid, terminal = ch['F4'], ch['F4mid'], ch['F4dag'] + else: + leading, mid, terminal = ch['C4'], ch['C4mid'], ch['C4dag'] + window = [leading.clone()] + for _ in range(span - 2): + window.append(mid.clone()) + window.append(terminal.clone()) + return window + + +def _four_op_window(m: int, n: int, k: int, l: int, ch: Dict[str, Tensor]) -> List[Tensor]: + """Concatenate two disjoint one-body windows for (c†_m c_l)(c†_n c_k).""" + a_hi = max(m, l) + b_lo = min(n, k) + if a_hi >= b_lo: + raise ValueError( + f"Factors not disjoint/ordered for (m,n,k,l)=({m},{n},{k},{l})" + ) + win_a = _onebody_window(l, m, ch) + win_b = _onebody_window(k, n, ch) + gap = [ch['I4'].clone() for _ in range(b_lo - a_hi - 1)] + return win_a + gap + win_b + + +# --------------------------------------------------------------------------- +# Geometry / harmonic helpers +# --------------------------------------------------------------------------- + +def _c4_geometry(lB: float, ny: int) -> Tuple[float, float, float, float]: + """Return (a0, Ly, alpha, g0) for one flux per square cell.""" + a0 = np.sqrt(2.0 * np.pi) * lB + Ly = ny * a0 + alpha = 2.0 * np.pi * lB / Ly + g0 = 2.0 * np.pi / a0 + return float(a0), float(Ly), float(alpha), float(g0) + + +def _c4_harmonics(V0: float, g0: float) -> List[Tuple[float, float, complex]]: + """Four C4 harmonics with amplitude V0 at (±g0, 0) and (0, ±g0).""" + return [ + (g0, 0.0, complex(V0)), + (-g0, 0.0, complex(V0)), + (0.0, g0, complex(V0)), + (0.0, -g0, complex(V0)), + ] + + +# --------------------------------------------------------------------------- +# One-body matrix B (Eq. 10) +# --------------------------------------------------------------------------- + +def onebody_matrix( + N: int, + Ly: float, + lB: float, + harmonics: Sequence[Tuple[float, float, complex]], + *, + comm_tol: float = 1e-10, + imag_tol: float = 1e-10, +) -> np.ndarray: + """Build the complex one-body matrix B from reciprocal harmonics. + + Parameters + ---------- + N: + Number of retained orbitals. + Ly: + Cylinder circumference. + lB: + Magnetic length. + harmonics: + Sequence of `(gx, gy, Vg)` triples. + comm_tol: + Tolerance for the commensurability check on `gy * Ly / (2π)`. + imag_tol: + Maximum allowed imaginary part after assembly (Alice `cpl` is real). + + Returns + ------- + ndarray + Complex `(N, N)` matrix. For the C4 potential it is real up to + `imag_tol`. + """ + B = np.zeros((N, N), dtype=complex) + X = 2.0 * np.pi * (lB ** 2) * np.arange(N) / Ly + + for gx, gy, Vg in harmonics: + s_raw = gy * Ly / (2.0 * np.pi) + s = int(round(s_raw)) + if abs(s_raw - s) > comm_tol: + raise ValueError( + f"Incommensurate harmonic gy={gy}: s_raw={s_raw} not near " + f"integer (tol={comm_tol})" + ) + # Use the commensurate gy to remove roundoff. + gy_c = 2.0 * np.pi * s / Ly + form = np.exp(-0.25 * (lB ** 2) * (gx ** 2 + gy_c ** 2)) + for n in range(N): + m = n + s + if 0 <= m < N: + phase = np.exp(1j * gx * (X[m] + X[n]) / 2.0) + B[m, n] += Vg * form * phase + + if np.max(np.abs(B - B.conj().T)) > imag_tol: + raise ValueError("One-body matrix B is not Hermitian within tolerance") + if np.max(np.abs(B.imag)) > imag_tol: + raise ValueError( + f"One-body matrix B has imaginary parts up to " + f"{np.max(np.abs(B.imag))}; Alice couplings must be real" + ) + return B + + +# --------------------------------------------------------------------------- +# Yukawa interaction table V_{p,r} (Eq. 22–24) +# --------------------------------------------------------------------------- + +def _integrate_I(a: float, b: float, tol: float) -> Tuple[float, float]: + """Evaluate the nonoscillatory integral I(a,b) of Eq. (24). + + Uses Gauss-Legendre quadrature on [0, v_max] with v_max = asinh(w_max / b), + doubling the node count until successive estimates differ by less than + `tol`. + """ + if b <= 0.0: + raise ValueError("Finite screening requires b > 0") + + # Tail bound: ΔV <= P * exp(-w_max^2/2) / w_max^2; for the dimensionless + # integral allocate tol/2 to the tail → choose w_max large enough that + # exp(-w^2/2)/w^2 <= tol/2 (P handled by the caller). + w_max = 8.0 + while True: + tail = np.exp(-0.5 * w_max ** 2) / (w_max ** 2) + if tail <= tol / 2.0: + break + w_max += 2.0 + if w_max > 40.0: + break + v_max = float(np.arcsinh(w_max / b)) + + def integrand(v: np.ndarray) -> np.ndarray: + return np.exp( + -0.5 * (b ** 2) * np.sinh(v) ** 2 + - 0.5 * (a ** 2) / np.cosh(v) ** 2 + ) + + n_nodes = 16 + prev = None + estimate = 0.0 + for _ in range(12): + x, w = leggauss(n_nodes) + # Map [-1, 1] → [0, v_max] + v = 0.5 * v_max * (x + 1.0) + jac = 0.5 * v_max + estimate = float(jac * np.dot(w, integrand(v))) + if prev is not None and abs(estimate - prev) < tol / 2.0: + err = abs(estimate - prev) + float(tail) + return estimate, err + prev = estimate + n_nodes *= 2 + + return estimate, abs(estimate - (prev or estimate)) + float(tail) + + +def interaction_table( + N: int, + Ly: float, + lB: float, + C: float, + lam: float, + *, + int_tol: float = 1e-12, +) -> Tuple[np.ndarray, np.ndarray]: + """Precompute the N×N Yukawa table V_{p,r} for p,r = 0…N-1. + + Returns + ------- + V: + Real table of shape `(N, N)`. + err: + Estimated absolute error per entry, same shape. + """ + alpha = 2.0 * np.pi * lB / Ly + kappa = lB / lam + V = np.zeros((N, N), dtype=float) + err = np.zeros((N, N), dtype=float) + + if C == 0.0: + return V, err + + for p in range(N): + Pp = (2.0 * C / Ly) * np.exp(-0.5 * (alpha * p) ** 2) + b = np.sqrt((alpha * p) ** 2 + kappa ** 2) + # Absolute tolerance on I: int_tol / (2 Pp) when Pp > 0. + i_tol = int_tol / (2.0 * Pp) if Pp > 0.0 else int_tol + for r in range(N): + a = alpha * abs(r) + I_val, I_err = _integrate_I(a, b, i_tol) + V[p, r] = Pp * I_val + err[p, r] = Pp * I_err + + return V, err + + +def lookup_V(V: np.ndarray, p: int, r: int) -> float: + """Retrieve V_{p,r} using absolute indices (isotropic symmetries).""" + return float(V[abs(p), abs(r)]) + + +def ordered_pair_W(V: np.ndarray, m: int, n: int, k: int, l: int) -> float: + """Ordered-pair coefficient W_{mnkl} (Eq. 32), or 0 if m+n ≠ k+l. + + Parameters + ---------- + V: + Yukawa table from `interaction_table`, indexed by `(|p|, |r|)`. + m, n, k, l: + Orbital indices of `c†_m c†_n c_k c_l`, ordered as `m < n` and `l < k`. + + Returns + ------- + float + Coefficient multiplying `c†_m c†_n c_k c_l` in the ordered-pair sum. + + Notes + ----- + The two-body term is carried as a sum over ordered pairs alone, + + H_int = Σ_{m None: + self.name = name + self.ref = ref + self.thresh = cpl_rtol * ref + self.n_total = 0 + self.n_drop = 0 + self.sum_total = 0.0 + self.sum_drop = 0.0 + self.max_drop = 0.0 + + def keep(self, cpl: float) -> bool: + """Record `cpl` and report whether it survives the threshold. + + A zero reference means the channel vanishes identically (`V0 = 0` or + `C = 0`), in which case every term is dropped. That path must be taken + explicitly: a relative comparison against a zero reference would + otherwise retain every coupling as an explicit zero in the MPO. + """ + mag = abs(cpl) + self.n_total += 1 + self.sum_total += mag + if self.ref == 0.0 or mag < self.thresh: + self.n_drop += 1 + self.sum_drop += mag + self.max_drop = max(self.max_drop, mag) + return False + return True + + @property + def fraction(self) -> float: + """Discarded share of the total coupling weight in this channel.""" + if self.sum_total == 0.0: + return 0.0 + return self.sum_drop / self.sum_total + + def report(self, warn_frac: float) -> None: + """Log the discard budget, warning when it exceeds `warn_frac`.""" + logger.info( + f" {self.name}: kept {self.n_total - self.n_drop}/{self.n_total}, " + f"ref={self.ref:.3e}, thresh={self.thresh:.3e}" + ) + logger.info( + f" discarded weight {self.sum_drop:.3e} of {self.sum_total:.3e} " + f"(fraction {self.fraction:.2e}, largest {self.max_drop:.3e})" + ) + if self.fraction > warn_frac: + logger.warning( + f" {self.name} truncation discards {self.fraction:.2%} of the " + f"total coupling weight, above warn_frac={warn_frac:.1e}. The " + f"energy error is bounded by {self.sum_drop:.3e}; lower cpl_rtol." + ) + + +# --------------------------------------------------------------------------- +# Model builder +# --------------------------------------------------------------------------- + +def build_moire_c4( + interactions: List[Interaction], + L: int, + *, + symmetry: str = 'U1', + lB: float = 1.0, + ny: int = 2, + V0: float = 0.3, + C: float = 1.0, + lam: float = 2.0, + cpl_rtol: float = 1e-10, + warn_frac: float = 1e-6, + comm_tol: float = 1e-10, + int_tol: float = 1e-12, + space_fn: Optional[Callable] = None, + **_ignored, +) -> Tuple[Index, Dict[str, Tensor]]: + """Populate interactions for the LLL-projected C4 moire model. + + Parameters + ---------- + interactions: + List from `intrcmap_moire_c4`. Modified in place. + L: + Chain length (number of orbitals). Must equal `N` used by the + interaction map. + symmetry: + Fermion symmetry; must be `'U1'`. + lB: + Magnetic length. + ny: + Number of flux cells along the cylinder circumference. + V0: + C4 potential amplitude. + C: + Yukawa interaction strength. Its scale relative to `V0` depends on the + half-weighted two-body convention documented in `ordered_pair_W`. + lam: + Screening length λ, as a bare length in the same units as `lB`. Sources + that quote λ in units of the orbital spacing `2π lB² / Ly` must be + converted before being passed here. + cpl_rtol: + Relative coupling-truncation threshold. A term is discarded when its + magnitude falls below `cpl_rtol` times its channel reference scale: + `max|B|` for the one-body terms and `V[0,0]` for the two-body terms. + This is the only truncation control — the interaction map enumerates + every admissible term, since only this stage knows the coefficients. + warn_frac: + Warn when the discarded coupling weight of either channel exceeds this + fraction of that channel's total weight. + comm_tol: + Commensurability tolerance for potential harmonics. + int_tol: + Absolute integration tolerance per V-table entry. + space_fn: + Optional replacement for `build_fermionic`. + """ + if symmetry != 'U1': + raise ValueError(f"moire_c4 requires symmetry='U1', got '{symmetry}'") + # `**_ignored` would otherwise swallow a stale `vtol`, silently falling back + # to the `cpl_rtol` default. Since the two differ in meaning (absolute vs + # relative to a channel reference), that would change the Hamiltonian + # without any indication in the log. + if 'vtol' in _ignored: + raise ValueError( + "'vtol' has been replaced by 'cpl_rtol', which is relative to the " + "channel reference scale (max|B| for one-body, V[0,0] for two-body) " + "rather than absolute. Set 'cpl_rtol' explicitly." + ) + + _space = space_fn if space_fn is not None else build_fermionic + spc, ops = _space(symmetry) + ch = _channel_templates(ops) + + a0, Ly, alpha, g0 = _c4_geometry(lB, ny) + harmonics = _c4_harmonics(V0, g0) + B = onebody_matrix(L, Ly, lB, harmonics, comm_tol=comm_tol) + Vtable, Verr = interaction_table(L, Ly, lB, C, lam, int_tol=int_tol) + + logger.info("─" * 60) + logger.info("Moire C4 Coefficients".center(60)) + logger.info("─" * 60) + logger.info("") + logger.info(f" lB={lB}, ny={ny}, a0={a0:.6f}, Ly={Ly:.6f}") + logger.info(f" alpha={alpha:.6f}, V0={V0}, C={C}, lam={lam}") + logger.info(f" max|B|={np.max(np.abs(B)):.6e}, V00={Vtable[0, 0]:.6e}") + logger.info(f" cpl_rtol={cpl_rtol:.1e}, warn_frac={warn_frac:.1e}") + logger.info("") + + onebody = _ChannelTally('one-body', float(np.max(np.abs(B))), cpl_rtol) + twobody = _ChannelTally('two-body', float(Vtable[0, 0]), cpl_rtol) + + # Cached only for the density-density channel (p == 0), where the window is + # [N4, I4 ... I4, N4] and its span |r| + 1 is fixed by r alone. Pair-hopping + # windows depend on the absolute site positions, not just (p, r), so they + # cannot be shared and are rebuilt per term. + dens_window_cache: Dict[int, List[Tensor]] = {} + + for intr in interactions: + if isinstance(intr, Interaction1Site) and 'DIAG' in intr.label: + # Diagonal B_nn is real for C4. + cpl = float(B[intr.site, intr.site].real) + if onebody.keep(cpl): + intr.cpl = cpl + intr.tnsr = ch['N4'].clone() + else: + intr.cpl = 0.0 + continue + + if isinstance(intr, Interaction2Site) and 'HOP' in intr.label: + i, j = intr.leading_site, intr.terminal_site + # Hermitian hopping: store B_ji as cpl with G4/G4dag (= c†_i c_j + h.c. + # channel sum). For real C4, B_ji = B_ij = V0 e^{-π/2}. + cpl = float(B[j, i].real) + if onebody.keep(cpl): + intr.cpl = cpl + intr.leading_tnsr = ch['G4'].clone() + intr.terminal_tnsr = ch['G4dag'].clone() + if j > i + 1: + intr.intermid_tnsr = ch['Z4mid'].clone() + else: + intr.cpl = 0.0 + continue + + if isinstance(intr, InteractionNSite) and ( + 'PAIR' in intr.label or 'DENS' in intr.label + ): + if len(intr.sites) != 4: + raise ValueError( + f"Expected sites=[m,n,k,l], got {intr.sites}" + ) + m, n, k, l = intr.sites + W = ordered_pair_W(Vtable, m, n, k, l) + if not twobody.keep(W): + intr.cpl = 0.0 + continue + intr.cpl = float(W) + + p = m - l + if p == 0: + # Density-density n_m n_n on [m, n]. The window is fixed by the + # separation n - m = |r| alone, so one template serves every m + # at that separation. Pair hopping (p != 0) has no such + # invariance: its window depends on the absolute positions. + key = n - m + if key not in dens_window_cache: + win = [ch['N4'].clone()] + for _ in range(key - 1): + win.append(ch['I4'].clone()) + win.append(ch['N4'].clone()) + dens_window_cache[key] = win + intr.tnsrs = [t.clone() for t in dens_window_cache[key]] + else: + intr.tnsrs = _four_op_window(m, n, k, l, ch) + continue + + onebody.report(warn_frac) + twobody.report(warn_frac) + logger.info("") + + # Attach coefficient tables on the ops dict for tests / inspection. + ops['_moire_B'] = B + ops['_moire_V'] = Vtable + ops['_moire_Verr'] = Verr + # Expose the discard budget so it can be audited programmatically rather + # than only scraped from the log. + ops['_moire_discard'] = { + t.name: { + 'ref': t.ref, 'thresh': t.thresh, + 'n_total': t.n_total, 'n_drop': t.n_drop, + 'sum_total': t.sum_total, 'sum_drop': t.sum_drop, + 'max_drop': t.max_drop, 'fraction': t.fraction, + } + for t in (onebody, twobody) + } + ops['_moire_params'] = { + 'lB': lB, 'ny': ny, 'a0': a0, 'Ly': Ly, 'alpha': alpha, + 'V0': V0, 'C': C, 'lam': lam, 'g0': g0, + } + + return spc, ops From f59ef6fc9378ac1e5aa2da891ccf5bd265de2b92 Mon Sep 17 00:00:00 2001 From: Changkai Zhang Date: Mon, 28 Sep 2026 23:33:01 +0800 Subject: [PATCH 3/3] Register the moire C4 model and interaction map in registry.yaml Catalogued both scripts under the existing model and intrcmap categories together with their SHA-256 digests, which is what makes them discoverable and installable once this reaches primary. --- registry.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/registry.yaml b/registry.yaml index bb3bf87..5896e6f 100644 --- a/registry.yaml +++ b/registry.yaml @@ -8,6 +8,10 @@ categories: file: models/model_bfg.py description: Balents-Fisher-Girvin model builder for Alice sha256: 5a3282ae34a5d68171bf60c45d45acc4683ef0fb3e3b46a8599d4ea3e59f9711 + - name: model_moire_c4 + file: models/model_moire_c4.py + description: LLL-projected C4 moire model builder for Alice + sha256: a4c3b224270fdf73b9f57ee7221756f6a3de990a3467c7126c4d87c858902554 intrcmaps: description: Interaction maps for Alice @@ -16,3 +20,7 @@ categories: file: intrcmaps/intrcmap_bfg.py description: Interaction map for the Balents-Fisher-Girvin model on Kagome sha256: 29b809abf07793bf0d833e51e1800c36dfa55d7f4c916e397054d3e4453bf377 + - name: intrcmap_moire_c4 + file: intrcmaps/intrcmap_moire_c4.py + description: Interaction map for the LLL-projected C4 moire model on a cylinder chain + sha256: 01828ff6103978bed0aea296702fd303152e8a413a636ede4b8be176ae37a51c