PageSourceSearch

https://ahuchala.com/components/hodge/chiGrassmannianCI.js

js ahuchala.com collected 2026-10-04 21:52:09 UTC 8,912 bytes, 260 lines download raw bytes

1// ============================================================
2// chiGrassmannianCI.js
3//
4// Computes χ(Ω^j_Z(t)) for a complete intersection Z of given
5// degrees in the Grassmannian Gr(k, n), using the f_λ(t)
6// hook-length formula and the Koszul/conormal recurrence.
7//
8// Exports: chiCI(k, n, degrees, t = 0)
9//   Returns an array of length (dim Z + 1) with
10//   [χ(Ω^0_Z(t)), χ(Ω^1_Z(t)), ..., χ(Ω^{dim}_Z(t))].
11// ============================================================
12
13// --- Enumerate all partitions λ in the k × m box with |λ| = target.
14// Yields arrays (non-increasing, first entry ≤ m, length ≤ k).
15export function* partitionsInBox(k, m, target, maxFirst = m, len = 0, partial = []) {
16  if (target === 0) { yield partial.slice(); return; }
17  if (len === k || target <= 0) return;
18  const hi = Math.min(maxFirst, target);
19  for (let v = hi; v >= 1; v--) {
20    partial.push(v);
21    yield* partitionsInBox(k, m, target - v, v, len + 1, partial);
22    partial.pop();
23  }
24}
25
26// --- Conjugate (transpose) of λ, padded to length m.
27function conjugate(lambda, m) {
28  const t = new Array(m).fill(0);
29  for (let a = 0; a < lambda.length; a++) {
30    for (let b = 0; b < lambda[a]; b++) t[b]++;
31  }
32  return t;
33}
34
35// --- Hook length h_λ(a, b) for 1-indexed a ∈ [1,k], b ∈ [1,m].
36// λ is 0-indexed (λ[0] = λ_1, etc.); λ_a = 0 for a > len(λ).
37// λ^T is already computed and 0-indexed.
38function hookLength(lambda, lambdaT, a, b) {
39  const la = a <= lambda.length ? lambda[a - 1] : 0;
40  const lb = b <= lambdaT.length ? lambdaT[b - 1] : 0;
41  return la + lb - a - b + 1;
42}
43
44// --- Exact BigInt rational arithmetic helpers ---
45
46export function gcdBig(a, b) {
47  if (a < 0n) a = -a;
48  if (b < 0n) b = -b;
49  while (b) { const t = b; b = a % b; a = t; }
50  return a;
51}
52
53// Reduce [num, den] to lowest terms with den > 0.
54export function reduce(num, den) {
55  if (num === 0n) return [0n, 1n];
56  const g = gcdBig(num < 0n ? -num : num, den < 0n ? -den : den);
57  num /= g; den /= g;
58  if (den < 0n) { num = -num; den = -den; }
59  return [num, den];
60}
61
62export function addFrac([a, b], [c, d]) {
63  return reduce(a * d + c * b, b * d);
64}
65
66export function subFrac([a, b], [c, d]) {
67  return reduce(a * d - c * b, b * d);
68}
69
70// --- f_λ(t): product over all (a,b) in [1,k]×[1,m].
71// Uses exact BigInt arithmetic to avoid float64 overflow on large Grassmannians.
72// If any h_λ(a,b) = 0: returns 1 when t = 0, else returns 0.
73export function fLambdaExact(lambda, k, m, t) {
74  const lT = conjugate(lambda, m);
75  const tB = BigInt(t);
76  let num = 1n;
77  let den = 1n;
78  for (let a = 1; a <= k; a++) {
79    for (let b = 1; b <= m; b++) {
80      const h = hookLength(lambda, lT, a, b);
81      if (h === 0) {
82        if (t !== 0) return [0n, 1n];
83        // h = 0, t = 0: factor (h-t)/h = 0/0 → L'Hôpital → 1; skip it
84      } else {
85        const hB = BigInt(h);
86        num *= (hB - tB);
87        den *= hB;
88      }
89    }
90  }
91  return reduce(num, den);
92}
93
94// --- χ(Ω^j_X(t)) for the Grassmannian X = Gr(k,n).
95// = (-1)^j * Σ_{λ : |λ|=j, λ in k×m box} f_λ(t)
96// Returns a BigInt fraction [num, den].
97export function chiGrassmannianExact(k, n, j, t) {
98  const m = n - k;
99  if (j < 0 || j > k * m) return [0n, 1n];
100  let sum = [0n, 1n];
101  for (const lambda of partitionsInBox(k, m, j)) {
102    sum = addFrac(sum, fLambdaExact(lambda, k, m, t));
103  }
104  const sign = j % 2 === 0 ? 1n : -1n;
105  return [sign * sum[0], sum[1]];
106}
107
108// --- Memoized recurrence for the CI.
109// χ(Ω^j_{Z_s}(t)) = χ(Ω^j_{Z_{s-1}}(t))
110//                  - χ(Ω^j_{Z_{s-1}}(t - d_s))
111//                  - χ(Ω^{j-1}_{Z_s}(t - d_s))
112// All values are BigInt fractions [num, den].
113function buildChiCI(k, n, degrees) {
114  const cache = new Map();
115
116  function chi(s, j, t) {
117    if (j < 0) return [0n, 1n];
118    const key = `${s},${j},${t}`;
119    if (cache.has(key)) return cache.get(key);
120    let val;
121    if (s === 0) {
122      val = chiGrassmannianExact(k, n, j, t);
123    } else {
124      const d = degrees[s - 1];
125      val = subFrac(
126        subFrac(chi(s - 1, j, t), chi(s - 1, j, t - d)),
127        chi(s, j - 1, t - d)
128      );
129    }
130    cache.set(key, val);
131    return val;
132  }
133
134  return chi;
135}
136
137// Convert a BigInt fraction to Number. For integer fractions (den divides num),
138// returns the exact integer; otherwise falls back to floating-point division.
139export function fracToNumber([num, den]) {
140  if (den === 1n) return Number(num);
141  // Exact integer check
142  if (num % den === 0n) return Number(num / den);
143  return Number(num) / Number(den);
144}
145
146// --- Main export.
147// Returns [χ(Ω^0_Z(t)), ..., χ(Ω^{dim Z}_Z(t))].
148export function chiCI(k, n, degrees, t = 0) {
149  const dim = k * (n - k) - degrees.length;
150  if (dim < 0) throw new Error("Too many equations: dim Z < 0");
151  const chi = buildChiCI(k, n, degrees);
152  const r = degrees.length;
153  return Array.from({ length: dim + 1 }, (_, j) => fracToNumber(chi(r, j, t)));
154}
155
156// --- Primitive middle-row Hodge numbers for rendering.
157// Returns an array of length floor(dim/2)+1 where entry j is
158// h^{dim-j, j}_prim = (-1)^{dim-j} * (χ(Ω^j_Z) - (-1)^j * a_j)
159// and a_j = #{partitions in k×m box of size j}.
160//
161// This is the format consumed by constructMiddleRow/renderDiamond in
162// grassmannian.js: renderDiamond adds h^{j,j}(Gr(k,n)) on top for the
163// diagonal entry when dim is even.
164// --- Full Hodge diamond for a CI Z in Gr(k,n).
165// Returns a 2D array in the same format as hodgeGrassmannian:
166//   2*dim+1 rows; row i has min(i+1, 2*dim+1-i) entries.
167// Non-middle rows: h^{p,p} from Grassmannian (Lefschetz + Hodge duality), all else 0.
168// Middle row: primitive Hodge numbers + Grassmannian diagonal contribution.
169
170// Module-level cache: keyed on "k,n,[d1,d2,...]" so repeated calls with the
171// same parameters (e.g. adjusting only the blow-up count s) skip recomputation.
172const _hodgeCICache = new Map()
173
174export function hodgeDiamondCI(k, n, degrees) {
175  const _cacheKey = `${k},${n},${JSON.stringify(degrees)}`
176  if (_hodgeCICache.has(_cacheKey)) return _hodgeCICache.get(_cacheKey)
177
178  const m = n - k;
179  const dim = k * m - degrees.length;
180  if (dim < 0) throw new Error("dim Z < 0");
181
182  // a[j] = #{partitions in k×m box of size j} = h^{j,j}(Gr(k,n))
183  const a = [];
184  for (let j = 0; j <= dim; j++) {
185    let count = 0;
186    for (const _ of partitionsInBox(k, m, j)) count++;
187    a.push(count);
188  }
189
190  // Build full middle row from primitive values + Grassmannian diagonal.
191  // Grassmannian contributes a[half] only at j=half when dim is even.
192  const prim = hodgePrimitiveMiddleRow(k, n, degrees);
193  const half = Math.floor(dim / 2);
194  const isOdd = dim % 2 === 1;
195  const midHalf = prim.map((p, j) =>
196    p + (!isOdd && j === half ? a[half] : 0)
197  );
198  const midFull = [...midHalf];
199  if (isOdd) {
200    for (let j = half; j >= 0; j--) midFull.push(midHalf[j]);
201  } else {
202    for (let j = half - 1; j >= 0; j--) midFull.push(midHalf[j]);
203  }
204
205  const totalRows = 2 * dim + 1;
206  const diamond = [];
207  for (let i = 0; i < totalRows; i++) {
208    const rowSize = i <= dim ? i + 1 : 2 * dim - i + 1;
209    const row = new Array(rowSize).fill(0);
210    if (i === dim) {
211      for (let j = 0; j < rowSize; j++) row[j] = midFull[j];
212    } else {
213      // Non-middle: only diagonal entry h^{p,p} is nonzero.
214      // mirrorI = row index in the "above-middle" half (same for below by Serre duality).
215      // colJ = mirrorI/2 is the column of the diagonal entry (if mirrorI is even).
216      const mirrorI = i < dim ? i : 2 * dim - i;
217      if (mirrorI % 2 === 0) {
218        const colJ = mirrorI / 2;
219        if (colJ < rowSize) row[colJ] = a[colJ];
220      }
221    }
222    diamond.push(row);
223  }
224  _hodgeCICache.set(_cacheKey, diamond)
225  return diamond;
226}
227
228export function hodgePrimitiveMiddleRow(k, n, degrees) {
229  const m = n - k;
230  const dim = k * m - degrees.length;
231  if (dim < 0) throw new Error("dim Z < 0");
232  const chi = buildChiCI(k, n, degrees);
233  const r = degrees.length;
234  const half = Math.floor(dim / 2);
235  const result = [];
236  for (let j = 0; j <= half; j++) {
237    const chiVal = fracToNumber(chi(r, j, 0));
238    let a_j = 0;
239    for (const _ of partitionsInBox(k, m, j)) a_j++;
240    const sign     = ((dim - j) % 2 === 0) ? 1 : -1;
241    const chiSign  = (j % 2 === 0) ? 1 : -1;
242    result.push(Math.round(sign * (chiVal - chiSign * a_j)) + 0);
243  }
244  return result;
245}
246
247// Returns the Hodge diamond of Bl_s(Z), the blow-up of Z at s points in
248// general position. Formula: h^{p,p} += s for 1 <= p <= dim-1; all other
249// Hodge numbers unchanged. diamond is the 2D array from hodgeDiamondCI.
250export function applyBlowUp(diamond, s, dim) {
251  const result = diamond.map(row => row.slice());
252  if (s <= 0 || dim <= 1) return result;
253  for (let p = 1; p <= dim - 1; p++) {
254    const rowIdx = 2 * p;
255    // mirrorI = min(2p, 2*(dim-p)) = 2*min(p, dim-p), so colJ = min(p, dim-p)
256    const colJ = Math.min(p, dim - p);
257    result[rowIdx][colJ] += s;
258  }
259  return result;
260}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.