# tm_diag — the Tsetlin diagnostics kit First-class, offline diagnostics for a Tsetlin-Machine head. Answers the two questions that clause-reading alone cannot: > **Is the TM learning badly, or is the data bad?** Built before the new TM gun, so a bad design cannot hide behind the data and a bad dataset cannot hide behind the model. Everything is pure and offline — no battles, no Java, no harness. ## Files | file | contents | |---|---| | `feature_spec.nim` | `FeatureSpec`, `describe`, `describeClause`, `draftTMSpec()` (49-bit draft), `tmPatternSpec()` (40-bit shipped encoding) | | `tm_core.nim` | compact deterministic Granmo Table 2/3 multiclass TM (mirrors the tm_pattern core), introspectable clause layout | | `diagnostics.nim` | the six groups (re-exports the two above) | Import everything with: ```nim import tm_diag/diagnostics ``` ## Task 1 — named features / clause rendering ```nim let spec = draftTMSpec() # 49 bits, all one-hot spec.nBits # 49 spec.describe(45) # "lat DEAD-ON -18..+18" spec.describeLiteral(49 + 45) # "NOT lat DEAD-ON -18..+18" spec.describeClause(@[0, 45], 2) # "IF dist-wall<50 AND lat DEAD-ON -18..+18 THEN class=2" spec.describeClause(@[], 1) # "IF TRUE (empty clause) THEN class=1" ``` A block is added with `addBlock(name, count, bitNames?)`; a bit with no explicit name renders as `blockName[k]`, and a single-bit block renders as its name. `tmPatternSpec()` mirrors `guns/tm_pattern.nim`'s `tmBuildBits` exactly. **Its two last bits (`UNUSED-38/39`) are a real bug**: `tmBuildBits` writes only 38 raw bits into an `array[TM_NBITS=40, uint8]`, so bits 38 and 39 are always 0 and their negations always 1. The kit reports them as constant dead inputs. ## Task 2 — the six groups All functions take a trained `TmMachine` (or an externally supplied clause set) plus `seq[DiagSample]` where `DiagSample.lits` is the pos-then-neg literal vector and `DiagSample.label` the true class. ```nim # build samples from raw bits let s = makeSample(nBits, rawBits, label, order) # 1. pre-flight DATA checks let dc = dataChecks(labels, nClasses, threshold = 0.30) # dc.classCounts, dc.classShares, dc.majorityClass, dc.majorityShare, # dc.majorityAccuracy, dc.overThreshold, dc.flags let sc = shuffledLabelControl(tmplMachine, samples) # (acc, majority, ...) # 2. clause introspection let infos = clauseInfo(m, samples, spec) let summ = clauseSummary(infos) # empty / neverFired / length hist for c in topClauses(infos, 10): echo c.text, " votes=", c.votes for cb in clauseBalanceByClass(infos, nClasses): echo cb for cl in 0..