s settled from OUR code (higher = LONGER clauses), and it cannot rescue the gun
=== TASK 1: THE DIRECTION QUESTION, ANSWERED WITH A DEMONSTRATION ===
I told the user `s` controls clause length but refused to claim the DIRECTION,
because I had seen it described both ways. It is now read out of our own code -
one site per core, in the Type I branch of `tmLearnDir` (`guns/tm_pattern.nim:258`,
`guns/tsetlin.nim:224`, `tm_diag/tm_core.nim:110`):
if pol * d > 0.0:
if lits[lit] == 1:
if cOut == 1: if rand < (s-1)/s: st += 1 # toward Include, w.p. (s-1)/s
else: if rand < 1/s: st -= 1 # toward Exclude, w.p. 1/s
else: if rand < 1/s: st -= 1 # toward Exclude, w.p. 1/s
=> **HIGHER `s` GIVES LONGER CLAUSES.** The include step runs w.p. (s-1)/s
(rising with s); both exclude steps run w.p. 1/s (falling with s).
DEMONSTRATED (49-bit draft, planted 2-literal rule, 3000 train / 1500 eval):
s=1.0 len 1.51 acc 100% s=2.0 len 1.82 acc 100% s=5.0 len 3.02 acc 100%
s=1.5 len 1.42 acc 100% s=3.0 len 2.19 acc 100% s=10 len 4.04 acc 99.7%
s=20 len 5.17 acc 94.5%
WHY s=1.0 DEGENERATES: (s-1)/s = 0 so the include step NEVER fires while 1/s = 1
so BOTH exclude steps always fire - Type I can only remove literals, so a clause
can grow only through the Type II penalty. (On random labels that leaves 43/120
non-empty clauses vs 120/120 at s>=3.)
USABLE RANGE ~[1.5, 5]. tm_pattern uses 3.0; tsetlin uses 1.5.
=== TASK 4: WOULD `s` HELP THE SHIPPED GUN? NO - MEASURED ===
Recompiling the offline driver with -d:TM_S_DEF=<v> (source untouched) retrains
the gun end to end:
s mean len verdict warm acc margin vs majority
1.5 15.82 too long 32.22% -2.03pp
2.0 14.99 too long 34.03% -0.22pp
3.0* 19.17 too long 35.72% +1.48pp (*shipped)
5.0 20.47 too long 34.72% +0.47pp
Lowering `s` shrinks the clauses and makes accuracy WORSE; raising it pads them
and also loses. The shipped 3.0 is the best of the four, and **no value comes
near the healthy 3-8 band.** Combined with the settledness finding, the shape is
consistent with "NO CONSISTENT SHORT RULE EXISTS in this representation/target".
So the bottleneck is the SIGNAL - now confirmed from a THIRD independent angle
(settledness, churn trend, and clause shape). This is the measurement behind the
decision not to spend effort sweeping N or s.
=== TASK 3: AN HONEST CORRECTION TO MY OWN HYPOTHESIS ===
I predicted that random labels would produce `too long` clauses (the TM padding).
MEASURED: on this encoding noise reads as **short / `collapsed`** (mean 1.88,
median 2.0, acc 33.3%) - the TM FAILS TO COMMIT rather than padding. So "too
long" is not the noise signature, which means the shipped gun's 19.17 mean is not
explained by label noise. Worth knowing.
Adds diagnostic group 8: the clause-shape checker - full length distribution
(min/median/p10/p90/std), per-polarity and per-class breakdowns, a
`clauseShapeVerdict` against a parameterised healthy band (default 3-8),
per-BLOCK length contributions, and clause coverage (mean firing clauses,
effectiveClauses = participation ratio, top3Share). `healthLine` now appends
`shape=<mean> (<verdict>)`.
Validation: `test_tm_clause_shape` 66 checks. A planted 2-literal rule reads
`healthy` with the literals recovered exactly; per-block correctly names the
planted blocks (WALLS 43.0%, BULLETS 28.1%) and buries an irrelevant block (3.4%,
below its uniform 8.3% share); random labels read `collapsed`.
REAL READING, shipped gun: mean 19.17 / median 16.00 / p90 44.80 / max 57,
173 non-empty of 200, 27 empty => **`too long`**; coverage firing/sample 48.53
(24.3%), effectiveClauses 97.85/200, top3Share 5.3% (voting NOT concentrated);
per-block is diffuse with no dominator, EXCEPT **UNUSED 6.4%** - the always-true
negations of the never-written bits 38/39 acting as FREE PADDING, the same bug the
kit found earlier now visible as clause bloat.
Guards: test_tm_clause_shape 66 (new), test_tm_diag 48, test_tm_automata_diag 55,
diag_synthetic 17, diag_automata_validation 11, test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 40,
test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20,
test_vbullet_admit_gate 12. acceptance_offline_vs_online not run (needs a live
battle; no tm_diag dependency).
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
## tm_diag/diagnostics.nim — THE DIAGNOSTICS KIT (Tasks 2 + 5).
|
||||
##
|
||||
## Everything runs OFFLINE: a trained `TmMachine` plus a labelled sample set.
|
||||
## No battles, no harness, no Java. The seven groups are:
|
||||
## No battles, no harness, no Java. The eight groups are:
|
||||
## 1. pre-flight DATA checks (dataChecks, shuffledLabelControl)
|
||||
## 2. clause introspection (clauseInfo, clauseSummary, topClauses)
|
||||
## 3. per-feature contribution (featureContributions, deadInputs, ...)
|
||||
@@ -12,6 +12,10 @@
|
||||
## voteDisagreement, stateHistogram,
|
||||
## perInputConfidence, healthLine,
|
||||
## automataDiagnostics)
|
||||
## 8. CLAUSE SHAPE (Task 2/4) (clauseShapeVerdict, lengthStats,
|
||||
## blockLengthContributions, clauseCoverage,
|
||||
## clauseShapeDiagnostics, shapeLine,
|
||||
## formatClauseShapeReport)
|
||||
##
|
||||
## A `DiagSample` carries the LITERAL vector (positive literals [0..nBits),
|
||||
## negations [nBits..2*nBits)) exactly as the shipped TM stores it, plus the
|
||||
@@ -184,6 +188,68 @@ type
|
||||
lengthHist*: seq[int] ## index = clause length, value = count (non-empty)
|
||||
meanLength*: float
|
||||
maxLength*: int
|
||||
## ── clause-SHAPE distribution (Task 2, group 8) ──
|
||||
minLength*: int ## shortest non-empty clause
|
||||
medianLength*: float ## p50 of the non-empty length distribution
|
||||
p10Length*: float
|
||||
p90Length*: float
|
||||
stdLength*: float
|
||||
posMeanLength*: float ## positive-polarity clauses only
|
||||
negMeanLength*: float ## negative-polarity clauses only
|
||||
perClassMeanLength*: seq[float]
|
||||
perClassMedianLength*: seq[float]
|
||||
perClassHist*: seq[seq[int]]
|
||||
emptyFraction*: float ## emptyClauses / totalClauses
|
||||
|
||||
proc percentile*(sorted: openArray[int], p: float): float =
|
||||
## Linear-interpolation percentile (numpy default) over an ASCENDING array.
|
||||
## Empty -> 0.0; p is clamped to [0,1].
|
||||
if sorted.len == 0: return 0.0
|
||||
let pp = clamp(p, 0.0, 1.0)
|
||||
let rank = pp * float(sorted.len - 1)
|
||||
let lo = int(floor(rank))
|
||||
let hi = int(ceil(rank))
|
||||
if lo == hi: return sorted[lo].float
|
||||
sorted[lo].float + (rank - float(lo)) * (sorted[hi] - sorted[lo]).float
|
||||
|
||||
type
|
||||
LengthStats* = object
|
||||
n*: int
|
||||
minL*: int
|
||||
maxL*: int
|
||||
mean*: float
|
||||
median*: float
|
||||
p10*: float
|
||||
p90*: float
|
||||
stdDev*: float
|
||||
hist*: seq[int]
|
||||
|
||||
proc lengthStats*(lengths: openArray[int]): LengthStats =
|
||||
## Full distribution of clause lengths: n, min/max, mean, median, p10/p90,
|
||||
## standard deviation and a histogram (index = length).
|
||||
result.n = lengths.len
|
||||
if result.n == 0:
|
||||
result.minL = 0
|
||||
result.maxL = 0
|
||||
return
|
||||
var sorted = newSeq[int](lengths.len)
|
||||
for i, l in lengths: sorted[i] = l
|
||||
sorted.sort()
|
||||
result.minL = sorted[0]
|
||||
result.maxL = sorted[^1]
|
||||
var sum = 0
|
||||
for l in sorted: sum += l
|
||||
result.mean = sum.float / result.n.float
|
||||
result.median = percentile(sorted, 0.5)
|
||||
result.p10 = percentile(sorted, 0.10)
|
||||
result.p90 = percentile(sorted, 0.90)
|
||||
var varSum = 0.0
|
||||
for l in sorted:
|
||||
let d = l.float - result.mean
|
||||
varSum += d * d
|
||||
result.stdDev = sqrt(varSum / result.n.float)
|
||||
result.hist = newSeq[int](result.maxL + 1)
|
||||
for l in sorted: inc result.hist[l]
|
||||
|
||||
proc clauseInfo*(m: TmMachine, samples: openArray[DiagSample],
|
||||
spec: FeatureSpec): seq[ClauseInfo] =
|
||||
@@ -200,14 +266,31 @@ proc clauseInfo*(m: TmMachine, samples: openArray[DiagSample],
|
||||
proc clauseSummary*(infos: openArray[ClauseInfo]): ClauseSummary =
|
||||
var maxLen = 0
|
||||
var lenSum = 0
|
||||
var posSum = 0
|
||||
var posN = 0
|
||||
var negSum = 0
|
||||
var negN = 0
|
||||
var lengths: seq[int]
|
||||
var posLengths, negLengths: seq[int]
|
||||
var nClasses = 0
|
||||
for inf in infos:
|
||||
if inf.cls + 1 > nClasses: nClasses = inf.cls + 1
|
||||
var classLengths = newSeq[seq[int]](nClasses)
|
||||
for inf in infos:
|
||||
inc result.totalClauses
|
||||
if inf.cls >= 0 and inf.cls < nClasses:
|
||||
if inf.length > 0: classLengths[inf.cls].add inf.length
|
||||
if inf.length == 0:
|
||||
inc result.emptyClauses
|
||||
else:
|
||||
inc result.nonEmpty
|
||||
lenSum += inf.length
|
||||
lengths.add inf.length
|
||||
if inf.length > maxLen: maxLen = inf.length
|
||||
if inf.polarity > 0:
|
||||
posSum += inf.length; inc posN; posLengths.add inf.length
|
||||
else:
|
||||
negSum += inf.length; inc negN; negLengths.add inf.length
|
||||
if inf.votes > 0:
|
||||
inc result.firedAtLeastOnce
|
||||
if inf.polarity > 0: inc result.posFired
|
||||
@@ -220,6 +303,26 @@ proc clauseSummary*(infos: openArray[ClauseInfo]): ClauseSummary =
|
||||
if inf.length > 0: inc result.lengthHist[inf.length]
|
||||
result.meanLength =
|
||||
if result.nonEmpty > 0: lenSum.float / result.nonEmpty.float else: 0.0
|
||||
let stats = lengthStats(lengths)
|
||||
result.minLength = stats.minL
|
||||
result.medianLength = stats.median
|
||||
result.p10Length = stats.p10
|
||||
result.p90Length = stats.p90
|
||||
result.stdLength = stats.stdDev
|
||||
result.posMeanLength = if posN > 0: posSum.float / posN.float else: 0.0
|
||||
result.negMeanLength = if negN > 0: negSum.float / negN.float else: 0.0
|
||||
result.emptyFraction =
|
||||
if result.totalClauses > 0:
|
||||
result.emptyClauses.float / result.totalClauses.float
|
||||
else: 0.0
|
||||
result.perClassMeanLength = newSeq[float](nClasses)
|
||||
result.perClassMedianLength = newSeq[float](nClasses)
|
||||
result.perClassHist = newSeq[seq[int]](nClasses)
|
||||
for c in 0..<nClasses:
|
||||
let cs = lengthStats(classLengths[c])
|
||||
result.perClassMeanLength[c] = cs.mean
|
||||
result.perClassMedianLength[c] = cs.median
|
||||
result.perClassHist[c] = cs.hist
|
||||
|
||||
type
|
||||
ClassClauseBalance* = object
|
||||
@@ -298,6 +401,200 @@ proc necessaryLiterals*(m: TmMachine, samples: openArray[DiagSample],
|
||||
result = keep
|
||||
result.sort()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GROUP 8 — CLAUSE SHAPE (Task 2 of the clause-shape checker)
|
||||
# length distribution + healthy-band verdict + per-block length contribution
|
||||
# + coverage / voting concentration.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
proc clauseShapeVerdict*(summ: ClauseSummary, healthyLo = 3.0,
|
||||
healthyHi = 8.0, collapsedMax = 2.0): string =
|
||||
## Judge the mean non-empty clause length against a HEALTHY BAND.
|
||||
##
|
||||
## For a problem with ~50 input bits a healthy mean is roughly 3-8 literals:
|
||||
## a rule that needs ~19 conditions to fire is almost certainly fitting noise,
|
||||
## and a mean of 1-2 means the clauses learned almost nothing (they are
|
||||
## essentially single-literal stubs). The band is a PARAMETER of the call so a
|
||||
## caller can retune it per encoding.
|
||||
##
|
||||
## Returns one of: "healthy" | "too long" | "collapsed" | "short" | "n/a".
|
||||
## mean > healthyHi -> too long
|
||||
## mean < collapsedMax -> collapsed
|
||||
## collapsedMax <= mean < healthyLo -> short (below band, not yet collapsed)
|
||||
## healthyLo <= mean <= healthyHi -> healthy
|
||||
if summ.nonEmpty == 0: return "n/a"
|
||||
let mean = summ.meanLength
|
||||
if mean > healthyHi: "too long"
|
||||
elif mean < collapsedMax: "collapsed"
|
||||
elif mean < healthyLo: "short"
|
||||
else: "healthy"
|
||||
|
||||
proc lengthHistText*(hist: openArray[int]): string =
|
||||
## One line per length, e.g. `len 3: 12 ####`.
|
||||
var mx = 0
|
||||
for c in hist:
|
||||
if c > mx: mx = c
|
||||
for l in 0..<hist.len:
|
||||
if hist[l] == 0: continue
|
||||
let bar = "#".repeat(min(40, hist[l] * 40 div max(1, mx)))
|
||||
result.add &" len {l:>3}: {hist[l]:>6} {bar}\n"
|
||||
|
||||
type
|
||||
BlockLengthContribution* = object
|
||||
name*: string
|
||||
first*: int
|
||||
count*: int
|
||||
totalLits*: int ## literals from this block across the clauses
|
||||
meanPerClause*: float ## totalLits / number of counted clauses
|
||||
share*: float ## totalLits / all literals across counted clauses
|
||||
clausesUsing*: int ## clauses with >= 1 literal from this block
|
||||
perClauseMax*: int ## most literals this block contributes to one clause
|
||||
|
||||
proc blockLengthContributions*(m: TmMachine, spec: FeatureSpec,
|
||||
skipEmpty = true): seq[BlockLengthContribution] =
|
||||
## Per input BLOCK (walls / us / motion / bullets from the `FeatureSpec`): how
|
||||
## many literals does it contribute on average across clauses?
|
||||
##
|
||||
## A block that contributes many literals to EVERY clause is dominating the
|
||||
## model; a block contributing none is dead (the block-level view of the dead
|
||||
## INPUT list). `skipEmpty = true` (default) averages over non-empty clauses
|
||||
## only, so empty clauses do not dilute the shares.
|
||||
result = newSeq[BlockLengthContribution](spec.blocks.len)
|
||||
for i, b in spec.blocks:
|
||||
result[i].name = b.name
|
||||
result[i].first = b.first
|
||||
result[i].count = b.count
|
||||
var nClauses = 0
|
||||
var total = 0
|
||||
for c in 0..<m.nClasses:
|
||||
for cl in 0..<m.nClauses:
|
||||
let ls = m.clauseLits(c, cl)
|
||||
if skipEmpty and ls.len == 0: continue
|
||||
inc nClauses
|
||||
var perBlock = newSeq[int](spec.blocks.len)
|
||||
for lit in ls:
|
||||
let raw = if lit < m.nBits: lit else: lit - m.nBits
|
||||
let bi = spec.blockOf(raw)
|
||||
if bi >= 0:
|
||||
inc perBlock[bi]
|
||||
inc total
|
||||
for i in 0..<spec.blocks.len:
|
||||
result[i].totalLits += perBlock[i]
|
||||
if perBlock[i] > 0: inc result[i].clausesUsing
|
||||
if perBlock[i] > result[i].perClauseMax:
|
||||
result[i].perClauseMax = perBlock[i]
|
||||
for i in 0..<spec.blocks.len:
|
||||
result[i].meanPerClause =
|
||||
if nClauses > 0: result[i].totalLits.float / nClauses.float else: 0.0
|
||||
result[i].share = if total > 0: result[i].totalLits.float / total.float else: 0.0
|
||||
|
||||
type
|
||||
ClauseCoverage* = object
|
||||
samples*: int
|
||||
totalClauses*: int
|
||||
meanFiringClauses*: float ## mean non-empty firing clauses per sample
|
||||
meanFiringFraction*: float ## ... / totalClauses
|
||||
effectiveClauses*: float ## participation ratio of the vote counts
|
||||
top3Share*: float ## share of all fires from the 3 busiest clauses
|
||||
firingClauseCount*: int ## clauses that fire at least once
|
||||
|
||||
proc clauseCoverage*(m: TmMachine, samples: openArray[DiagSample]):
|
||||
ClauseCoverage =
|
||||
## How many clauses are actually needed to cover the cases?
|
||||
##
|
||||
## `meanFiringFraction` is the fraction of clauses that fire on any given
|
||||
## sample. `effectiveClauses` is the participation ratio
|
||||
## `(sum v)^2 / sum v^2` of the per-clause fire counts: if only 3 clauses cast
|
||||
## most of the votes, it reads ~3 regardless of the configured count.
|
||||
result.totalClauses = m.nClasses * m.nClauses
|
||||
result.samples = samples.len
|
||||
if samples.len == 0 or result.totalClauses == 0: return
|
||||
var counts = newSeq[int](result.totalClauses)
|
||||
var fireSum = 0.0
|
||||
for s in samples:
|
||||
var firing = 0
|
||||
for c in 0..<m.nClasses:
|
||||
for cl in 0..<m.nClauses:
|
||||
if m.clauseFires(c, cl, s.lits):
|
||||
inc firing
|
||||
inc counts[c * m.nClauses + cl]
|
||||
fireSum += firing.float
|
||||
result.meanFiringClauses = fireSum / samples.len.float
|
||||
result.meanFiringFraction = result.meanFiringClauses / result.totalClauses.float
|
||||
var totalVotes = 0
|
||||
var sqVotes = 0.0
|
||||
for v in counts:
|
||||
if v > 0: inc result.firingClauseCount
|
||||
totalVotes += v
|
||||
sqVotes += v.float * v.float
|
||||
if sqVotes > 0.0:
|
||||
result.effectiveClauses = totalVotes.float * totalVotes.float / sqVotes
|
||||
var sorted = counts
|
||||
sorted.sort(SortOrder.Descending)
|
||||
let k = min(3, sorted.len)
|
||||
var top = 0
|
||||
for i in 0..<k: top += sorted[i]
|
||||
result.top3Share = if totalVotes > 0: top.float / totalVotes.float else: 0.0
|
||||
|
||||
type
|
||||
ClauseShapeDiag* = object
|
||||
summary*: ClauseSummary
|
||||
verdict*: string
|
||||
blocks*: seq[BlockLengthContribution]
|
||||
coverage*: ClauseCoverage
|
||||
healthyLo*: float
|
||||
healthyHi*: float
|
||||
collapsedMax*: float
|
||||
|
||||
proc clauseShapeDiagnostics*(m: TmMachine, samples: openArray[DiagSample],
|
||||
spec: FeatureSpec, healthyLo = 3.0,
|
||||
healthyHi = 8.0, collapsedMax = 2.0,
|
||||
skipEmpty = true): ClauseShapeDiag =
|
||||
## The one-call clause-shape readout: length distribution + healthy-band
|
||||
## verdict + per-block length contribution + coverage/concentration.
|
||||
result.summary = clauseSummary(clauseInfo(m, samples, spec))
|
||||
result.healthyLo = healthyLo
|
||||
result.healthyHi = healthyHi
|
||||
result.collapsedMax = collapsedMax
|
||||
result.verdict = clauseShapeVerdict(result.summary, healthyLo, healthyHi,
|
||||
collapsedMax)
|
||||
result.blocks = blockLengthContributions(m, spec, skipEmpty)
|
||||
result.coverage = clauseCoverage(m, samples)
|
||||
|
||||
proc shapeLine*(d: ClauseShapeDiag): string =
|
||||
## One-line clause-shape summary, in the same style as `healthLine`.
|
||||
let s = d.summary
|
||||
&"shape mean={s.meanLength:.2f} med={s.medianLength:.1f} " &
|
||||
&"p10={s.p10Length:.1f} p90={s.p90Length:.1f} max={s.maxLength} " &
|
||||
&"({d.verdict}) | coverage={d.coverage.meanFiringFraction*100:.1f}%/sample " &
|
||||
&"eff={d.coverage.effectiveClauses:.1f}/{d.coverage.totalClauses} " &
|
||||
&"top3={d.coverage.top3Share*100:.0f}%"
|
||||
|
||||
proc formatClauseShapeReport*(d: ClauseShapeDiag): string =
|
||||
## Full clause-shape readout: distribution, per-polarity / per-class means,
|
||||
## per-block contribution and coverage.
|
||||
let s = d.summary
|
||||
result.add &"shape: mean={s.meanLength:.2f} median={s.medianLength:.2f} " &
|
||||
&"p10={s.p10Length:.2f} p90={s.p90Length:.2f} std={s.stdLength:.2f} " &
|
||||
&"min={s.minLength} max={s.maxLength} nonEmpty={s.nonEmpty} " &
|
||||
&"empty={s.emptyClauses}/{s.totalClauses} " &
|
||||
&"({d.verdict}, healthy band {d.healthyLo:.0f}-{d.healthyHi:.0f})\n"
|
||||
result.add &" polarity: posMean={s.posMeanLength:.2f} negMean={s.negMeanLength:.2f}\n"
|
||||
result.add " per-class mean/median:"
|
||||
for c in 0..<s.perClassMeanLength.len:
|
||||
result.add &" c{c}={s.perClassMeanLength[c]:.2f}/{s.perClassMedianLength[c]:.1f}"
|
||||
result.add "\n"
|
||||
result.add " length histogram:\n" & lengthHistText(s.lengthHist)
|
||||
result.add " per-block literal contribution (share of all clause literals):\n"
|
||||
for b in d.blocks:
|
||||
result.add &" {b.name:<28} mean/clause={b.meanPerClause:5.2f} " &
|
||||
&"share={b.share*100:5.1f}% clausesUsing={b.clausesUsing:<5} max={b.perClauseMax}\n"
|
||||
result.add &" coverage: firing/sample={d.coverage.meanFiringClauses:.2f} " &
|
||||
&"({d.coverage.meanFiringFraction*100:.1f}%) effectiveClauses=" &
|
||||
&"{d.coverage.effectiveClauses:.2f}/{d.coverage.totalClauses} " &
|
||||
&"top3Share={d.coverage.top3Share*100:.1f}% " &
|
||||
&"firingClauses={d.coverage.firingClauseCount}\n"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GROUP 3 — per-feature contribution + DEAD-INPUT LIST
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1033,6 +1330,7 @@ type
|
||||
disagreement*: VoteDisagreementResult
|
||||
histogram*: StateHistogram
|
||||
inputConfidence*: seq[InputConfidence]
|
||||
shape*: ClauseShapeDiag
|
||||
summary*: string
|
||||
|
||||
proc settlednessVerdict*(s: SettlednessResult): string =
|
||||
@@ -1061,14 +1359,19 @@ proc disagreementVerdict*(d: VoteDisagreementResult): string =
|
||||
else: "fragmented"
|
||||
|
||||
proc healthLine*(d: AutomataDiag): string =
|
||||
## ONE line to glance at: settledness / diversity / churn trend / disagreement,
|
||||
## each with its own verdict word.
|
||||
&"settledness={d.settledness.overallMean:.3f} ({settlednessVerdict(d.settledness)}) | " &
|
||||
&"diversity={d.diversity.jaccardOverall:.3f} ({diversityVerdict(d.diversity)}) | " &
|
||||
&"churn={d.churn.flipRatePer100:.3f}/100 {d.churn.flipTrend} " &
|
||||
&"({churnVerdict(d.churn)}) | " &
|
||||
&"disagreement={d.disagreement.overall:.3f} " &
|
||||
&"({disagreementVerdict(d.disagreement)})"
|
||||
## ONE line to glance at: settledness / diversity / churn trend / disagreement
|
||||
## / CLAUSE SHAPE, each with its own verdict word. The shape clause is included
|
||||
## only when the shape readout was computed (a manually-built `AutomataDiag`
|
||||
## without `shape` leaves it out).
|
||||
result =
|
||||
&"settledness={d.settledness.overallMean:.3f} ({settlednessVerdict(d.settledness)}) | " &
|
||||
&"diversity={d.diversity.jaccardOverall:.3f} ({diversityVerdict(d.diversity)}) | " &
|
||||
&"churn={d.churn.flipRatePer100:.3f}/100 {d.churn.flipTrend} " &
|
||||
&"({churnVerdict(d.churn)}) | " &
|
||||
&"disagreement={d.disagreement.overall:.3f} " &
|
||||
&"({disagreementVerdict(d.disagreement)})"
|
||||
if d.shape.summary.totalClauses > 0:
|
||||
result.add &" | shape={d.shape.summary.meanLength:.2f} ({d.shape.verdict})"
|
||||
|
||||
proc automataDiagnostics*(tmpl: TmMachine, samples: openArray[DiagSample],
|
||||
spec: FeatureSpec, epochs = 1, seed = 777'u64,
|
||||
@@ -1092,6 +1395,7 @@ proc automataDiagnostics*(tmpl: TmMachine, samples: openArray[DiagSample],
|
||||
result.histogram = stateHistogram(result.machine, nHistBins)
|
||||
result.inputConfidence = perInputConfidence(result.machine, spec, samples,
|
||||
settleThreshold)
|
||||
result.shape = clauseShapeDiagnostics(result.machine, samples, spec)
|
||||
result.summary = healthLine(result)
|
||||
|
||||
proc automataVerdict*(d: AutomataDiag): string =
|
||||
@@ -1139,3 +1443,5 @@ proc formatAutomataReport*(d: AutomataDiag): string =
|
||||
for c in 0..<d.disagreement.perClass.len:
|
||||
result.add &" c{c}={d.disagreement.perClass[c]:.3f}"
|
||||
result.add "\n"
|
||||
if d.shape.summary.totalClauses > 0:
|
||||
result.add formatClauseShapeReport(d.shape)
|
||||
|
||||
Reference in New Issue
Block a user