@hoox-sh/pynets 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +20 -9
  3. package/dist/browser.js +95 -34
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +23002 -12427
  6. package/package.json +1 -1
  7. package/src/ast/builder.ts +235 -12
  8. package/src/ast/nodes.ts +114 -4
  9. package/src/ast/unparse.ts +192 -17
  10. package/src/cli/rich.ts +4 -3
  11. package/src/cli.ts +29 -7
  12. package/src/generated/PinescriptLexer.interp +5 -1
  13. package/src/generated/PinescriptLexer.tokens +97 -95
  14. package/src/generated/PinescriptLexer.ts +512 -502
  15. package/src/generated/PinescriptParser.interp +8 -1
  16. package/src/generated/PinescriptParser.tokens +97 -95
  17. package/src/generated/PinescriptParser.ts +1907 -1287
  18. package/src/generated/PinescriptParserVisitor.ts +36 -1
  19. package/src/index.ts +22 -0
  20. package/src/runtime/array.ts +238 -68
  21. package/src/runtime/compile/collections.ts +492 -0
  22. package/src/runtime/compile/draw.ts +181 -0
  23. package/src/runtime/compile/emit.ts +1421 -0
  24. package/src/runtime/compile/emit_call.ts +1796 -0
  25. package/src/runtime/compile/emit_udf.ts +326 -0
  26. package/src/runtime/compile/engine.ts +265 -0
  27. package/src/runtime/compile/helpers.ts +140 -0
  28. package/src/runtime/compile/index.ts +33 -0
  29. package/src/runtime/compile/runtime.ts +357 -0
  30. package/src/runtime/compile/strategy.ts +250 -0
  31. package/src/runtime/compile/types.ts +127 -0
  32. package/src/runtime/compile/udt_runtime.ts +50 -0
  33. package/src/runtime/drawings.ts +405 -8
  34. package/src/runtime/interpret.ts +2387 -281
  35. package/src/runtime/matrix.ts +371 -42
  36. package/src/runtime/strategy.ts +695 -33
  37. package/src/runtime/ta.ts +654 -20
  38. package/src/runtime/timeframe.ts +199 -0
@@ -3,9 +3,12 @@
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  *
5
5
  * Tiny Pine `matrix.*` value. `null` is `na`. Get is na-safe (OOB / non-finite → na).
6
- * Set OOB is a no-op. Python `Matrix` remains the source of truth.
6
+ * Set OOB is a no-op. Slots may hold numbers, na, or UDT instances.
7
+ * Python `Matrix` remains the source of truth.
7
8
  */
8
9
 
10
+ import { UdtInstance } from "./udt.ts";
11
+
9
12
  export type Cell = number | null;
10
13
 
11
14
  /** Python has no matrix-size cap; bound elements like `array.set` (1e6). */
@@ -29,13 +32,176 @@ function fitsElements(rows: number, cols: number): boolean {
29
32
  }
30
33
 
31
34
  /** Pad with `na` or truncate so the vector has length *n*. */
32
- function padOrTrunc(values: Cell[] | undefined, n: number): Cell[] {
33
- const out: Cell[] = [];
35
+ function padOrTrunc(values: unknown[] | undefined, n: number): unknown[] {
36
+ const out: unknown[] = [];
34
37
  const src = values ?? [];
35
38
  for (let i = 0; i < n; i++) out.push(i < src.length ? src[i]! : null);
36
39
  return out;
37
40
  }
38
41
 
42
+ function looksLikeUdt(item: unknown): boolean {
43
+ if (item == null || typeof item !== "object") return false;
44
+ if (item instanceof UdtInstance) return true;
45
+ return !Array.isArray(item) && Object.prototype.hasOwnProperty.call(item, "__type__");
46
+ }
47
+
48
+ function udtFieldNames(item: object): string[] | null {
49
+ if (item instanceof UdtInstance) return item.type.fields.map((f) => f.name);
50
+ const rec = item as Record<string, unknown>;
51
+ if (Object.prototype.hasOwnProperty.call(rec, "__type__")) {
52
+ return Object.keys(rec).filter((k) => k !== "__type__");
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function readUdtField(item: object, name: string): unknown {
58
+ if (item instanceof UdtInstance) return item.get(name);
59
+ return (item as Record<string, unknown>)[name];
60
+ }
61
+
62
+ function udtFieldKey(item: unknown, sortField: unknown): unknown {
63
+ if (item == null || sortField == null || typeof item !== "object") return item;
64
+ if (typeof sortField === "string") return readUdtField(item, sortField);
65
+ if (typeof sortField === "number" && Number.isFinite(sortField)) {
66
+ const names = udtFieldNames(item);
67
+ if (names == null) return item;
68
+ const idx = Math.trunc(sortField);
69
+ if (idx < 0 || idx >= names.length) return item;
70
+ return readUdtField(item, names[idx]!);
71
+ }
72
+ return item;
73
+ }
74
+
75
+ function cellSortValue(cell: unknown, sortField: unknown): unknown {
76
+ return sortField != null ? udtFieldKey(cell, sortField) : cell;
77
+ }
78
+
79
+ function isSortNa(v: unknown): boolean {
80
+ if (v == null) return true;
81
+ if (typeof v === "number") return !Number.isFinite(v);
82
+ return false;
83
+ }
84
+
85
+ function asFiniteNumber(v: unknown): number | null {
86
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
87
+ }
88
+
89
+ /** Python `isinstance(x, int | float)` — bool counts; `na` / non-finite does not. */
90
+ function numericCell(v: unknown): number | null {
91
+ if (typeof v === "boolean") return v ? 1 : 0;
92
+ return asFiniteNumber(v);
93
+ }
94
+
95
+ function sumNumeric(cells: readonly unknown[]): number {
96
+ let total = 0;
97
+ for (const x of cells) {
98
+ const n = numericCell(x);
99
+ if (n !== null) total += n;
100
+ }
101
+ return total;
102
+ }
103
+
104
+ /** Empty numeric line → 0 (Python `avg_row` / `avg_col`). */
105
+ function avgNumeric(cells: readonly unknown[]): number {
106
+ let total = 0;
107
+ let count = 0;
108
+ for (const x of cells) {
109
+ const n = numericCell(x);
110
+ if (n === null) continue;
111
+ total += n;
112
+ count += 1;
113
+ }
114
+ return count > 0 ? total / count : 0;
115
+ }
116
+
117
+ function extremumNumeric(cells: readonly unknown[], max: boolean): Cell {
118
+ let best: number | null = null;
119
+ for (const x of cells) {
120
+ const n = numericCell(x);
121
+ if (n === null) continue;
122
+ if (best === null || (max ? n > best : n < best)) best = n;
123
+ }
124
+ return best;
125
+ }
126
+
127
+ /** Python `Counter.most_common`: first-seen wins ties. Non-finite is `na`. */
128
+ function modeOf(values: readonly unknown[]): unknown {
129
+ if (values.length === 0) return null;
130
+ const keys: unknown[] = [];
131
+ const counts: number[] = [];
132
+ let bestI = 0;
133
+ let bestN = 0;
134
+ for (const raw of values) {
135
+ const key = typeof raw === "number" && !Number.isFinite(raw) ? null : (raw ?? null);
136
+ let idx = -1;
137
+ for (let i = 0; i < keys.length; i++) {
138
+ if (keys[i] === key) {
139
+ idx = i;
140
+ break;
141
+ }
142
+ }
143
+ if (idx < 0) {
144
+ idx = keys.length;
145
+ keys.push(key);
146
+ counts.push(1);
147
+ } else {
148
+ counts[idx] = (counts[idx] ?? 0) + 1;
149
+ }
150
+ const n = counts[idx] ?? 0;
151
+ if (n > bestN) {
152
+ bestN = n;
153
+ bestI = idx;
154
+ }
155
+ }
156
+ return keys[bestI] ?? null;
157
+ }
158
+
159
+ /** Sample moments (Python `statistics.variance` / `stdev`, divisor n−1). */
160
+ function sampleMoments(vals: readonly number[]): { variance: number; stdev: number } | null {
161
+ const n = vals.length;
162
+ if (n < 2) return null;
163
+ let sum = 0;
164
+ for (const x of vals) sum += x;
165
+ const mean = sum / n;
166
+ let ss = 0;
167
+ let dev = 0;
168
+ for (const x of vals) {
169
+ const d = x - mean;
170
+ dev += d;
171
+ ss += d * d;
172
+ }
173
+ ss -= (dev * dev) / n;
174
+ if (ss < 0) ss = 0;
175
+ const variance = ss / (n - 1);
176
+ const stdev = Math.sqrt(variance);
177
+ if (!Number.isFinite(variance) || !Number.isFinite(stdev)) return null;
178
+ return { variance, stdev };
179
+ }
180
+
181
+ function defaultMatrixSortField(rows: readonly unknown[][], column: number): unknown {
182
+ for (const row of rows) {
183
+ const cell = row[column];
184
+ if (cell == null) continue;
185
+ if (looksLikeUdt(cell)) return 0;
186
+ break;
187
+ }
188
+ return null;
189
+ }
190
+
191
+ function compareSortKeys(a: unknown, b: unknown): number {
192
+ if (a === b) return 0;
193
+ if (typeof a === "number" && typeof b === "number") {
194
+ if (a < b) return -1;
195
+ if (a > b) return 1;
196
+ return 0;
197
+ }
198
+ const sa = `${String(Object.prototype.toString.call(a))}${String(a)}`;
199
+ const sb = `${String(Object.prototype.toString.call(b))}${String(b)}`;
200
+ if (sa < sb) return -1;
201
+ if (sa > sb) return 1;
202
+ return 0;
203
+ }
204
+
39
205
  function cloneGrid(src: number[][]): number[][] {
40
206
  const out: number[][] = [];
41
207
  for (let i = 0; i < src.length; i++) out.push(src[i]!.slice());
@@ -346,11 +512,11 @@ function nullspaceUnit(M: number[][]): number[] | null {
346
512
  }
347
513
 
348
514
  export class PineMatrix {
349
- private readonly cells: Cell[][] = [];
515
+ private readonly cells: unknown[][] = [];
350
516
  private nRows: number;
351
517
  private nCols: number;
352
518
 
353
- constructor(rows: number, cols: number, initial?: Cell) {
519
+ constructor(rows: number, cols: number, initial?: unknown) {
354
520
  let r = dim(rows);
355
521
  let c = dim(cols);
356
522
  if (r > 0 && c > Math.floor(MAX_MATRIX_ELEMENTS / r)) {
@@ -359,9 +525,9 @@ export class PineMatrix {
359
525
  }
360
526
  this.nRows = r;
361
527
  this.nCols = c;
362
- const fill: Cell = initial === undefined ? null : initial;
528
+ const fill = initial === undefined ? null : initial;
363
529
  for (let i = 0; i < this.nRows; i++) {
364
- const row: Cell[] = [];
530
+ const row: unknown[] = [];
365
531
  for (let j = 0; j < this.nCols; j++) row.push(fill);
366
532
  this.cells.push(row);
367
533
  }
@@ -380,18 +546,18 @@ export class PineMatrix {
380
546
  const i = resolveIndex(row, this.nRows);
381
547
  const j = resolveIndex(col, this.nCols);
382
548
  if (i === null || j === null) return null;
383
- return this.cells[i]![j]!;
549
+ return this.cells[i]![j] as Cell;
384
550
  }
385
551
 
386
552
  /** `matrix.set` — OOB / non-finite index is a no-op. */
387
- set(row: number, col: number, value: Cell): void {
553
+ set(row: number, col: number, value: unknown): void {
388
554
  const i = resolveIndex(row, this.nRows);
389
555
  const j = resolveIndex(col, this.nCols);
390
556
  if (i === null || j === null) return;
391
557
  this.cells[i]![j] = value;
392
558
  }
393
559
 
394
- fill(value: Cell): void {
560
+ fill(value: unknown): void {
395
561
  for (let i = 0; i < this.nRows; i++) {
396
562
  const row = this.cells[i]!;
397
563
  for (let j = 0; j < this.nCols; j++) row[j] = value;
@@ -408,14 +574,14 @@ export class PineMatrix {
408
574
 
409
575
  row(i: number): Cell[] {
410
576
  const r = resolveIndex(i, this.nRows);
411
- return r === null ? [] : this.cells[r]!.slice();
577
+ return r === null ? [] : (this.cells[r]!.slice() as Cell[]);
412
578
  }
413
579
 
414
580
  col(j: number): Cell[] {
415
581
  const c = resolveIndex(j, this.nCols);
416
582
  if (c === null) return [];
417
583
  const out: Cell[] = [];
418
- for (let i = 0; i < this.nRows; i++) out.push(this.cells[i]![c]!);
584
+ for (let i = 0; i < this.nRows; i++) out.push(this.cells[i]![c] as Cell);
419
585
  return out;
420
586
  }
421
587
 
@@ -473,8 +639,8 @@ export class PineMatrix {
473
639
  if (!this.isSquare()) return null;
474
640
  let acc = 0;
475
641
  for (let i = 0; i < this.nRows; i++) {
476
- const v = this.cells[i]![i]!;
477
- if (v === null || !Number.isFinite(v)) return null;
642
+ const v = asFiniteNumber(this.cells[i]![i]);
643
+ if (v === null) return null;
478
644
  acc += v;
479
645
  }
480
646
  return acc;
@@ -489,8 +655,8 @@ export class PineMatrix {
489
655
  for (let i = 0; i < n; i++) {
490
656
  const row: number[] = [];
491
657
  for (let j = 0; j < n; j++) {
492
- const v = this.cells[i]![j]!;
493
- if (v === null || !Number.isFinite(v)) return null;
658
+ const v = asFiniteNumber(this.cells[i]![j]);
659
+ if (v === null) return null;
494
660
  row.push(v);
495
661
  }
496
662
  a.push(row);
@@ -536,8 +702,8 @@ export class PineMatrix {
536
702
  const out = new PineMatrix(this.nRows, this.nCols);
537
703
  for (let i = 0; i < this.nRows; i++) {
538
704
  for (let j = 0; j < this.nCols; j++) {
539
- const v = this.cells[i]![j]!;
540
- if (v === null || !Number.isFinite(v)) {
705
+ const v = asFiniteNumber(this.cells[i]![j]);
706
+ if (v === null) {
541
707
  out.set(i, j, null);
542
708
  continue;
543
709
  }
@@ -773,7 +939,7 @@ export class PineMatrix {
773
939
  * Short rows pad `na`; long rows truncate. 0×0 adopts column count from *values*.
774
940
  * Negative / non-finite index or size cap → no-op.
775
941
  */
776
- addRow(index?: number, values?: Cell[]): void {
942
+ addRow(index?: number, values?: unknown[]): void {
777
943
  const adopt = this.nRows === 0 && this.nCols === 0;
778
944
  const cols = adopt ? (values?.length ?? 0) : this.nCols;
779
945
  if (!fitsElements(this.nRows + 1, cols)) return;
@@ -793,7 +959,7 @@ export class PineMatrix {
793
959
  * Insert a column at *index* (append if omitted / past end).
794
960
  * Short cols pad `na`; long cols truncate. 0×0 becomes N×1 from *values*.
795
961
  */
796
- addCol(index?: number, values?: Cell[]): void {
962
+ addCol(index?: number, values?: unknown[]): void {
797
963
  if (this.nRows === 0 && this.nCols === 0) {
798
964
  const n = values?.length ?? 0;
799
965
  if (!fitsElements(n, n === 0 ? 0 : 1)) return;
@@ -891,35 +1057,60 @@ export class PineMatrix {
891
1057
  for (const row of this.cells) row.reverse();
892
1058
  }
893
1059
 
894
- /** Sort rows by *column*. `na` / non-finite keys always last. */
895
- sort(column = 0, order: "asc" | "desc" = "asc"): void {
1060
+ /** Sort rows by *column*. `na` last. Optional *sortField* keys UDT cells. */
1061
+ sort(column = 0, order: "asc" | "desc" = "asc", sortField?: unknown): void {
896
1062
  if (this.nRows === 0) return;
897
1063
  const c = resolveIndex(column, this.nCols);
898
1064
  if (c === null) return;
899
1065
  const desc = order === "desc";
900
- const nonNa: Cell[][] = [];
901
- const naRows: Cell[][] = [];
1066
+ const field = sortField ?? defaultMatrixSortField(this.cells, c);
1067
+ const nonNa: unknown[][] = [];
1068
+ const naRows: unknown[][] = [];
902
1069
  for (const row of this.cells) {
903
- const v = row[c]!;
904
- if (v === null || !Number.isFinite(v)) naRows.push(row);
1070
+ const v = cellSortValue(row[c], field);
1071
+ if (isSortNa(v)) naRows.push(row);
905
1072
  else nonNa.push(row);
906
1073
  }
907
1074
  nonNa.sort((a, b) => {
908
- const d = (a[c] as number) - (b[c] as number);
909
- return desc ? -d : d;
1075
+ const ka = cellSortValue(a[c], field);
1076
+ const kb = cellSortValue(b[c], field);
1077
+ const cmp = compareSortKeys(ka, kb);
1078
+ return desc ? -cmp : cmp;
910
1079
  });
911
1080
  this.cells.length = 0;
912
1081
  for (const row of nonNa) this.cells.push(row);
913
1082
  for (const row of naRows) this.cells.push(row);
914
1083
  }
915
1084
 
1085
+ /** Row indices that would sort by *column*; `na` indices last. */
1086
+ sortIndices(column = 0, order: "asc" | "desc" = "asc", sortField?: unknown): number[] {
1087
+ if (this.nRows === 0) return [];
1088
+ const c = resolveIndex(column, this.nCols);
1089
+ if (c === null) return [];
1090
+ const desc = order === "desc";
1091
+ const field = sortField ?? defaultMatrixSortField(this.cells, c);
1092
+ const nonNa: { key: unknown; idx: number }[] = [];
1093
+ const naIdx: number[] = [];
1094
+ for (let i = 0; i < this.nRows; i++) {
1095
+ const v = cellSortValue(this.cells[i]![c], field);
1096
+ if (isSortNa(v)) naIdx.push(i);
1097
+ else nonNa.push({ key: v, idx: i });
1098
+ }
1099
+ nonNa.sort((a, b) => {
1100
+ const cmp = compareSortKeys(a.key, b.key);
1101
+ if (cmp !== 0) return desc ? -cmp : cmp;
1102
+ return a.idx - b.idx;
1103
+ });
1104
+ return nonNa.map((x) => x.idx).concat(naIdx);
1105
+ }
1106
+
916
1107
  /** Median of finite cells (Python skips `na`). Empty → `na`. */
917
1108
  median(): Cell {
918
1109
  const vals: number[] = [];
919
1110
  for (let i = 0; i < this.nRows; i++) {
920
1111
  for (let j = 0; j < this.nCols; j++) {
921
- const v = this.cells[i]![j]!;
922
- if (v !== null && Number.isFinite(v)) vals.push(v);
1112
+ const v = asFiniteNumber(this.cells[i]![j]);
1113
+ if (v !== null) vals.push(v);
923
1114
  }
924
1115
  }
925
1116
  if (vals.length === 0) return null;
@@ -936,8 +1127,8 @@ export class PineMatrix {
936
1127
  let bestCount = 0;
937
1128
  for (let i = 0; i < this.nRows; i++) {
938
1129
  for (let j = 0; j < this.nCols; j++) {
939
- const v = this.cells[i]![j]!;
940
- if (v === null || !Number.isFinite(v)) continue;
1130
+ const v = asFiniteNumber(this.cells[i]![j]);
1131
+ if (v === null) continue;
941
1132
  const n = (counts.get(v) ?? 0) + 1;
942
1133
  counts.set(v, n);
943
1134
  if (n > bestCount) {
@@ -949,6 +1140,114 @@ export class PineMatrix {
949
1140
  return best;
950
1141
  }
951
1142
 
1143
+ /** Copy of the row. OOB → empty (same as `row`). */
1144
+ copyRow(index: number): Cell[] {
1145
+ return this.row(index);
1146
+ }
1147
+
1148
+ /** Copy of the column. OOB → empty (same as `col`). */
1149
+ copyCol(index: number): Cell[] {
1150
+ return this.col(index);
1151
+ }
1152
+
1153
+ /** Sum of numeric cells. Skips `na`. No numeric cells → 0. OOB → `na`. */
1154
+ sumRow(index: number): Cell {
1155
+ const line = this.lineAt("row", index);
1156
+ return line === null ? null : sumNumeric(line);
1157
+ }
1158
+
1159
+ sumCol(index: number): Cell {
1160
+ const line = this.lineAt("col", index);
1161
+ return line === null ? null : sumNumeric(line);
1162
+ }
1163
+
1164
+ /** Mean of numeric cells. Skips `na`. No numeric cells → 0. OOB → `na`. */
1165
+ avgRow(index: number): Cell {
1166
+ const line = this.lineAt("row", index);
1167
+ return line === null ? null : avgNumeric(line);
1168
+ }
1169
+
1170
+ avgCol(index: number): Cell {
1171
+ const line = this.lineAt("col", index);
1172
+ return line === null ? null : avgNumeric(line);
1173
+ }
1174
+
1175
+ /** Min numeric cell. Skips `na`. None → `na`. OOB → `na`. */
1176
+ minRow(index: number): Cell {
1177
+ const line = this.lineAt("row", index);
1178
+ return line === null ? null : extremumNumeric(line, false);
1179
+ }
1180
+
1181
+ minCol(index: number): Cell {
1182
+ const line = this.lineAt("col", index);
1183
+ return line === null ? null : extremumNumeric(line, false);
1184
+ }
1185
+
1186
+ maxRow(index: number): Cell {
1187
+ const line = this.lineAt("row", index);
1188
+ return line === null ? null : extremumNumeric(line, true);
1189
+ }
1190
+
1191
+ maxCol(index: number): Cell {
1192
+ const line = this.lineAt("col", index);
1193
+ return line === null ? null : extremumNumeric(line, true);
1194
+ }
1195
+
1196
+ /** Most common cell in the row, including `na`. Empty / OOB → `na`. */
1197
+ modeRow(index: number): unknown {
1198
+ const line = this.lineAt("row", index);
1199
+ return line === null ? null : modeOf(line);
1200
+ }
1201
+
1202
+ modeCol(index: number): unknown {
1203
+ const line = this.lineAt("col", index);
1204
+ return line === null ? null : modeOf(line);
1205
+ }
1206
+
1207
+ /** Most common cell, row-major, including `na` (Python `Counter`). Empty → `na`. */
1208
+ modeAll(): unknown {
1209
+ return modeOf(this.flatCells());
1210
+ }
1211
+
1212
+ /** OOB / non-finite index is a no-op. */
1213
+ fillRow(index: number, value: unknown): void {
1214
+ const i = resolveIndex(index, this.nRows);
1215
+ if (i === null) return;
1216
+ const row = this.cells[i]!;
1217
+ for (let j = 0; j < this.nCols; j++) row[j] = value;
1218
+ }
1219
+
1220
+ /** OOB / non-finite index is a no-op. */
1221
+ fillCol(index: number, value: unknown): void {
1222
+ const j = resolveIndex(index, this.nCols);
1223
+ if (j === null) return;
1224
+ for (let i = 0; i < this.nRows; i++) this.cells[i]![j] = value;
1225
+ }
1226
+
1227
+ /** Main diagonal, `min(rows, columns)` long. */
1228
+ fillDiagonal(value: unknown): void {
1229
+ const n = Math.min(this.nRows, this.nCols);
1230
+ for (let i = 0; i < n; i++) this.cells[i]![i] = value;
1231
+ }
1232
+
1233
+ reverseRows(): void {
1234
+ this.cells.reverse();
1235
+ }
1236
+
1237
+ reverseCols(): void {
1238
+ for (const row of this.cells) row.reverse();
1239
+ }
1240
+
1241
+ /** Sample stdev of numeric cells (n−1). Skips `na`. Fewer than 2 → `na`. */
1242
+ stdev(): Cell {
1243
+ return sampleMoments(this.numericFlat())?.stdev ?? null;
1244
+ }
1245
+
1246
+ /** Sample variance of numeric cells (n−1). Skips `na`. Fewer than 2 → `na`. */
1247
+ variance(): Cell {
1248
+ return sampleMoments(this.numericFlat())?.variance ?? null;
1249
+ }
1250
+
952
1251
  /** Every cell is 0 or 1. `na` / other values → false. Empty is true. */
953
1252
  isBinary(): boolean {
954
1253
  for (let i = 0; i < this.nRows; i++) {
@@ -965,8 +1264,8 @@ export class PineMatrix {
965
1264
  for (let i = 0; i < this.nRows; i++) {
966
1265
  let total = 0;
967
1266
  for (let j = 0; j < this.nCols; j++) {
968
- const v = this.cells[i]![j]!;
969
- if (v === null || !Number.isFinite(v) || v < 0) return false;
1267
+ const v = asFiniteNumber(this.cells[i]![j]);
1268
+ if (v === null || v < 0) return false;
970
1269
  total += v;
971
1270
  }
972
1271
  if (Math.abs(total - 1) > 1e-9) return false;
@@ -994,9 +1293,9 @@ export class PineMatrix {
994
1293
  const out = new PineMatrix(this.nRows, this.nCols);
995
1294
  for (let i = 0; i < this.nRows; i++) {
996
1295
  for (let j = 0; j < this.nCols; j++) {
997
- const a = this.cells[i]![j]!;
998
- const b = other.cells[i]![j]!;
999
- if (a === null || b === null || !Number.isFinite(a) || !Number.isFinite(b)) {
1296
+ const a = asFiniteNumber(this.cells[i]![j]);
1297
+ const b = asFiniteNumber(other.cells[i]![j]);
1298
+ if (a === null || b === null) {
1000
1299
  out.set(i, j, null);
1001
1300
  continue;
1002
1301
  }
@@ -1123,8 +1422,8 @@ export class PineMatrix {
1123
1422
  for (let i = 0; i < this.nRows; i++) {
1124
1423
  const row: number[] = [];
1125
1424
  for (let j = 0; j < this.nCols; j++) {
1126
- const v = this.cells[i]![j]!;
1127
- if (v === null || !Number.isFinite(v)) return null;
1425
+ const v = asFiniteNumber(this.cells[i]![j]);
1426
+ if (v === null) return null;
1128
1427
  row.push(v);
1129
1428
  }
1130
1429
  out.push(row);
@@ -1136,8 +1435,8 @@ export class PineMatrix {
1136
1435
  const out: number[] = [];
1137
1436
  for (let i = 0; i < this.nRows; i++) {
1138
1437
  for (let j = 0; j < this.nCols; j++) {
1139
- const v = this.cells[i]![j]!;
1140
- if (v === null || !Number.isFinite(v)) return null;
1438
+ const v = asFiniteNumber(this.cells[i]![j]);
1439
+ if (v === null) return null;
1141
1440
  out.push(v);
1142
1441
  }
1143
1442
  }
@@ -1147,7 +1446,37 @@ export class PineMatrix {
1147
1446
  private flatten(): Cell[] {
1148
1447
  const out: Cell[] = [];
1149
1448
  for (let i = 0; i < this.nRows; i++) {
1150
- for (let j = 0; j < this.nCols; j++) out.push(this.cells[i]![j]!);
1449
+ for (let j = 0; j < this.nCols; j++) out.push(this.cells[i]![j] as Cell);
1450
+ }
1451
+ return out;
1452
+ }
1453
+
1454
+ /** In-range line, or `null` when the index is OOB / non-finite. */
1455
+ private lineAt(axis: "row" | "col", index: number): readonly unknown[] | null {
1456
+ if (axis === "row") {
1457
+ const i = resolveIndex(index, this.nRows);
1458
+ return i === null ? null : this.cells[i]!;
1459
+ }
1460
+ const j = resolveIndex(index, this.nCols);
1461
+ if (j === null) return null;
1462
+ const out: unknown[] = [];
1463
+ for (let i = 0; i < this.nRows; i++) out.push(this.cells[i]![j]);
1464
+ return out;
1465
+ }
1466
+
1467
+ private flatCells(): unknown[] {
1468
+ const out: unknown[] = [];
1469
+ for (let i = 0; i < this.nRows; i++) {
1470
+ for (let j = 0; j < this.nCols; j++) out.push(this.cells[i]![j]);
1471
+ }
1472
+ return out;
1473
+ }
1474
+
1475
+ private numericFlat(): number[] {
1476
+ const out: number[] = [];
1477
+ for (const v of this.flatCells()) {
1478
+ const n = numericCell(v);
1479
+ if (n !== null) out.push(n);
1151
1480
  }
1152
1481
  return out;
1153
1482
  }