Skip to content

Commit db52073

Browse files
andigclaude
andauthored
simplex: hypersparse FTRAN via activation graph (#20)
ftran mirrors btran's sparse path: for a mostly-zero rhs (nnz*10 <= m) route through ftranSparse, which fires only the pivots reachable through the activation graph. Skipped pivots would compute exactly 0, so results match the dense path bitwise. buildBtranGraph now also builds ftranOwner (row -> resolving ftran seq), covering the forward singletons, the shared kernel seq, and the reversed backward singletons. Adds TestFactorFtranSparse: 200-trial property test over unit vectors and 1-3 nonzero spikes, checking B*x == v on the sparse path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent acbd018 commit db52073

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

simplex/factor.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ type factor struct {
4747
rdrHead []int32 // row -> [rdrHead[r], rdrHead[r+1]) into rdrList
4848
rdrList []int32 // pivot seqs whose column reads that row
4949

50+
// ftran activation: a written row activates the one pivot that reads it
51+
// as its pivot row; ftranOwner maps row -> that ftran seq (kernel shares one).
52+
ftranOwner []int32
53+
5054
ws *factorWS // shared per-LP solve scratch (solver is single-threaded)
5155
}
5256

@@ -336,11 +340,36 @@ func (f *factor) buildBtranGraph() {
336340
}
337341
head[0] = 0
338342
f.rdrHead, f.rdrList = head, list
343+
344+
// ftran solve order: fwd forward (seq i), kernel (seq nf), bwd reversed
345+
// (bwd[i] fires at seq nf+1+(nb-1-i)); own each row by its resolving seq.
346+
owner := make([]int32, m)
347+
for i, tp := range f.fwd {
348+
owner[tp.row] = int32(i)
349+
}
350+
for _, r := range f.kRows {
351+
owner[r] = int32(nf)
352+
}
353+
for i, tp := range f.bwd {
354+
owner[tp.row] = int32(nf + 1 + (nb - 1 - i))
355+
}
356+
f.ftranOwner = owner
339357
}
340358

341359
// ftranFactor solves B*x = v in place: v becomes x indexed by basis
342360
// position (x[pos] = multiplier of basic column pos).
343361
func (f *factor) ftran(v []float64) {
362+
nnz := 0
363+
for _, val := range v[:f.m] {
364+
if val != 0 {
365+
nnz++
366+
}
367+
}
368+
// same activation-graph payoff threshold as btran: mostly-zero rhs
369+
if nnz*10 <= f.m {
370+
f.ftranSparse(v)
371+
return
372+
}
344373
x := f.ws.xScratch // by basis position
345374
clear(x)
346375
// forward: row singletons
@@ -390,6 +419,83 @@ func (f *factor) ftran(v []float64) {
390419
copy(v, x)
391420
}
392421

422+
// ftranSparse is ftran firing only activation-reachable pivots; skipped
423+
// pivots would compute exactly 0, so results match the dense path bitwise.
424+
func (f *factor) ftranSparse(v []float64) {
425+
nf, nb := len(f.fwd), len(f.bwd)
426+
kernelSeq := int32(nf)
427+
x := f.ws.xScratch // by basis position
428+
clear(x)
429+
f.ws.gen++
430+
gen := f.ws.gen
431+
act := f.ws.actGen
432+
for r := range f.m {
433+
if v[r] != 0 {
434+
act[f.ftranOwner[r]] = gen
435+
}
436+
}
437+
// a fired pivot writes its column's rows; activate each written row's owner
438+
mark := func(pos int32) {
439+
for _, r := range f.tblRow[f.cols[pos]] {
440+
act[f.ftranOwner[r]] = gen
441+
}
442+
}
443+
// forward: row singletons
444+
for i, tp := range f.fwd {
445+
if act[i] != gen {
446+
continue
447+
}
448+
xc := v[tp.row] / tp.a
449+
x[tp.pos] = xc
450+
if xc != 0 {
451+
cr, cv := f.tblRow[f.cols[tp.pos]], f.tblVal[f.cols[tp.pos]]
452+
for k, r := range cr {
453+
v[r] -= cv[k] * xc
454+
}
455+
v[tp.row] = 0 // exactly resolved
456+
mark(tp.pos)
457+
}
458+
}
459+
// kernel: dense solve when reachable (matches btranSparse's solveT)
460+
if k := len(f.kRows); k > 0 && act[kernelSeq] == gen {
461+
kv := f.ws.kScratch[:k]
462+
for ki, r := range f.kRows {
463+
kv[ki] = v[r]
464+
}
465+
f.klu.solve(kv)
466+
for ki := range k {
467+
s := kv[ki]
468+
pos := f.kPos[ki]
469+
x[pos] = s
470+
if s != 0 {
471+
cr, cv := f.tblRow[f.cols[pos]], f.tblVal[f.cols[pos]]
472+
for kk, r := range cr {
473+
v[r] -= cv[kk] * s
474+
}
475+
mark(int32(pos))
476+
}
477+
}
478+
}
479+
// backward: col singletons in reverse
480+
for i := nb - 1; i >= 0; i-- {
481+
seq := int32(nf + 1 + (nb - 1 - i))
482+
if act[seq] != gen {
483+
continue
484+
}
485+
tp := f.bwd[i]
486+
xc := v[tp.row] / tp.a
487+
x[tp.pos] = xc
488+
if xc != 0 {
489+
cr, cv := f.tblRow[f.cols[tp.pos]], f.tblVal[f.cols[tp.pos]]
490+
for k, r := range cr {
491+
v[r] -= cv[k] * xc
492+
}
493+
mark(tp.pos)
494+
}
495+
}
496+
copy(v, x)
497+
}
498+
393499
// btran solves B^T*y = w in place: w is indexed by basis position on entry,
394500
// y by row on exit. Ops are the adjoints of ftran's, in reverse order.
395501
func (f *factor) btran(w []float64) {

simplex/factor_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,48 @@ func multiply(colRow [][]int32, colVal [][]float64, m int, x []float64) []float6
5454
return v
5555
}
5656

57+
// TestFactorFtranSparse drives the mostly-zero rhs that routes through
58+
// ftranSparse (unit vectors and 1-3 nonzeros), checking B*x == v.
59+
func TestFactorFtranSparse(t *testing.T) {
60+
rng := rand.New(rand.NewSource(13))
61+
for trial := range 200 {
62+
m := 5 + rng.Intn(80)
63+
colRow, colVal := randomBasis(rng, m)
64+
f := factorize(m, identCols(m), colRow, colVal, nil)
65+
if f == nil {
66+
continue
67+
}
68+
v := make([]float64, m)
69+
want := make([]float64, m)
70+
// unit vector on even trials (the alpha/tau case), else 1-3 spikes
71+
if trial%2 == 0 {
72+
v[rng.Intn(m)] = 1
73+
} else {
74+
for n := 1 + rng.Intn(3); n > 0; n-- {
75+
v[rng.Intn(m)] = rng.NormFloat64()
76+
}
77+
}
78+
copy(want, v)
79+
nnz := 0
80+
for _, x := range v {
81+
if x != 0 {
82+
nnz++
83+
}
84+
}
85+
if nnz*10 > m {
86+
continue // would route dense; not the path under test
87+
}
88+
f.ftran(v)
89+
back := multiply(colRow, colVal, m, v)
90+
for i := range back {
91+
if math.Abs(back[i]-want[i]) > 1e-7 {
92+
t.Fatalf("trial %d m=%d sparse ftran: B*x[%d]=%g want %g (kernel %d fwd %d bwd %d)",
93+
trial, m, i, back[i], want[i], len(f.kRows), len(f.fwd), len(f.bwd))
94+
}
95+
}
96+
}
97+
}
98+
5799
func TestFactorFtranBtran(t *testing.T) {
58100
rng := rand.New(rand.NewSource(7))
59101
for trial := range 50 {

0 commit comments

Comments
 (0)