oakscriptpy 0.1.0__py3-none-any.whl

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.
oakscriptpy/matrix.py ADDED
@@ -0,0 +1,1136 @@
1
+ """Matrix namespace — mirrors PineScript matrix.* functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins as _builtins
6
+ import math
7
+ from typing import Any
8
+
9
+ from ._types import PineMatrix
10
+
11
+ builtins_max = _builtins.max
12
+ builtins_min = _builtins.min
13
+
14
+ EPSILON = 1e-10
15
+
16
+
17
+ def new_matrix(rows: int = 0, columns: int = 0, initial_value: Any = None) -> PineMatrix:
18
+ data: list[list[Any]] = []
19
+ for i in range(rows):
20
+ row: list[Any] = []
21
+ for j in range(columns):
22
+ row.append(initial_value)
23
+ data.append(row)
24
+ return PineMatrix(rows=rows, columns=columns, data=data)
25
+
26
+
27
+ def get(id: PineMatrix, row: int, column: int) -> Any:
28
+ if row < 0 or row >= id.rows or column < 0 or column >= id.columns:
29
+ raise ValueError(f"Matrix index out of bounds: [{row}, {column}] for matrix of size [{id.rows}, {id.columns}]")
30
+ return id.data[row][column]
31
+
32
+
33
+ def set(id: PineMatrix, row: int, column: int, value: Any) -> None:
34
+ if row < 0 or row >= id.rows or column < 0 or column >= id.columns:
35
+ raise ValueError(f"Matrix index out of bounds: [{row}, {column}] for matrix of size [{id.rows}, {id.columns}]")
36
+ id.data[row][column] = value
37
+
38
+
39
+ def rows(id: PineMatrix) -> int:
40
+ return id.rows
41
+
42
+
43
+ def columns(id: PineMatrix) -> int:
44
+ return id.columns
45
+
46
+
47
+ def elements_count(id: PineMatrix) -> int:
48
+ return id.rows * id.columns
49
+
50
+
51
+ def row(id: PineMatrix, row_index: int) -> list[Any]:
52
+ if row_index < 0 or row_index >= id.rows:
53
+ raise ValueError(f"Row index out of bounds: {row_index} for matrix with {id.rows} rows")
54
+ return list(id.data[row_index])
55
+
56
+
57
+ def col(id: PineMatrix, column_index: int) -> list[Any]:
58
+ if column_index < 0 or column_index >= id.columns:
59
+ raise ValueError(f"Column index out of bounds: {column_index} for matrix with {id.columns} columns")
60
+ return [r[column_index] for r in id.data]
61
+
62
+
63
+ def copy_matrix(id: PineMatrix) -> PineMatrix:
64
+ new_data = [list(r) for r in id.data]
65
+ return PineMatrix(rows=id.rows, columns=id.columns, data=new_data)
66
+
67
+
68
+ def fill(
69
+ id: PineMatrix,
70
+ value: Any,
71
+ from_row: int = 0,
72
+ to_row: int | None = None,
73
+ from_column: int = 0,
74
+ to_column: int | None = None,
75
+ ) -> None:
76
+ end_row = to_row if to_row is not None else id.rows
77
+ end_col = to_column if to_column is not None else id.columns
78
+ for i in range(from_row, end_row):
79
+ for j in range(from_column, end_col):
80
+ id.data[i][j] = value
81
+
82
+
83
+ def is_square(id: PineMatrix) -> bool:
84
+ return id.rows == id.columns
85
+
86
+
87
+ def is_zero(id: PineMatrix) -> bool:
88
+ for i in range(id.rows):
89
+ for j in range(id.columns):
90
+ if id.data[i][j] != 0:
91
+ return False
92
+ return True
93
+
94
+
95
+ def is_binary(id: PineMatrix) -> bool:
96
+ for i in range(id.rows):
97
+ for j in range(id.columns):
98
+ val = id.data[i][j]
99
+ if val != 0 and val != 1:
100
+ return False
101
+ return True
102
+
103
+
104
+ # ==========================================
105
+ # Row/Column Operations
106
+ # ==========================================
107
+
108
+
109
+ def add_row(id: PineMatrix, row_idx: int | None = None, array_id: list[Any] | None = None) -> None:
110
+ insert_index = row_idx if row_idx is not None else id.rows
111
+
112
+ if insert_index < 0 or insert_index > id.rows:
113
+ raise ValueError(f"Row index out of bounds: {insert_index} for matrix with {id.rows} rows")
114
+
115
+ if array_id is not None:
116
+ if id.columns > 0 and len(array_id) != id.columns:
117
+ raise ValueError(f"Array size {len(array_id)} does not match matrix columns {id.columns}")
118
+ new_row = list(array_id)
119
+ if id.rows == 0 and id.columns == 0:
120
+ id.columns = len(array_id)
121
+ else:
122
+ new_row = [None] * id.columns
123
+
124
+ id.data.insert(insert_index, new_row)
125
+ id.rows += 1
126
+
127
+
128
+ def add_col(id: PineMatrix, column: int | None = None, array_id: list[Any] | None = None) -> None:
129
+ insert_index = column if column is not None else id.columns
130
+
131
+ if insert_index < 0 or insert_index > id.columns:
132
+ raise ValueError(f"Column index out of bounds: {insert_index} for matrix with {id.columns} columns")
133
+
134
+ if array_id is not None:
135
+ if id.rows > 0 and len(array_id) != id.rows:
136
+ raise ValueError(f"Array size {len(array_id)} does not match matrix rows {id.rows}")
137
+ if id.rows == 0 and id.columns == 0:
138
+ id.rows = len(array_id)
139
+ for i in range(len(array_id)):
140
+ id.data.append([])
141
+ for i in range(id.rows):
142
+ id.data[i].insert(insert_index, array_id[i])
143
+ else:
144
+ for i in range(id.rows):
145
+ id.data[i].insert(insert_index, None)
146
+
147
+ id.columns += 1
148
+
149
+
150
+ def remove_row(id: PineMatrix, row_idx: int | None = None) -> list[Any]:
151
+ remove_index = row_idx if row_idx is not None else (id.rows - 1)
152
+
153
+ if remove_index < 0 or remove_index >= id.rows:
154
+ raise ValueError(f"Row index out of bounds: {remove_index} for matrix with {id.rows} rows")
155
+
156
+ removed_row = id.data.pop(remove_index)
157
+ id.rows -= 1
158
+ return removed_row
159
+
160
+
161
+ def remove_col(id: PineMatrix, column: int | None = None) -> list[Any]:
162
+ remove_index = column if column is not None else (id.columns - 1)
163
+
164
+ if remove_index < 0 or remove_index >= id.columns:
165
+ raise ValueError(f"Column index out of bounds: {remove_index} for matrix with {id.columns} columns")
166
+
167
+ removed_col: list[Any] = []
168
+ for i in range(id.rows):
169
+ removed_col.append(id.data[i].pop(remove_index))
170
+ id.columns -= 1
171
+ return removed_col
172
+
173
+
174
+ def swap_rows(id: PineMatrix, row1: int, row2: int) -> None:
175
+ if row1 < 0 or row1 >= id.rows:
176
+ raise ValueError(f"Row1 index out of bounds: {row1} for matrix with {id.rows} rows")
177
+ if row2 < 0 or row2 >= id.rows:
178
+ raise ValueError(f"Row2 index out of bounds: {row2} for matrix with {id.rows} rows")
179
+ id.data[row1], id.data[row2] = id.data[row2], id.data[row1]
180
+
181
+
182
+ def swap_columns(id: PineMatrix, column1: int, column2: int) -> None:
183
+ if column1 < 0 or column1 >= id.columns:
184
+ raise ValueError(f"Column1 index out of bounds: {column1} for matrix with {id.columns} columns")
185
+ if column2 < 0 or column2 >= id.columns:
186
+ raise ValueError(f"Column2 index out of bounds: {column2} for matrix with {id.columns} columns")
187
+ for i in range(id.rows):
188
+ id.data[i][column1], id.data[i][column2] = id.data[i][column2], id.data[i][column1]
189
+
190
+
191
+ # ==========================================
192
+ # Matrix Transformations
193
+ # ==========================================
194
+
195
+
196
+ def transpose(id: PineMatrix) -> PineMatrix:
197
+ new_data: list[list[Any]] = []
198
+ for j in range(id.columns):
199
+ new_row: list[Any] = []
200
+ for i in range(id.rows):
201
+ new_row.append(id.data[i][j])
202
+ new_data.append(new_row)
203
+ return PineMatrix(rows=id.columns, columns=id.rows, data=new_data)
204
+
205
+
206
+ def concat(id1: PineMatrix, id2: PineMatrix) -> PineMatrix:
207
+ if id1.columns != id2.columns:
208
+ raise ValueError(f"Column count mismatch: {id1.columns} vs {id2.columns}")
209
+ for i in range(id2.rows):
210
+ id1.data.append(list(id2.data[i]))
211
+ id1.rows += id2.rows
212
+ return id1
213
+
214
+
215
+ def submatrix(
216
+ id: PineMatrix,
217
+ from_row: int = 0,
218
+ to_row: int | None = None,
219
+ from_column: int = 0,
220
+ to_column: int | None = None,
221
+ ) -> PineMatrix:
222
+ end_row = to_row if to_row is not None else id.rows
223
+ end_col = to_column if to_column is not None else id.columns
224
+
225
+ if from_row < 0 or from_row > id.rows:
226
+ raise ValueError(f"from_row index out of bounds: {from_row}")
227
+ if end_row < 0 or end_row > id.rows:
228
+ raise ValueError(f"to_row index out of bounds: {end_row}")
229
+ if from_column < 0 or from_column > id.columns:
230
+ raise ValueError(f"from_column index out of bounds: {from_column}")
231
+ if end_col < 0 or end_col > id.columns:
232
+ raise ValueError(f"to_column index out of bounds: {end_col}")
233
+
234
+ new_rows = end_row - from_row
235
+ new_cols = end_col - from_column
236
+ new_data: list[list[Any]] = []
237
+
238
+ for i in range(from_row, end_row):
239
+ new_row: list[Any] = []
240
+ for j in range(from_column, end_col):
241
+ new_row.append(id.data[i][j])
242
+ new_data.append(new_row)
243
+
244
+ return PineMatrix(rows=new_rows, columns=new_cols, data=new_data)
245
+
246
+
247
+ def reshape(id: PineMatrix, rows_count: int, columns_count: int) -> None:
248
+ total_elements = id.rows * id.columns
249
+ new_total_elements = rows_count * columns_count
250
+
251
+ if total_elements != new_total_elements:
252
+ raise ValueError(
253
+ f"Cannot reshape {id.rows}x{id.columns} ({total_elements} elements) "
254
+ f"to {rows_count}x{columns_count} ({new_total_elements} elements)"
255
+ )
256
+
257
+ flat: list[Any] = []
258
+ for i in range(id.rows):
259
+ for j in range(id.columns):
260
+ flat.append(id.data[i][j])
261
+
262
+ new_data: list[list[Any]] = []
263
+ index = 0
264
+ for i in range(rows_count):
265
+ new_row: list[Any] = []
266
+ for j in range(columns_count):
267
+ new_row.append(flat[index])
268
+ index += 1
269
+ new_data.append(new_row)
270
+
271
+ id.rows = rows_count
272
+ id.columns = columns_count
273
+ id.data = new_data
274
+
275
+
276
+ def reverse(id: PineMatrix) -> None:
277
+ id.data.reverse()
278
+ for i in range(id.rows):
279
+ id.data[i].reverse()
280
+
281
+
282
+ def sort(id: PineMatrix, column: int = 0, order: str = "ascending") -> None:
283
+ if column < 0 or column >= id.columns:
284
+ raise ValueError(f"Column index out of bounds: {column} for matrix with {id.columns} columns")
285
+
286
+ def sort_key(a_row: list[Any]) -> Any:
287
+ val = a_row[column]
288
+ if isinstance(val, (int, float)):
289
+ return val
290
+ return val
291
+
292
+ reverse_flag = order == "descending"
293
+ id.data.sort(key=sort_key, reverse=reverse_flag)
294
+
295
+
296
+ # ==========================================
297
+ # Element-wise Arithmetic
298
+ # ==========================================
299
+
300
+
301
+ def sum(id1: PineMatrix, id2: PineMatrix | float | int) -> PineMatrix:
302
+ new_data: list[list[float]] = []
303
+
304
+ if isinstance(id2, (int, float)):
305
+ for i in range(id1.rows):
306
+ new_row: list[float] = []
307
+ for j in range(id1.columns):
308
+ new_row.append(id1.data[i][j] + id2)
309
+ new_data.append(new_row)
310
+ return PineMatrix(rows=id1.rows, columns=id1.columns, data=new_data)
311
+ else:
312
+ if id1.rows != id2.rows or id1.columns != id2.columns:
313
+ raise ValueError(
314
+ f"Matrix dimensions must match: {id1.rows}x{id1.columns} vs {id2.rows}x{id2.columns}"
315
+ )
316
+ for i in range(id1.rows):
317
+ new_row = []
318
+ for j in range(id1.columns):
319
+ new_row.append(id1.data[i][j] + id2.data[i][j])
320
+ new_data.append(new_row)
321
+ return PineMatrix(rows=id1.rows, columns=id1.columns, data=new_data)
322
+
323
+
324
+ def diff(id1: PineMatrix, id2: PineMatrix | float | int) -> PineMatrix:
325
+ new_data: list[list[float]] = []
326
+
327
+ if isinstance(id2, (int, float)):
328
+ for i in range(id1.rows):
329
+ new_row: list[float] = []
330
+ for j in range(id1.columns):
331
+ new_row.append(id1.data[i][j] - id2)
332
+ new_data.append(new_row)
333
+ return PineMatrix(rows=id1.rows, columns=id1.columns, data=new_data)
334
+ else:
335
+ if id1.rows != id2.rows or id1.columns != id2.columns:
336
+ raise ValueError(
337
+ f"Matrix dimensions must match: {id1.rows}x{id1.columns} vs {id2.rows}x{id2.columns}"
338
+ )
339
+ for i in range(id1.rows):
340
+ new_row = []
341
+ for j in range(id1.columns):
342
+ new_row.append(id1.data[i][j] - id2.data[i][j])
343
+ new_data.append(new_row)
344
+ return PineMatrix(rows=id1.rows, columns=id1.columns, data=new_data)
345
+
346
+
347
+ # ==========================================
348
+ # Statistical Functions
349
+ # ==========================================
350
+
351
+
352
+ def _is_valid_number(val: Any) -> bool:
353
+ if val is None:
354
+ return False
355
+ if isinstance(val, float) and math.isnan(val):
356
+ return False
357
+ return isinstance(val, (int, float))
358
+
359
+
360
+ def avg(id: PineMatrix) -> float:
361
+ if id.rows == 0 or id.columns == 0:
362
+ return float("nan")
363
+
364
+ total = 0.0
365
+ count = 0
366
+ for i in range(id.rows):
367
+ for j in range(id.columns):
368
+ val = id.data[i][j]
369
+ if _is_valid_number(val):
370
+ total += val
371
+ count += 1
372
+
373
+ return float("nan") if count == 0 else total / count
374
+
375
+
376
+ def min(id: PineMatrix) -> float:
377
+ if id.rows == 0 or id.columns == 0:
378
+ return float("nan")
379
+
380
+ min_val = float("inf")
381
+ for i in range(id.rows):
382
+ for j in range(id.columns):
383
+ val = id.data[i][j]
384
+ if _is_valid_number(val) and val < min_val:
385
+ min_val = val
386
+
387
+ return float("nan") if min_val == float("inf") else min_val
388
+
389
+
390
+ def max(id: PineMatrix) -> float:
391
+ if id.rows == 0 or id.columns == 0:
392
+ return float("nan")
393
+
394
+ max_val = float("-inf")
395
+ for i in range(id.rows):
396
+ for j in range(id.columns):
397
+ val = id.data[i][j]
398
+ if _is_valid_number(val) and val > max_val:
399
+ max_val = val
400
+
401
+ return float("nan") if max_val == float("-inf") else max_val
402
+
403
+
404
+ def median(id: PineMatrix) -> float:
405
+ if id.rows == 0 or id.columns == 0:
406
+ return float("nan")
407
+
408
+ values: list[float] = []
409
+ for i in range(id.rows):
410
+ for j in range(id.columns):
411
+ val = id.data[i][j]
412
+ if _is_valid_number(val):
413
+ values.append(val)
414
+
415
+ if len(values) == 0:
416
+ return float("nan")
417
+
418
+ values.sort()
419
+ mid = len(values) // 2
420
+
421
+ if len(values) % 2 == 0:
422
+ return (values[mid - 1] + values[mid]) / 2
423
+ else:
424
+ return values[mid]
425
+
426
+
427
+ def mode(id: PineMatrix) -> float:
428
+ if id.rows == 0 or id.columns == 0:
429
+ return float("nan")
430
+
431
+ frequency: dict[float, int] = {}
432
+ max_freq = 0
433
+ mode_value = float("nan")
434
+
435
+ for i in range(id.rows):
436
+ for j in range(id.columns):
437
+ val = id.data[i][j]
438
+ if _is_valid_number(val):
439
+ freq = frequency.get(val, 0) + 1
440
+ frequency[val] = freq
441
+ if freq > max_freq or (freq == max_freq and (math.isnan(mode_value) or val < mode_value)):
442
+ max_freq = freq
443
+ mode_value = val
444
+
445
+ return mode_value
446
+
447
+
448
+ def trace(id: PineMatrix) -> float:
449
+ if not is_square(id):
450
+ raise ValueError(f"Matrix must be square for trace calculation: {id.rows}x{id.columns}")
451
+
452
+ if id.rows == 0:
453
+ return 0.0
454
+
455
+ total = 0.0
456
+ for i in range(id.rows):
457
+ val = id.data[i][i]
458
+ if _is_valid_number(val):
459
+ total += val
460
+
461
+ return total
462
+
463
+
464
+ # ==========================================
465
+ # Boolean Checks
466
+ # ==========================================
467
+
468
+
469
+ def is_diagonal(id: PineMatrix) -> bool:
470
+ if not is_square(id):
471
+ return False
472
+
473
+ for i in range(id.rows):
474
+ for j in range(id.columns):
475
+ if i != j and id.data[i][j] != 0:
476
+ return False
477
+
478
+ return True
479
+
480
+
481
+ def is_identity(id: PineMatrix) -> bool:
482
+ if not is_square(id):
483
+ return False
484
+
485
+ for i in range(id.rows):
486
+ for j in range(id.columns):
487
+ val = id.data[i][j]
488
+ if i == j:
489
+ if val != 1:
490
+ return False
491
+ else:
492
+ if val != 0:
493
+ return False
494
+
495
+ return True
496
+
497
+
498
+ def is_symmetric(id: PineMatrix) -> bool:
499
+ if not is_square(id):
500
+ return False
501
+
502
+ for i in range(id.rows):
503
+ for j in range(i + 1, id.columns):
504
+ if id.data[i][j] != id.data[j][i]:
505
+ return False
506
+
507
+ return True
508
+
509
+
510
+ def is_antisymmetric(id: PineMatrix) -> bool:
511
+ if not is_square(id):
512
+ return False
513
+
514
+ for i in range(id.rows):
515
+ if id.data[i][i] != 0:
516
+ return False
517
+ for j in range(i + 1, id.columns):
518
+ upper_val = id.data[i][j]
519
+ lower_val = id.data[j][i]
520
+ if upper_val != -lower_val:
521
+ return False
522
+
523
+ return True
524
+
525
+
526
+ def is_triangular(id: PineMatrix) -> bool:
527
+ if not is_square(id):
528
+ return False
529
+
530
+ is_upper = True
531
+ for i in range(1, id.rows):
532
+ if not is_upper:
533
+ break
534
+ for j in range(0, i):
535
+ if id.data[i][j] != 0:
536
+ is_upper = False
537
+ break
538
+
539
+ if is_upper:
540
+ return True
541
+
542
+ is_lower = True
543
+ for i in range(0, id.rows - 1):
544
+ if not is_lower:
545
+ break
546
+ for j in range(i + 1, id.columns):
547
+ if id.data[i][j] != 0:
548
+ is_lower = False
549
+ break
550
+
551
+ return is_lower
552
+
553
+
554
+ def is_antidiagonal(id: PineMatrix) -> bool:
555
+ if not is_square(id):
556
+ return False
557
+
558
+ n = id.rows
559
+ for i in range(n):
560
+ for j in range(n):
561
+ is_anti_diag = (i + j == n - 1)
562
+ if not is_anti_diag and id.data[i][j] != 0:
563
+ return False
564
+
565
+ return True
566
+
567
+
568
+ def is_stochastic(id: PineMatrix) -> bool:
569
+ if id.rows == 0 or id.columns == 0:
570
+ return False
571
+
572
+ tolerance = 1e-10
573
+
574
+ for i in range(id.rows):
575
+ row_sum = 0.0
576
+ for j in range(id.columns):
577
+ val = id.data[i][j]
578
+ if val < 0:
579
+ return False
580
+ row_sum += val
581
+ if abs(row_sum - 1) > tolerance:
582
+ return False
583
+
584
+ return True
585
+
586
+
587
+ # ==========================================
588
+ # Linear Algebra Functions
589
+ # ==========================================
590
+
591
+
592
+ def _create_identity(n: int) -> PineMatrix:
593
+ m = new_matrix(n, n, 0.0)
594
+ for i in range(n):
595
+ m.data[i][i] = 1.0
596
+ return m
597
+
598
+
599
+ def mult(id1: PineMatrix, id2: PineMatrix | float | int | list[float]) -> PineMatrix | list[float]:
600
+ if isinstance(id2, (int, float)):
601
+ new_data: list[list[float]] = []
602
+ for i in range(id1.rows):
603
+ new_row: list[float] = []
604
+ for j in range(id1.columns):
605
+ new_row.append(id1.data[i][j] * id2)
606
+ new_data.append(new_row)
607
+ return PineMatrix(rows=id1.rows, columns=id1.columns, data=new_data)
608
+
609
+ if isinstance(id2, list):
610
+ vec = id2
611
+ if len(vec) != id1.columns:
612
+ raise ValueError(f"Vector length {len(vec)} must equal matrix columns {id1.columns}")
613
+ result: list[float] = []
614
+ for i in range(id1.rows):
615
+ s = 0.0
616
+ for j in range(id1.columns):
617
+ s += id1.data[i][j] * vec[j]
618
+ result.append(s)
619
+ return result
620
+
621
+ m2 = id2
622
+ if id1.columns != m2.rows:
623
+ raise ValueError(
624
+ f"Matrix multiplication dimension mismatch: {id1.rows}x{id1.columns} x {m2.rows}x{m2.columns}. "
625
+ f"First matrix columns ({id1.columns}) must equal second matrix rows ({m2.rows})"
626
+ )
627
+
628
+ new_rows = id1.rows
629
+ new_cols = m2.columns
630
+ new_data_m: list[list[float]] = []
631
+
632
+ for i in range(new_rows):
633
+ new_row_m: list[float] = []
634
+ for j in range(new_cols):
635
+ s = 0.0
636
+ for k in range(id1.columns):
637
+ s += id1.data[i][k] * m2.data[k][j]
638
+ new_row_m.append(s)
639
+ new_data_m.append(new_row_m)
640
+
641
+ return PineMatrix(rows=new_rows, columns=new_cols, data=new_data_m)
642
+
643
+
644
+ def pow(id: PineMatrix, power: int) -> PineMatrix:
645
+ if not is_square(id):
646
+ raise ValueError(f"Matrix must be square for power calculation: {id.rows}x{id.columns}")
647
+
648
+ n = id.rows
649
+
650
+ if power == 0:
651
+ return _create_identity(n)
652
+
653
+ if power < 0:
654
+ inverse = inv(id)
655
+ if inverse is None:
656
+ raise ValueError("Cannot compute negative power of singular matrix")
657
+ base = inverse
658
+ p = -power
659
+ else:
660
+ base = copy_matrix(id)
661
+ p = power
662
+
663
+ result = _create_identity(n)
664
+
665
+ while p > 0:
666
+ if p % 2 == 1:
667
+ result = mult(result, base) # type: ignore[assignment]
668
+ base = mult(base, base) # type: ignore[assignment]
669
+ p = p // 2
670
+
671
+ return result # type: ignore[return-value]
672
+
673
+
674
+ def det(id: PineMatrix) -> float:
675
+ if not is_square(id):
676
+ raise ValueError(f"Matrix must be square for determinant calculation: {id.rows}x{id.columns}")
677
+
678
+ n = id.rows
679
+
680
+ if n == 0:
681
+ return 1.0
682
+
683
+ if n == 1:
684
+ return float(id.data[0][0])
685
+
686
+ if n == 2:
687
+ return float(id.data[0][0] * id.data[1][1] - id.data[0][1] * id.data[1][0])
688
+
689
+ a: list[list[float]] = [list(r) for r in id.data]
690
+ determinant = 1.0
691
+ swap_count = 0
692
+
693
+ for col_idx in range(n):
694
+ max_row = col_idx
695
+ max_val = abs(a[col_idx][col_idx])
696
+ for row_idx in range(col_idx + 1, n):
697
+ abs_val = abs(a[row_idx][col_idx])
698
+ if abs_val > max_val:
699
+ max_val = abs_val
700
+ max_row = row_idx
701
+
702
+ if max_row != col_idx:
703
+ a[col_idx], a[max_row] = a[max_row], a[col_idx]
704
+ swap_count += 1
705
+
706
+ pivot = a[col_idx][col_idx]
707
+ if abs(pivot) < EPSILON:
708
+ return 0.0
709
+
710
+ determinant *= pivot
711
+
712
+ for row_idx in range(col_idx + 1, n):
713
+ factor = a[row_idx][col_idx] / pivot
714
+ for j in range(col_idx, n):
715
+ a[row_idx][j] = a[row_idx][j] - factor * a[col_idx][j]
716
+
717
+ if swap_count % 2 == 1:
718
+ determinant = -determinant
719
+
720
+ return determinant
721
+
722
+
723
+ def inv(id: PineMatrix) -> PineMatrix | None:
724
+ if not is_square(id):
725
+ raise ValueError(f"Matrix must be square for inverse calculation: {id.rows}x{id.columns}")
726
+
727
+ n = id.rows
728
+
729
+ if n == 0:
730
+ return new_matrix(0, 0, 0.0)
731
+
732
+ aug: list[list[float]] = []
733
+ for i in range(n):
734
+ row_data: list[float] = []
735
+ for j in range(n):
736
+ row_data.append(float(id.data[i][j]))
737
+ for j in range(n):
738
+ row_data.append(1.0 if i == j else 0.0)
739
+ aug.append(row_data)
740
+
741
+ for col_idx in range(n):
742
+ max_row = col_idx
743
+ max_val = abs(aug[col_idx][col_idx])
744
+ for row_idx in range(col_idx + 1, n):
745
+ abs_val = abs(aug[row_idx][col_idx])
746
+ if abs_val > max_val:
747
+ max_val = abs_val
748
+ max_row = row_idx
749
+
750
+ if max_row != col_idx:
751
+ aug[col_idx], aug[max_row] = aug[max_row], aug[col_idx]
752
+
753
+ pivot = aug[col_idx][col_idx]
754
+ if abs(pivot) < EPSILON:
755
+ return None
756
+
757
+ for j in range(2 * n):
758
+ aug[col_idx][j] = aug[col_idx][j] / pivot
759
+
760
+ for row_idx in range(n):
761
+ if row_idx != col_idx:
762
+ factor = aug[row_idx][col_idx]
763
+ for j in range(2 * n):
764
+ aug[row_idx][j] = aug[row_idx][j] - factor * aug[col_idx][j]
765
+
766
+ inverse_data: list[list[float]] = []
767
+ for i in range(n):
768
+ row_data = []
769
+ for j in range(n):
770
+ row_data.append(aug[i][n + j])
771
+ inverse_data.append(row_data)
772
+
773
+ return PineMatrix(rows=n, columns=n, data=inverse_data)
774
+
775
+
776
+ def pinv(id: PineMatrix) -> PineMatrix:
777
+ m = id.rows
778
+ n = id.columns
779
+
780
+ if m == 0 or n == 0:
781
+ return new_matrix(n, m, 0.0)
782
+
783
+ if m == n:
784
+ inverse = inv(id)
785
+ if inverse is not None:
786
+ return inverse
787
+
788
+ at = transpose(id)
789
+
790
+ if m >= n:
791
+ ata = mult(at, id)
792
+ ata_inv = inv(ata) # type: ignore[arg-type]
793
+ if ata_inv is not None:
794
+ return mult(ata_inv, at) # type: ignore[return-value]
795
+
796
+ aat = mult(id, at)
797
+ aat_inv = inv(aat) # type: ignore[arg-type]
798
+ if aat_inv is not None:
799
+ return mult(at, aat_inv) # type: ignore[return-value]
800
+
801
+ lam = 1e-10
802
+ ata = mult(at, id)
803
+ for i in range(ata.rows): # type: ignore[union-attr]
804
+ ata.data[i][i] = ata.data[i][i] + lam # type: ignore[union-attr]
805
+
806
+ ata_inv = inv(ata) # type: ignore[arg-type]
807
+ if ata_inv is not None:
808
+ return mult(ata_inv, at) # type: ignore[return-value]
809
+
810
+ return new_matrix(n, m, 0.0)
811
+
812
+
813
+ def rank(id: PineMatrix) -> int:
814
+ if id.rows == 0 or id.columns == 0:
815
+ return 0
816
+
817
+ a: list[list[float]] = [list(r) for r in id.data]
818
+ m = id.rows
819
+ n = id.columns
820
+
821
+ r = 0
822
+
823
+ for col_idx in range(n):
824
+ if r >= m:
825
+ break
826
+
827
+ max_row = r
828
+ max_val = abs(a[r][col_idx])
829
+ for row_idx in range(r + 1, m):
830
+ abs_val = abs(a[row_idx][col_idx])
831
+ if abs_val > max_val:
832
+ max_val = abs_val
833
+ max_row = row_idx
834
+
835
+ if max_val < EPSILON:
836
+ continue
837
+
838
+ if max_row != r:
839
+ a[r], a[max_row] = a[max_row], a[r]
840
+
841
+ pivot = a[r][col_idx]
842
+ for row_idx in range(r + 1, m):
843
+ factor = a[row_idx][col_idx] / pivot
844
+ for j in range(col_idx, n):
845
+ a[row_idx][j] = a[row_idx][j] - factor * a[r][j]
846
+
847
+ r += 1
848
+
849
+ return r
850
+
851
+
852
+ def eigenvalues(id: PineMatrix) -> list[float]:
853
+ if not is_square(id):
854
+ raise ValueError(f"Matrix must be square for eigenvalue calculation: {id.rows}x{id.columns}")
855
+
856
+ n = id.rows
857
+
858
+ if n == 0:
859
+ return []
860
+
861
+ if n == 1:
862
+ return [float(id.data[0][0])]
863
+
864
+ if n == 2:
865
+ a = id.data[0][0]
866
+ b = id.data[0][1]
867
+ c = id.data[1][0]
868
+ d = id.data[1][1]
869
+
870
+ tr = a + d
871
+ determinant = a * d - b * c
872
+ discriminant = tr * tr - 4 * determinant
873
+
874
+ if discriminant >= 0:
875
+ sqrt_disc = math.sqrt(discriminant)
876
+ return [(tr + sqrt_disc) / 2, (tr - sqrt_disc) / 2]
877
+ else:
878
+ return [tr / 2, tr / 2]
879
+
880
+ h = _to_hessenberg(id)
881
+ result = _qr_algorithm(h, 100)
882
+ return result
883
+
884
+
885
+ def _to_hessenberg(m: PineMatrix) -> PineMatrix:
886
+ n = m.rows
887
+ h: list[list[float]] = [list(r) for r in m.data]
888
+
889
+ for k in range(n - 2):
890
+ max_val = 0.0
891
+ for i in range(k + 1, n):
892
+ max_val = builtins_max(max_val, abs(h[i][k]))
893
+
894
+ if max_val < EPSILON:
895
+ continue
896
+
897
+ sigma = 0.0
898
+ for i in range(k + 1, n):
899
+ sigma += h[i][k] * h[i][k]
900
+ sigma = math.sqrt(sigma)
901
+
902
+ if h[k + 1][k] < 0:
903
+ sigma = -sigma
904
+
905
+ u: list[float] = [0.0] * n
906
+ u[k + 1] = h[k + 1][k] + sigma
907
+ for i in range(k + 2, n):
908
+ u[i] = h[i][k]
909
+
910
+ u_tu = 0.0
911
+ for i in range(k + 1, n):
912
+ u_tu += u[i] * u[i]
913
+
914
+ if u_tu < EPSILON:
915
+ continue
916
+
917
+ for j in range(k, n):
918
+ dot = 0.0
919
+ for i in range(k + 1, n):
920
+ dot += u[i] * h[i][j]
921
+ factor = 2 * dot / u_tu
922
+ for i in range(k + 1, n):
923
+ h[i][j] = h[i][j] - factor * u[i]
924
+
925
+ for i in range(n):
926
+ dot = 0.0
927
+ for j in range(k + 1, n):
928
+ dot += h[i][j] * u[j]
929
+ factor = 2 * dot / u_tu
930
+ for j in range(k + 1, n):
931
+ h[i][j] = h[i][j] - factor * u[j]
932
+
933
+ return PineMatrix(rows=n, columns=n, data=h)
934
+
935
+
936
+ def _qr_algorithm(h: PineMatrix, max_iter: int) -> list[float]:
937
+ n = h.rows
938
+ a: list[list[float]] = [list(r) for r in h.data]
939
+ eigenvals: list[float] = []
940
+
941
+ remaining = n
942
+
943
+ for _iter in range(max_iter):
944
+ if remaining <= 1:
945
+ break
946
+
947
+ converged = False
948
+ for i in range(remaining - 1, 0, -1):
949
+ if abs(a[i][i - 1]) < EPSILON * (abs(a[i - 1][i - 1]) + abs(a[i][i])):
950
+ a[i][i - 1] = 0.0
951
+ if i == remaining - 1:
952
+ eigenvals.append(a[remaining - 1][remaining - 1])
953
+ remaining -= 1
954
+ converged = True
955
+ break
956
+
957
+ if converged:
958
+ continue
959
+ if remaining <= 1:
960
+ break
961
+
962
+ d = (a[remaining - 2][remaining - 2] - a[remaining - 1][remaining - 1]) / 2
963
+ sign = 1 if d >= 0 else -1
964
+ sub_diag_sq = a[remaining - 1][remaining - 2] * a[remaining - 1][remaining - 2]
965
+ mu = a[remaining - 1][remaining - 1] - sign * sub_diag_sq / (abs(d) + math.sqrt(d * d + sub_diag_sq))
966
+
967
+ x = a[0][0] - mu
968
+ z = a[1][0]
969
+
970
+ for k in range(remaining - 1):
971
+ r = math.sqrt(x * x + z * z)
972
+ if r < EPSILON:
973
+ r = EPSILON
974
+ c = x / r
975
+ s = z / r
976
+
977
+ for j in range(builtins_max(0, k - 1), remaining):
978
+ temp = c * a[k][j] + s * a[k + 1][j]
979
+ a[k + 1][j] = -s * a[k][j] + c * a[k + 1][j]
980
+ a[k][j] = temp
981
+
982
+ for i in range(builtins_min(k + 3, remaining)):
983
+ temp = c * a[i][k] + s * a[i][k + 1]
984
+ a[i][k + 1] = -s * a[i][k] + c * a[i][k + 1]
985
+ a[i][k] = temp
986
+
987
+ if k < remaining - 2:
988
+ x = a[k + 1][k]
989
+ z = a[k + 2][k]
990
+
991
+ for i in range(remaining):
992
+ eigenvals.append(a[i][i])
993
+
994
+ eigenvals.sort(key=lambda v: abs(v), reverse=True)
995
+ return eigenvals
996
+
997
+
998
+ def eigenvectors(id: PineMatrix) -> PineMatrix:
999
+ if not is_square(id):
1000
+ raise ValueError(f"Matrix must be square for eigenvector calculation: {id.rows}x{id.columns}")
1001
+
1002
+ n = id.rows
1003
+
1004
+ if n == 0:
1005
+ return new_matrix(0, 0, 0.0)
1006
+
1007
+ evals = eigenvalues(id)
1008
+
1009
+ vectors: list[list[float]] = []
1010
+ for i in range(n):
1011
+ lam = evals[i]
1012
+ vec = _inverse_iteration(id, lam)
1013
+ vectors.append(vec)
1014
+
1015
+ result_data: list[list[float]] = []
1016
+ for i in range(n):
1017
+ row_data: list[float] = []
1018
+ for j in range(n):
1019
+ row_data.append(vectors[j][i])
1020
+ result_data.append(row_data)
1021
+
1022
+ return PineMatrix(rows=n, columns=n, data=result_data)
1023
+
1024
+
1025
+ def _inverse_iteration(m: PineMatrix, lam: float) -> list[float]:
1026
+ n = m.rows
1027
+
1028
+ shifted: list[list[float]] = []
1029
+ for i in range(n):
1030
+ row_data: list[float] = []
1031
+ for j in range(n):
1032
+ val = m.data[i][j]
1033
+ if i == j:
1034
+ val = val - lam
1035
+ row_data.append(float(val))
1036
+ shifted.append(row_data)
1037
+
1038
+ for i in range(n):
1039
+ if abs(shifted[i][i]) < EPSILON:
1040
+ shifted[i][i] = EPSILON
1041
+
1042
+ v: list[float] = [1.0] * n
1043
+
1044
+ for _iter in range(50):
1045
+ w = _solve_linear_system(shifted, v)
1046
+
1047
+ norm = 0.0
1048
+ for i in range(n):
1049
+ norm += w[i] * w[i]
1050
+ norm = math.sqrt(norm)
1051
+
1052
+ if norm < EPSILON:
1053
+ return v
1054
+
1055
+ new_v: list[float] = []
1056
+ for i in range(n):
1057
+ new_v.append(w[i] / norm)
1058
+
1059
+ diff_val = 0.0
1060
+ for i in range(n):
1061
+ diff_val += abs(abs(new_v[i]) - abs(v[i]))
1062
+
1063
+ v = new_v
1064
+
1065
+ if diff_val < EPSILON:
1066
+ break
1067
+
1068
+ return v
1069
+
1070
+
1071
+ def _solve_linear_system(a: list[list[float]], b: list[float]) -> list[float]:
1072
+ n = len(a)
1073
+
1074
+ aug: list[list[float]] = []
1075
+ for i in range(n):
1076
+ aug.append(list(a[i]) + [b[i]])
1077
+
1078
+ for col_idx in range(n):
1079
+ max_row = col_idx
1080
+ max_val = abs(aug[col_idx][col_idx])
1081
+ for row_idx in range(col_idx + 1, n):
1082
+ abs_val = abs(aug[row_idx][col_idx])
1083
+ if abs_val > max_val:
1084
+ max_val = abs_val
1085
+ max_row = row_idx
1086
+
1087
+ if max_row != col_idx:
1088
+ aug[col_idx], aug[max_row] = aug[max_row], aug[col_idx]
1089
+
1090
+ pivot = aug[col_idx][col_idx]
1091
+ if abs(pivot) < EPSILON:
1092
+ continue
1093
+
1094
+ for row_idx in range(col_idx + 1, n):
1095
+ factor = aug[row_idx][col_idx] / pivot
1096
+ for j in range(col_idx, n + 1):
1097
+ aug[row_idx][j] = aug[row_idx][j] - factor * aug[col_idx][j]
1098
+
1099
+ x: list[float] = [0.0] * n
1100
+ for i in range(n - 1, -1, -1):
1101
+ s = aug[i][n]
1102
+ for j in range(i + 1, n):
1103
+ s -= aug[i][j] * x[j]
1104
+ diag = aug[i][i]
1105
+ x[i] = 0.0 if abs(diag) < EPSILON else s / diag
1106
+
1107
+ return x
1108
+
1109
+
1110
+ def kron(id1: PineMatrix, id2: PineMatrix) -> PineMatrix:
1111
+ m1 = id1.rows
1112
+ n1 = id1.columns
1113
+ m2 = id2.rows
1114
+ n2 = id2.columns
1115
+
1116
+ result_rows = m1 * m2
1117
+ result_cols = n1 * n2
1118
+ data: list[list[float]] = []
1119
+
1120
+ for i in range(result_rows):
1121
+ data.append([0.0] * result_cols)
1122
+
1123
+ for i1 in range(m1):
1124
+ for j1 in range(n1):
1125
+ a = id1.data[i1][j1]
1126
+ for i2 in range(m2):
1127
+ for j2 in range(n2):
1128
+ r = i1 * m2 + i2
1129
+ c = j1 * n2 + j2
1130
+ data[r][c] = a * id2.data[i2][j2]
1131
+
1132
+ return PineMatrix(rows=result_rows, columns=result_cols, data=data)
1133
+
1134
+
1135
+ def newtype(rows_count: int = 0, columns_count: int = 0, initial_value: Any = None) -> PineMatrix:
1136
+ return new_matrix(rows_count, columns_count, initial_value)