opencode-pyneruntime 6.6.4__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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
pynecore/types/matrix.py
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar, Generic, Any, cast
|
|
4
|
+
import copy
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from .na import NA, na_float
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Matrix(Generic[T]):
|
|
12
|
+
"""
|
|
13
|
+
A matrix implementation in pure Python
|
|
14
|
+
"""
|
|
15
|
+
data: list[list[Any]]
|
|
16
|
+
|
|
17
|
+
def __init__(self, rows: int = 0, cols: int = 0, initial_value: T = NA(T)):
|
|
18
|
+
self.rows = rows
|
|
19
|
+
self.cols = cols
|
|
20
|
+
self.data = [[initial_value for _ in range(cols)] for _ in range(rows)]
|
|
21
|
+
|
|
22
|
+
def __iter__(self):
|
|
23
|
+
"""Iterate over the matrix rows (Pine `for row in myMatrix`), each row
|
|
24
|
+
being the underlying list of that row's values."""
|
|
25
|
+
return iter(self.data)
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
"""The number of rows (so len(matrix) mirrors matrix.rows())."""
|
|
29
|
+
return len(self.data)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def rows(self) -> int:
|
|
33
|
+
"""Return the number of rows in the matrix."""
|
|
34
|
+
return self._rows
|
|
35
|
+
|
|
36
|
+
@rows.setter
|
|
37
|
+
def rows(self, value: int) -> None:
|
|
38
|
+
self._rows = value
|
|
39
|
+
|
|
40
|
+
def add_row(self, row: int | None = None, array_id: list[Any] | None = None) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Add a row at the specified index of the matrix.
|
|
43
|
+
|
|
44
|
+
The row can consist of NA values, or an array can be used to provide values.
|
|
45
|
+
|
|
46
|
+
:param row: The index where the new row will be inserted. If None, appends to the end.
|
|
47
|
+
:param array_id: Array to use for providing values to the new row. If shorter than matrix
|
|
48
|
+
width, remaining cells are filled with NA. If longer, array is truncated.
|
|
49
|
+
If matrix is empty, the array size determines the column count.
|
|
50
|
+
:raises IndexError: If row index is out of bounds.
|
|
51
|
+
"""
|
|
52
|
+
row_idx: int = self.rows if row is None else row
|
|
53
|
+
|
|
54
|
+
if row_idx < 0 or row_idx > self.rows:
|
|
55
|
+
raise IndexError(f"Row index {row_idx} out of bounds for matrix with {self.rows} rows")
|
|
56
|
+
|
|
57
|
+
if array_id is not None:
|
|
58
|
+
# If matrix is empty (0 columns), use array size to set column count
|
|
59
|
+
if self.cols == 0 and len(array_id) > 0:
|
|
60
|
+
self.cols = len(array_id)
|
|
61
|
+
|
|
62
|
+
new_row = []
|
|
63
|
+
for i in range(self.cols):
|
|
64
|
+
if i < len(array_id):
|
|
65
|
+
new_row.append(array_id[i])
|
|
66
|
+
else:
|
|
67
|
+
new_row.append(NA(T))
|
|
68
|
+
else:
|
|
69
|
+
new_row = [NA(T) for _ in range(self.cols)]
|
|
70
|
+
|
|
71
|
+
self.data.insert(row_idx, new_row)
|
|
72
|
+
self.rows += 1
|
|
73
|
+
|
|
74
|
+
def avg(self) -> float | int | NA[float]:
|
|
75
|
+
"""
|
|
76
|
+
Calculate the average of all elements in the matrix.
|
|
77
|
+
|
|
78
|
+
:return: The average value of all non-NA elements in the matrix.
|
|
79
|
+
:rtype: Union[float, int]
|
|
80
|
+
"""
|
|
81
|
+
total = 0
|
|
82
|
+
count = 0
|
|
83
|
+
for row in self.data:
|
|
84
|
+
for val in row:
|
|
85
|
+
if not (isinstance(val, NA) or val != val):
|
|
86
|
+
total += val
|
|
87
|
+
count += 1
|
|
88
|
+
return total / count if count > 0 else na_float
|
|
89
|
+
|
|
90
|
+
def col(self, column: int) -> list[T]:
|
|
91
|
+
"""
|
|
92
|
+
Create a one-dimensional array from the elements of a matrix column.
|
|
93
|
+
|
|
94
|
+
:param column: Index of the required column.
|
|
95
|
+
:return: An array containing the column values.
|
|
96
|
+
:raises IndexError: If column index is out of bounds.
|
|
97
|
+
"""
|
|
98
|
+
if column < 0 or column >= self.cols:
|
|
99
|
+
raise IndexError(f"Column index {column} out of bounds")
|
|
100
|
+
return [self.data[row][column] for row in range(self.rows)]
|
|
101
|
+
|
|
102
|
+
def columns(self) -> int:
|
|
103
|
+
"""
|
|
104
|
+
Return the number of columns in the matrix.
|
|
105
|
+
|
|
106
|
+
:return: The number of columns.
|
|
107
|
+
"""
|
|
108
|
+
return self.cols
|
|
109
|
+
|
|
110
|
+
def concat(self, other: Matrix) -> Matrix:
|
|
111
|
+
"""
|
|
112
|
+
Append another matrix to this matrix.
|
|
113
|
+
|
|
114
|
+
:param other: Matrix object whose elements will be appended.
|
|
115
|
+
:return: This matrix after concatenation.
|
|
116
|
+
:raises ValueError: If matrices don't have the same number of columns.
|
|
117
|
+
"""
|
|
118
|
+
if self.cols != other.cols:
|
|
119
|
+
raise ValueError("Matrices must have the same number of columns")
|
|
120
|
+
|
|
121
|
+
# Append all rows from other matrix
|
|
122
|
+
for row in other.data:
|
|
123
|
+
self.data.append(row[:])
|
|
124
|
+
self.rows += other.rows
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def copy(self) -> Matrix:
|
|
128
|
+
"""
|
|
129
|
+
Create a new matrix which is a copy of the original.
|
|
130
|
+
|
|
131
|
+
:return: A new matrix object containing a deep copy of this matrix.
|
|
132
|
+
"""
|
|
133
|
+
new_matrix = Matrix(self.rows, self.cols)
|
|
134
|
+
new_matrix.data = copy.deepcopy(self.data)
|
|
135
|
+
return new_matrix
|
|
136
|
+
|
|
137
|
+
def det(self) -> float | int:
|
|
138
|
+
"""
|
|
139
|
+
Return the determinant of a square matrix.
|
|
140
|
+
|
|
141
|
+
:return: The determinant value of the matrix.
|
|
142
|
+
:raises ValueError: If matrix is not square.
|
|
143
|
+
"""
|
|
144
|
+
if self.rows != self.cols:
|
|
145
|
+
raise ValueError("Determinant can only be calculated for square matrices")
|
|
146
|
+
|
|
147
|
+
if self.rows == 0:
|
|
148
|
+
return 1
|
|
149
|
+
if self.rows == 1:
|
|
150
|
+
return self.data[0][0]
|
|
151
|
+
if self.rows == 2:
|
|
152
|
+
return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0]
|
|
153
|
+
|
|
154
|
+
# For larger matrices, use LU decomposition
|
|
155
|
+
return self._lu_determinant()
|
|
156
|
+
|
|
157
|
+
def diff(self, other: Matrix | int | float) -> Matrix:
|
|
158
|
+
"""
|
|
159
|
+
Return a new matrix resulting from subtraction.
|
|
160
|
+
|
|
161
|
+
:param other: Matrix object or scalar value to be subtracted.
|
|
162
|
+
:return: A new matrix containing the difference.
|
|
163
|
+
:raises ValueError: If matrix dimensions don't match.
|
|
164
|
+
"""
|
|
165
|
+
result = Matrix(self.rows, self.cols)
|
|
166
|
+
|
|
167
|
+
if isinstance(other, Matrix):
|
|
168
|
+
if self.rows != other.rows or self.cols != other.cols:
|
|
169
|
+
raise ValueError("Matrices must have same dimensions")
|
|
170
|
+
for i in range(self.rows):
|
|
171
|
+
for j in range(self.cols):
|
|
172
|
+
result.data[i][j] = self.data[i][j] - other.data[i][j]
|
|
173
|
+
else:
|
|
174
|
+
# Scalar subtraction
|
|
175
|
+
for i in range(self.rows):
|
|
176
|
+
for j in range(self.cols):
|
|
177
|
+
result.data[i][j] = self.data[i][j] - other
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def elements_count(self) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Return the total number of all matrix elements.
|
|
184
|
+
|
|
185
|
+
:return: The total number of elements (rows * columns).
|
|
186
|
+
"""
|
|
187
|
+
return self.rows * self.cols
|
|
188
|
+
|
|
189
|
+
def fill(self, value: T, from_row: int = 0, to_row: int | None = None,
|
|
190
|
+
from_column: int = 0, to_column: int | None = None) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Fill a rectangular area of the matrix with the specified value.
|
|
193
|
+
|
|
194
|
+
:param value: The value to fill with.
|
|
195
|
+
:param from_row: Row index from which the fill will begin (inclusive).
|
|
196
|
+
:param to_row: Row index where the fill will end (exclusive). If None, fills to end.
|
|
197
|
+
:param from_column: Column index from which the fill will begin (inclusive).
|
|
198
|
+
:param to_column: Column index where the fill will end (exclusive). If None, fills to end.
|
|
199
|
+
"""
|
|
200
|
+
_to_row: int = self.rows if to_row is None else to_row
|
|
201
|
+
_to_col: int = self.cols if to_column is None else to_column
|
|
202
|
+
|
|
203
|
+
for i in range(from_row, _to_row):
|
|
204
|
+
for j in range(from_column, _to_col):
|
|
205
|
+
if 0 <= i < self.rows and 0 <= j < self.cols:
|
|
206
|
+
self.data[i][j] = value
|
|
207
|
+
|
|
208
|
+
def get(self, row: int, column: int) -> T:
|
|
209
|
+
"""
|
|
210
|
+
Return the element with the specified index of the matrix.
|
|
211
|
+
|
|
212
|
+
:param row: Index of the required row.
|
|
213
|
+
:param column: Index of the required column.
|
|
214
|
+
:return: The value at the specified position.
|
|
215
|
+
:raises IndexError: If indices are out of bounds.
|
|
216
|
+
"""
|
|
217
|
+
if row < 0 or row >= self.rows or column < 0 or column >= self.cols:
|
|
218
|
+
raise IndexError(f"Index ({row}, {column}) out of bounds")
|
|
219
|
+
return self.data[row][column]
|
|
220
|
+
|
|
221
|
+
def set(self, row: int, column: int, value: T) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Assign value to the element at the specified row and column.
|
|
224
|
+
|
|
225
|
+
:param row: The row index of the element to be modified.
|
|
226
|
+
:param column: The column index of the element to be modified.
|
|
227
|
+
:param value: The new value to be set.
|
|
228
|
+
:raises IndexError: If indices are out of bounds.
|
|
229
|
+
"""
|
|
230
|
+
if row < 0 or row >= self.rows or column < 0 or column >= self.cols:
|
|
231
|
+
raise IndexError(f"Index ({row}, {column}) out of bounds")
|
|
232
|
+
self.data[row][column] = value
|
|
233
|
+
|
|
234
|
+
def max(self) -> float | int | NA[float]:
|
|
235
|
+
"""
|
|
236
|
+
Return the largest value from the matrix elements.
|
|
237
|
+
|
|
238
|
+
:return: The maximum value from the matrix.
|
|
239
|
+
"""
|
|
240
|
+
max_val: float | int | None = None
|
|
241
|
+
for row in self.data:
|
|
242
|
+
for val in row:
|
|
243
|
+
if not (isinstance(val, NA) or val != val):
|
|
244
|
+
if max_val is None or val > max_val:
|
|
245
|
+
max_val = val
|
|
246
|
+
return max_val if max_val is not None else na_float
|
|
247
|
+
|
|
248
|
+
def min(self) -> float | int | NA[float]:
|
|
249
|
+
"""
|
|
250
|
+
Return the smallest value from the matrix elements.
|
|
251
|
+
|
|
252
|
+
:return: The minimum value from the matrix.
|
|
253
|
+
"""
|
|
254
|
+
min_val: float | int | None = None
|
|
255
|
+
for row in self.data:
|
|
256
|
+
for val in row:
|
|
257
|
+
if not (isinstance(val, NA) or val != val):
|
|
258
|
+
if min_val is None or val < min_val:
|
|
259
|
+
min_val = val
|
|
260
|
+
return min_val if min_val is not None else na_float
|
|
261
|
+
|
|
262
|
+
def median(self) -> float | int | NA[float]:
|
|
263
|
+
"""
|
|
264
|
+
Calculate the median ("middle" value) of matrix elements.
|
|
265
|
+
|
|
266
|
+
:return: The median value of all non-NA elements.
|
|
267
|
+
"""
|
|
268
|
+
values = []
|
|
269
|
+
for row in self.data:
|
|
270
|
+
for val in row:
|
|
271
|
+
if not (isinstance(val, NA) or val != val):
|
|
272
|
+
values.append(val)
|
|
273
|
+
|
|
274
|
+
if not values:
|
|
275
|
+
return na_float
|
|
276
|
+
|
|
277
|
+
values.sort()
|
|
278
|
+
n = len(values)
|
|
279
|
+
if n % 2 == 0:
|
|
280
|
+
return (values[n // 2 - 1] + values[n // 2]) / 2
|
|
281
|
+
else:
|
|
282
|
+
return values[n // 2]
|
|
283
|
+
|
|
284
|
+
def mode(self) -> float | int | NA[float]:
|
|
285
|
+
"""
|
|
286
|
+
Calculate the mode of the matrix.
|
|
287
|
+
|
|
288
|
+
Returns the most frequently occurring value. When there are multiple
|
|
289
|
+
values occurring equally frequently, returns the smallest.
|
|
290
|
+
|
|
291
|
+
:return: The most frequently occurring value from the matrix.
|
|
292
|
+
"""
|
|
293
|
+
values = []
|
|
294
|
+
for row in self.data:
|
|
295
|
+
for val in row:
|
|
296
|
+
if not (isinstance(val, NA) or val != val):
|
|
297
|
+
values.append(val)
|
|
298
|
+
|
|
299
|
+
if not values:
|
|
300
|
+
return na_float
|
|
301
|
+
|
|
302
|
+
counter = Counter(values)
|
|
303
|
+
if not counter:
|
|
304
|
+
return na_float
|
|
305
|
+
|
|
306
|
+
# Get all values with max count
|
|
307
|
+
max_count = max(counter.values())
|
|
308
|
+
modes = [val for val, count in counter.items() if count == max_count]
|
|
309
|
+
|
|
310
|
+
# Return smallest if multiple modes
|
|
311
|
+
return min(modes)
|
|
312
|
+
|
|
313
|
+
def mult(self, other: Matrix | list[T] | int | float) -> Matrix | list[T]:
|
|
314
|
+
"""
|
|
315
|
+
Return the product of matrices, matrix and vector, or matrix and scalar.
|
|
316
|
+
|
|
317
|
+
:param other: Second matrix object, array, or scalar value.
|
|
318
|
+
:return: A new matrix (matrix multiplication) or array (vector multiplication).
|
|
319
|
+
:raises ValueError: If dimensions are incompatible for multiplication.
|
|
320
|
+
"""
|
|
321
|
+
if isinstance(other, Matrix):
|
|
322
|
+
# Matrix multiplication
|
|
323
|
+
if self.cols != other.rows:
|
|
324
|
+
raise ValueError("Invalid dimensions for matrix multiplication")
|
|
325
|
+
|
|
326
|
+
result = Matrix(self.rows, other.cols)
|
|
327
|
+
for i in range(self.rows):
|
|
328
|
+
for j in range(other.cols):
|
|
329
|
+
sum_val = 0
|
|
330
|
+
for k in range(self.cols):
|
|
331
|
+
sum_val += self.data[i][k] * other.data[k][j]
|
|
332
|
+
result.data[i][j] = sum_val
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
elif isinstance(other, list):
|
|
336
|
+
# Matrix-vector multiplication
|
|
337
|
+
if self.cols != len(other):
|
|
338
|
+
raise ValueError("Invalid dimensions for matrix-vector multiplication")
|
|
339
|
+
|
|
340
|
+
result = []
|
|
341
|
+
for i in range(self.rows):
|
|
342
|
+
sum_val = 0
|
|
343
|
+
for j in range(self.cols):
|
|
344
|
+
sum_val += self.data[i][j] * other[j]
|
|
345
|
+
result.append(sum_val)
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
else:
|
|
349
|
+
# Scalar multiplication
|
|
350
|
+
result = Matrix(self.rows, self.cols)
|
|
351
|
+
for i in range(self.rows):
|
|
352
|
+
for j in range(self.cols):
|
|
353
|
+
result.data[i][j] = self.data[i][j] * other
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
def remove_col(self, column: int | None = None) -> list[T]:
|
|
357
|
+
"""
|
|
358
|
+
Remove the column at the specified index and return its values.
|
|
359
|
+
|
|
360
|
+
:param column: The index of the column to be removed. If None, removes last column.
|
|
361
|
+
:return: An array containing the removed column's values.
|
|
362
|
+
:raises IndexError: If column index is out of bounds.
|
|
363
|
+
"""
|
|
364
|
+
col_idx: int = (self.cols - 1) if column is None else column
|
|
365
|
+
|
|
366
|
+
if col_idx < 0 or col_idx >= self.cols:
|
|
367
|
+
raise IndexError(f"Column index {col_idx} out of bounds")
|
|
368
|
+
|
|
369
|
+
removed = []
|
|
370
|
+
for row in self.data:
|
|
371
|
+
removed.append(row.pop(col_idx))
|
|
372
|
+
self.cols -= 1
|
|
373
|
+
return removed
|
|
374
|
+
|
|
375
|
+
def remove_row(self, row: int | None = None) -> list[T]:
|
|
376
|
+
"""
|
|
377
|
+
Remove the row at the specified index and return its values.
|
|
378
|
+
|
|
379
|
+
:param row: The index of the row to be removed. If None, removes last row.
|
|
380
|
+
:return: An array containing the removed row's values.
|
|
381
|
+
:raises IndexError: If row index is out of bounds.
|
|
382
|
+
"""
|
|
383
|
+
row_idx: int = (self.rows - 1) if row is None else row
|
|
384
|
+
|
|
385
|
+
if row_idx < 0 or row_idx >= self.rows:
|
|
386
|
+
raise IndexError(f"Row index {row_idx} out of bounds")
|
|
387
|
+
|
|
388
|
+
removed = self.data.pop(row_idx)
|
|
389
|
+
self.rows -= 1
|
|
390
|
+
return removed
|
|
391
|
+
|
|
392
|
+
def reshape(self, rows: int, columns: int) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Rebuild the matrix to the specified dimensions.
|
|
395
|
+
|
|
396
|
+
:param rows: The number of rows of the reshaped matrix.
|
|
397
|
+
:param columns: The number of columns of the reshaped matrix.
|
|
398
|
+
:raises ValueError: If new shape doesn't have same number of elements.
|
|
399
|
+
"""
|
|
400
|
+
if rows * columns != self.rows * self.cols:
|
|
401
|
+
raise ValueError("New shape must have same number of elements")
|
|
402
|
+
|
|
403
|
+
# Flatten to 1D
|
|
404
|
+
flat = []
|
|
405
|
+
for row in self.data:
|
|
406
|
+
flat.extend(row)
|
|
407
|
+
|
|
408
|
+
# Reshape to new dimensions
|
|
409
|
+
self.data = []
|
|
410
|
+
for i in range(rows):
|
|
411
|
+
self.data.append(flat[i * columns:(i + 1) * columns])
|
|
412
|
+
|
|
413
|
+
self.rows = rows
|
|
414
|
+
self.cols = columns
|
|
415
|
+
|
|
416
|
+
def reverse(self) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Reverse the order of rows and columns in the matrix.
|
|
419
|
+
|
|
420
|
+
The first row and first column become the last, and the last become the first.
|
|
421
|
+
"""
|
|
422
|
+
# Reverse rows
|
|
423
|
+
self.data.reverse()
|
|
424
|
+
# Reverse each row
|
|
425
|
+
for row in self.data:
|
|
426
|
+
row.reverse()
|
|
427
|
+
|
|
428
|
+
def row(self, row: int) -> list[T]:
|
|
429
|
+
"""
|
|
430
|
+
Create a one-dimensional array from the elements of a matrix row.
|
|
431
|
+
|
|
432
|
+
:param row: Index of the required row.
|
|
433
|
+
:return: An array containing the row values.
|
|
434
|
+
:raises IndexError: If row index is out of bounds.
|
|
435
|
+
"""
|
|
436
|
+
if row < 0 or row >= self.rows:
|
|
437
|
+
raise IndexError(f"Row index {row} out of bounds")
|
|
438
|
+
return self.data[row][:]
|
|
439
|
+
|
|
440
|
+
def sort(self, column: int = 0, order: str = 'ascending') -> None:
|
|
441
|
+
"""
|
|
442
|
+
Rearrange rows following the sorted order of values in the specified column.
|
|
443
|
+
|
|
444
|
+
:param column: Index of the column whose sorted values determine the new order of rows.
|
|
445
|
+
:param order: The sort order. 'ascending' (default) or 'descending'.
|
|
446
|
+
:raises IndexError: If column index is out of bounds.
|
|
447
|
+
"""
|
|
448
|
+
if column < 0 or column >= self.cols:
|
|
449
|
+
raise IndexError(f"Column index {column} out of bounds")
|
|
450
|
+
|
|
451
|
+
reverse = order == 'descending'
|
|
452
|
+
self.data.sort(key=lambda row: row[column], reverse=reverse)
|
|
453
|
+
|
|
454
|
+
def submatrix(self, from_row: int = 0, to_row: int | None = None,
|
|
455
|
+
from_column: int = 0, to_column: int | None = None) -> Matrix:
|
|
456
|
+
"""
|
|
457
|
+
Extract a submatrix within the specified indices.
|
|
458
|
+
|
|
459
|
+
:param from_row: Row index from which extraction begins (inclusive).
|
|
460
|
+
:param to_row: Row index where extraction ends (exclusive). If None, extracts to end.
|
|
461
|
+
:param from_column: Column index from which extraction begins (inclusive).
|
|
462
|
+
:param to_column: Column index where extraction ends (exclusive). If None, extracts to end.
|
|
463
|
+
:return: A new matrix containing the submatrix.
|
|
464
|
+
"""
|
|
465
|
+
_to_row: int = self.rows if to_row is None else to_row
|
|
466
|
+
_to_col: int = self.cols if to_column is None else to_column
|
|
467
|
+
|
|
468
|
+
result = Matrix(_to_row - from_row, _to_col - from_column)
|
|
469
|
+
for i in range(from_row, _to_row):
|
|
470
|
+
for j in range(from_column, _to_col):
|
|
471
|
+
if 0 <= i < self.rows and 0 <= j < self.cols:
|
|
472
|
+
result.data[i - from_row][j - from_column] = self.data[i][j]
|
|
473
|
+
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
def sum(self, other: Matrix | int | float) -> Matrix:
|
|
477
|
+
"""
|
|
478
|
+
Return a new matrix resulting from addition.
|
|
479
|
+
|
|
480
|
+
:param other: Second matrix object or scalar value.
|
|
481
|
+
:return: A new matrix containing the sum.
|
|
482
|
+
:raises ValueError: If matrix dimensions don't match.
|
|
483
|
+
"""
|
|
484
|
+
result = Matrix(self.rows, self.cols)
|
|
485
|
+
|
|
486
|
+
if isinstance(other, Matrix):
|
|
487
|
+
if self.rows != other.rows or self.cols != other.cols:
|
|
488
|
+
raise ValueError("Matrices must have same dimensions")
|
|
489
|
+
for i in range(self.rows):
|
|
490
|
+
for j in range(self.cols):
|
|
491
|
+
result.data[i][j] = self.data[i][j] + other.data[i][j]
|
|
492
|
+
else:
|
|
493
|
+
# Scalar addition
|
|
494
|
+
for i in range(self.rows):
|
|
495
|
+
for j in range(self.cols):
|
|
496
|
+
result.data[i][j] = self.data[i][j] + other
|
|
497
|
+
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
def swap_columns(self, column1: int, column2: int) -> None:
|
|
501
|
+
"""
|
|
502
|
+
Swap the columns at the specified indices.
|
|
503
|
+
|
|
504
|
+
:param column1: Index of the first column to be swapped.
|
|
505
|
+
:param column2: Index of the second column to be swapped.
|
|
506
|
+
:raises IndexError: If column indices are out of bounds.
|
|
507
|
+
"""
|
|
508
|
+
if column1 < 0 or column1 >= self.cols or column2 < 0 or column2 >= self.cols:
|
|
509
|
+
raise IndexError("Column index out of bounds")
|
|
510
|
+
|
|
511
|
+
for row in self.data:
|
|
512
|
+
row[column1], row[column2] = row[column2], row[column1]
|
|
513
|
+
|
|
514
|
+
def swap_rows(self, row1: int, row2: int) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Swap the rows at the specified indices.
|
|
517
|
+
|
|
518
|
+
:param row1: Index of the first row to be swapped.
|
|
519
|
+
:param row2: Index of the second row to be swapped.
|
|
520
|
+
:raises IndexError: If row indices are out of bounds.
|
|
521
|
+
"""
|
|
522
|
+
if row1 < 0 or row1 >= self.rows or row2 < 0 or row2 >= self.rows:
|
|
523
|
+
raise IndexError("Row index out of bounds")
|
|
524
|
+
|
|
525
|
+
self.data[row1], self.data[row2] = self.data[row2], self.data[row1]
|
|
526
|
+
|
|
527
|
+
def trace(self) -> float | int:
|
|
528
|
+
"""
|
|
529
|
+
Calculate the trace of a matrix (sum of the main diagonal's elements).
|
|
530
|
+
|
|
531
|
+
:return: The trace value of the matrix.
|
|
532
|
+
:raises ValueError: If matrix is not square.
|
|
533
|
+
"""
|
|
534
|
+
if self.rows != self.cols:
|
|
535
|
+
raise ValueError("Trace can only be calculated for square matrices")
|
|
536
|
+
|
|
537
|
+
trace_sum = 0
|
|
538
|
+
for i in range(self.rows):
|
|
539
|
+
trace_sum += self.data[i][i]
|
|
540
|
+
return trace_sum
|
|
541
|
+
|
|
542
|
+
def transpose(self) -> Matrix:
|
|
543
|
+
"""
|
|
544
|
+
Create a new transposed version of the matrix.
|
|
545
|
+
|
|
546
|
+
This interchanges the row and column index of each element.
|
|
547
|
+
|
|
548
|
+
:return: A new matrix containing the transposed version.
|
|
549
|
+
"""
|
|
550
|
+
result = Matrix(self.cols, self.rows)
|
|
551
|
+
for i in range(self.rows):
|
|
552
|
+
for j in range(self.cols):
|
|
553
|
+
result.data[j][i] = self.data[i][j]
|
|
554
|
+
return result
|
|
555
|
+
|
|
556
|
+
# Helper methods
|
|
557
|
+
def _lu_determinant(self) -> float | int:
|
|
558
|
+
"""
|
|
559
|
+
Calculate determinant using LU decomposition.
|
|
560
|
+
|
|
561
|
+
:return: The determinant value.
|
|
562
|
+
"""
|
|
563
|
+
# Simplified LU for determinant calculation
|
|
564
|
+
n = self.rows
|
|
565
|
+
lu = [row[:] for row in self.data] # Copy matrix
|
|
566
|
+
|
|
567
|
+
det = 1
|
|
568
|
+
for i in range(n):
|
|
569
|
+
# Find pivot
|
|
570
|
+
max_row = i
|
|
571
|
+
for k in range(i + 1, n):
|
|
572
|
+
if abs(lu[k][i]) > abs(lu[max_row][i]):
|
|
573
|
+
max_row = k
|
|
574
|
+
|
|
575
|
+
# Swap rows if needed
|
|
576
|
+
if max_row != i:
|
|
577
|
+
lu[i], lu[max_row] = lu[max_row], lu[i]
|
|
578
|
+
det *= -1
|
|
579
|
+
|
|
580
|
+
# Check for zero pivot
|
|
581
|
+
if lu[i][i] == 0:
|
|
582
|
+
return 0
|
|
583
|
+
|
|
584
|
+
det *= lu[i][i]
|
|
585
|
+
|
|
586
|
+
# Eliminate column
|
|
587
|
+
for k in range(i + 1, n):
|
|
588
|
+
factor = lu[k][i] / lu[i][i]
|
|
589
|
+
for j in range(i + 1, n):
|
|
590
|
+
lu[k][j] -= factor * lu[i][j]
|
|
591
|
+
|
|
592
|
+
return det
|
|
593
|
+
|
|
594
|
+
# is_* check methods
|
|
595
|
+
def is_antidiagonal(self) -> bool:
|
|
596
|
+
"""
|
|
597
|
+
Determine if the matrix is anti-diagonal.
|
|
598
|
+
|
|
599
|
+
All elements outside the secondary diagonal are zero.
|
|
600
|
+
|
|
601
|
+
:return: True if matrix is anti-diagonal, False otherwise.
|
|
602
|
+
"""
|
|
603
|
+
if self.rows != self.cols:
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
for i in range(self.rows):
|
|
607
|
+
for j in range(self.cols):
|
|
608
|
+
if i + j != self.rows - 1 and self.data[i][j] != 0:
|
|
609
|
+
return False
|
|
610
|
+
return True
|
|
611
|
+
|
|
612
|
+
def is_antisymmetric(self) -> bool:
|
|
613
|
+
"""
|
|
614
|
+
Determine if a matrix is antisymmetric.
|
|
615
|
+
|
|
616
|
+
A matrix is antisymmetric if its transpose equals its negative.
|
|
617
|
+
|
|
618
|
+
:return: True if matrix is antisymmetric, False otherwise.
|
|
619
|
+
"""
|
|
620
|
+
if self.rows != self.cols:
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
for i in range(self.rows):
|
|
624
|
+
for j in range(self.cols):
|
|
625
|
+
if self.data[i][j] != -self.data[j][i]:
|
|
626
|
+
return False
|
|
627
|
+
return True
|
|
628
|
+
|
|
629
|
+
def is_binary(self) -> bool:
|
|
630
|
+
"""
|
|
631
|
+
Determine if the matrix is binary.
|
|
632
|
+
|
|
633
|
+
A matrix is binary when all elements are 0 or 1.
|
|
634
|
+
|
|
635
|
+
:return: True if matrix is binary, False otherwise.
|
|
636
|
+
"""
|
|
637
|
+
for row in self.data:
|
|
638
|
+
for val in row:
|
|
639
|
+
if not (isinstance(val, NA) or val != val) and val != 0 and val != 1:
|
|
640
|
+
return False
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
def is_diagonal(self) -> bool:
|
|
644
|
+
"""
|
|
645
|
+
Determine if the matrix is diagonal.
|
|
646
|
+
|
|
647
|
+
All elements outside the main diagonal are zero.
|
|
648
|
+
|
|
649
|
+
:return: True if matrix is diagonal, False otherwise.
|
|
650
|
+
"""
|
|
651
|
+
if self.rows != self.cols:
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
for i in range(self.rows):
|
|
655
|
+
for j in range(self.cols):
|
|
656
|
+
if i != j and self.data[i][j] != 0:
|
|
657
|
+
return False
|
|
658
|
+
return True
|
|
659
|
+
|
|
660
|
+
def is_identity(self) -> bool:
|
|
661
|
+
"""
|
|
662
|
+
Determine if a matrix is an identity matrix.
|
|
663
|
+
|
|
664
|
+
Elements on the main diagonal are ones and zeros elsewhere.
|
|
665
|
+
|
|
666
|
+
:return: True if matrix is identity, False otherwise.
|
|
667
|
+
"""
|
|
668
|
+
if self.rows != self.cols:
|
|
669
|
+
return False
|
|
670
|
+
|
|
671
|
+
for i in range(self.rows):
|
|
672
|
+
for j in range(self.cols):
|
|
673
|
+
expected = 1 if i == j else 0
|
|
674
|
+
if self.data[i][j] != expected:
|
|
675
|
+
return False
|
|
676
|
+
return True
|
|
677
|
+
|
|
678
|
+
def is_square(self) -> bool:
|
|
679
|
+
"""
|
|
680
|
+
Determine if the matrix is square.
|
|
681
|
+
|
|
682
|
+
A matrix is square if it has the same number of rows and columns.
|
|
683
|
+
|
|
684
|
+
:return: True if matrix is square, False otherwise.
|
|
685
|
+
"""
|
|
686
|
+
return self.rows == self.cols
|
|
687
|
+
|
|
688
|
+
def is_stochastic(self) -> bool:
|
|
689
|
+
"""
|
|
690
|
+
Determine if the matrix is stochastic.
|
|
691
|
+
|
|
692
|
+
A matrix is stochastic if all row sums equal 1.
|
|
693
|
+
|
|
694
|
+
:return: True if matrix is stochastic, False otherwise.
|
|
695
|
+
"""
|
|
696
|
+
for row in self.data:
|
|
697
|
+
row_sum = sum(val for val in row if not (isinstance(val, NA) or val != val))
|
|
698
|
+
if abs(row_sum - 1.0) > 1e-10:
|
|
699
|
+
return False
|
|
700
|
+
return True
|
|
701
|
+
|
|
702
|
+
def is_symmetric(self) -> bool:
|
|
703
|
+
"""
|
|
704
|
+
Determine if a square matrix is symmetric.
|
|
705
|
+
|
|
706
|
+
Elements are symmetric with respect to the main diagonal.
|
|
707
|
+
|
|
708
|
+
:return: True if matrix is symmetric, False otherwise.
|
|
709
|
+
"""
|
|
710
|
+
if self.rows != self.cols:
|
|
711
|
+
return False
|
|
712
|
+
|
|
713
|
+
for i in range(self.rows):
|
|
714
|
+
for j in range(i + 1, self.cols):
|
|
715
|
+
if self.data[i][j] != self.data[j][i]:
|
|
716
|
+
return False
|
|
717
|
+
return True
|
|
718
|
+
|
|
719
|
+
def is_triangular(self) -> bool:
|
|
720
|
+
"""
|
|
721
|
+
Determine if the matrix is triangular.
|
|
722
|
+
|
|
723
|
+
A matrix is triangular if all elements above or below the main diagonal are zero.
|
|
724
|
+
|
|
725
|
+
:return: True if matrix is triangular, False otherwise.
|
|
726
|
+
"""
|
|
727
|
+
if self.rows != self.cols:
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
# Check if upper triangular
|
|
731
|
+
upper = True
|
|
732
|
+
for i in range(1, self.rows):
|
|
733
|
+
for j in range(i):
|
|
734
|
+
if self.data[i][j] != 0:
|
|
735
|
+
upper = False
|
|
736
|
+
break
|
|
737
|
+
if not upper:
|
|
738
|
+
break
|
|
739
|
+
|
|
740
|
+
# Check if lower triangular
|
|
741
|
+
lower = True
|
|
742
|
+
for i in range(self.rows - 1):
|
|
743
|
+
for j in range(i + 1, self.cols):
|
|
744
|
+
if self.data[i][j] != 0:
|
|
745
|
+
lower = False
|
|
746
|
+
break
|
|
747
|
+
if not lower:
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
return upper or lower
|
|
751
|
+
|
|
752
|
+
def is_zero(self) -> bool:
|
|
753
|
+
"""
|
|
754
|
+
Determine if all elements of the matrix are zero.
|
|
755
|
+
|
|
756
|
+
:return: True if all elements are zero, False otherwise.
|
|
757
|
+
"""
|
|
758
|
+
for row in self.data:
|
|
759
|
+
for val in row:
|
|
760
|
+
if not (isinstance(val, NA) or val != val) and val != 0:
|
|
761
|
+
return False
|
|
762
|
+
return True
|
|
763
|
+
|
|
764
|
+
# Additional methods
|
|
765
|
+
def kron(self, other: Matrix) -> Matrix:
|
|
766
|
+
"""
|
|
767
|
+
Return the Kronecker product of two matrices.
|
|
768
|
+
|
|
769
|
+
:param other: Second matrix object.
|
|
770
|
+
:return: A new matrix containing the Kronecker product.
|
|
771
|
+
"""
|
|
772
|
+
result_rows = self.rows * other.rows
|
|
773
|
+
result_cols = self.cols * other.cols
|
|
774
|
+
result = Matrix(result_rows, result_cols)
|
|
775
|
+
|
|
776
|
+
for i in range(self.rows):
|
|
777
|
+
for j in range(self.cols):
|
|
778
|
+
for k in range(other.rows):
|
|
779
|
+
for l in range(other.cols):
|
|
780
|
+
result.data[i * other.rows + k][j * other.cols + l] = \
|
|
781
|
+
self.data[i][j] * other.data[k][l]
|
|
782
|
+
|
|
783
|
+
return result
|
|
784
|
+
|
|
785
|
+
def pow(self, power: int) -> Matrix:
|
|
786
|
+
"""
|
|
787
|
+
Calculate the product of the matrix by itself power times.
|
|
788
|
+
|
|
789
|
+
:param power: The number of times the matrix will be multiplied by itself.
|
|
790
|
+
:return: The matrix raised to the specified power.
|
|
791
|
+
:raises ValueError: If matrix is not square.
|
|
792
|
+
"""
|
|
793
|
+
if self.rows != self.cols:
|
|
794
|
+
raise ValueError("Power can only be calculated for square matrices")
|
|
795
|
+
|
|
796
|
+
if power == 0:
|
|
797
|
+
# Return identity matrix
|
|
798
|
+
result = Matrix(self.rows, self.cols)
|
|
799
|
+
for i in range(self.rows):
|
|
800
|
+
result.data[i][i] = 1
|
|
801
|
+
return result
|
|
802
|
+
|
|
803
|
+
result = self.copy()
|
|
804
|
+
for _ in range(power - 1):
|
|
805
|
+
result = cast(Matrix, result.mult(self))
|
|
806
|
+
|
|
807
|
+
return result
|
|
808
|
+
|
|
809
|
+
def rank(self) -> int:
|
|
810
|
+
"""
|
|
811
|
+
Calculate the rank of the matrix.
|
|
812
|
+
|
|
813
|
+
:return: The rank of the matrix.
|
|
814
|
+
"""
|
|
815
|
+
# Simple rank calculation using row echelon form
|
|
816
|
+
m = [row[:] for row in self.data] # Copy matrix
|
|
817
|
+
rows, cols = self.rows, self.cols
|
|
818
|
+
rank = 0
|
|
819
|
+
|
|
820
|
+
for col in range(cols):
|
|
821
|
+
# Find pivot
|
|
822
|
+
pivot_row = None
|
|
823
|
+
for row in range(rank, rows):
|
|
824
|
+
if m[row][col] != 0:
|
|
825
|
+
pivot_row = row
|
|
826
|
+
break
|
|
827
|
+
|
|
828
|
+
if pivot_row is None:
|
|
829
|
+
continue
|
|
830
|
+
|
|
831
|
+
# Swap rows
|
|
832
|
+
m[rank], m[pivot_row] = m[pivot_row], m[rank]
|
|
833
|
+
|
|
834
|
+
# Eliminate column
|
|
835
|
+
for row in range(rank + 1, rows):
|
|
836
|
+
if m[row][col] != 0:
|
|
837
|
+
factor = m[row][col] / m[rank][col]
|
|
838
|
+
for j in range(col, cols):
|
|
839
|
+
m[row][j] -= factor * m[rank][j]
|
|
840
|
+
|
|
841
|
+
rank += 1
|
|
842
|
+
|
|
843
|
+
return rank
|
|
844
|
+
|
|
845
|
+
def inv(self) -> Matrix:
|
|
846
|
+
"""
|
|
847
|
+
Return the inverse of a square matrix.
|
|
848
|
+
|
|
849
|
+
:return: A new matrix which is the inverse of this matrix.
|
|
850
|
+
:raises ValueError: If matrix is not square or is singular.
|
|
851
|
+
"""
|
|
852
|
+
if self.rows != self.cols:
|
|
853
|
+
raise ValueError("Inverse can only be calculated for square matrices")
|
|
854
|
+
|
|
855
|
+
n = self.rows
|
|
856
|
+
# Create augmented matrix [A|I]
|
|
857
|
+
aug = [row[:] + [0] * n for row in self.data]
|
|
858
|
+
for i in range(n):
|
|
859
|
+
aug[i][n + i] = 1
|
|
860
|
+
|
|
861
|
+
# Forward elimination
|
|
862
|
+
for i in range(n):
|
|
863
|
+
# Find pivot
|
|
864
|
+
max_row = i
|
|
865
|
+
for k in range(i + 1, n):
|
|
866
|
+
if abs(aug[k][i]) > abs(aug[max_row][i]):
|
|
867
|
+
max_row = k
|
|
868
|
+
|
|
869
|
+
aug[i], aug[max_row] = aug[max_row], aug[i]
|
|
870
|
+
|
|
871
|
+
# Check for singular matrix
|
|
872
|
+
if aug[i][i] == 0:
|
|
873
|
+
raise ValueError("Matrix is singular")
|
|
874
|
+
|
|
875
|
+
# Scale pivot row
|
|
876
|
+
pivot = aug[i][i]
|
|
877
|
+
for j in range(2 * n):
|
|
878
|
+
aug[i][j] /= pivot
|
|
879
|
+
|
|
880
|
+
# Eliminate column
|
|
881
|
+
for k in range(n):
|
|
882
|
+
if k != i:
|
|
883
|
+
factor = aug[k][i]
|
|
884
|
+
for j in range(2 * n):
|
|
885
|
+
aug[k][j] -= factor * aug[i][j]
|
|
886
|
+
|
|
887
|
+
# Extract inverse from augmented matrix
|
|
888
|
+
result = Matrix(n, n)
|
|
889
|
+
for i in range(n):
|
|
890
|
+
for j in range(n):
|
|
891
|
+
result.data[i][j] = aug[i][n + j]
|
|
892
|
+
|
|
893
|
+
return result
|
|
894
|
+
|
|
895
|
+
def pinv(self) -> Matrix:
|
|
896
|
+
"""
|
|
897
|
+
Return the pseudoinverse of a matrix.
|
|
898
|
+
|
|
899
|
+
Uses Moore-Penrose inverse formula. For non-singular square matrices,
|
|
900
|
+
this returns the same result as inv().
|
|
901
|
+
|
|
902
|
+
:return: A new matrix containing the pseudoinverse.
|
|
903
|
+
"""
|
|
904
|
+
# For simplicity, if matrix is square and non-singular, return regular inverse
|
|
905
|
+
if self.is_square():
|
|
906
|
+
try:
|
|
907
|
+
return self.inv()
|
|
908
|
+
except ValueError:
|
|
909
|
+
pass
|
|
910
|
+
|
|
911
|
+
# Otherwise, use A+ = (A^T A)^(-1) A^T for overdetermined systems
|
|
912
|
+
# or A+ = A^T (A A^T)^(-1) for underdetermined systems
|
|
913
|
+
at = self.transpose()
|
|
914
|
+
|
|
915
|
+
if self.rows >= self.cols:
|
|
916
|
+
# Overdetermined
|
|
917
|
+
ata = cast(Matrix, at.mult(self))
|
|
918
|
+
return cast(Matrix, ata.inv().mult(at))
|
|
919
|
+
else:
|
|
920
|
+
# Underdetermined
|
|
921
|
+
aat = cast(Matrix, self.mult(at))
|
|
922
|
+
return cast(Matrix, at.mult(aat.inv()))
|
|
923
|
+
|
|
924
|
+
def eigenvalues(self) -> list[float | int]:
|
|
925
|
+
"""
|
|
926
|
+
Return an array containing the eigenvalues of a square matrix.
|
|
927
|
+
|
|
928
|
+
:return: An array containing the eigenvalues.
|
|
929
|
+
:raises ValueError: If matrix is not square.
|
|
930
|
+
"""
|
|
931
|
+
if self.rows != self.cols:
|
|
932
|
+
raise ValueError("Eigenvalues can only be calculated for square matrices")
|
|
933
|
+
|
|
934
|
+
# For 2x2 matrix, use analytical solution
|
|
935
|
+
if self.rows == 2:
|
|
936
|
+
a, b = self.data[0][0], self.data[0][1]
|
|
937
|
+
c, d = self.data[1][0], self.data[1][1]
|
|
938
|
+
|
|
939
|
+
trace = a + d
|
|
940
|
+
det = a * d - b * c
|
|
941
|
+
discriminant = trace * trace - 4 * det
|
|
942
|
+
|
|
943
|
+
if discriminant >= 0:
|
|
944
|
+
sqrt_disc = discriminant ** 0.5
|
|
945
|
+
return [(trace + sqrt_disc) / 2, (trace - sqrt_disc) / 2]
|
|
946
|
+
else:
|
|
947
|
+
# Complex eigenvalues - return real parts only
|
|
948
|
+
return [trace / 2, trace / 2]
|
|
949
|
+
|
|
950
|
+
# For larger matrices, this would need a proper algorithm
|
|
951
|
+
# For now, return empty list
|
|
952
|
+
return []
|
|
953
|
+
|
|
954
|
+
def eigenvectors(self) -> Matrix:
|
|
955
|
+
"""
|
|
956
|
+
Return a matrix of eigenvectors.
|
|
957
|
+
|
|
958
|
+
Each column is an eigenvector of the matrix.
|
|
959
|
+
|
|
960
|
+
:return: A new matrix containing the eigenvectors.
|
|
961
|
+
:raises ValueError: If matrix is not square.
|
|
962
|
+
"""
|
|
963
|
+
if self.rows != self.cols:
|
|
964
|
+
raise ValueError("Eigenvectors can only be calculated for square matrices")
|
|
965
|
+
|
|
966
|
+
# Return identity matrix as placeholder
|
|
967
|
+
result = Matrix(self.rows, self.cols)
|
|
968
|
+
for i in range(self.rows):
|
|
969
|
+
result.data[i][i] = 1
|
|
970
|
+
return result
|
|
971
|
+
|
|
972
|
+
def add_col(self, col: int | None = None, array_id: list[Any] | None = None) -> None:
|
|
973
|
+
"""
|
|
974
|
+
Add a column at the specified index of the matrix.
|
|
975
|
+
|
|
976
|
+
The column can consist of NA values, or an array can be used to provide values.
|
|
977
|
+
|
|
978
|
+
:param col: The index where the new column will be inserted. If None, appends to the end.
|
|
979
|
+
:param array_id: Array to use for providing values to the new column.
|
|
980
|
+
If matrix is empty, the array size determines the row count.
|
|
981
|
+
:raises IndexError: If column index is out of bounds.
|
|
982
|
+
"""
|
|
983
|
+
col_idx: int = self.cols if col is None else col
|
|
984
|
+
|
|
985
|
+
if col_idx < 0 or col_idx > self.cols:
|
|
986
|
+
raise IndexError(f"Column index {col_idx} out of bounds")
|
|
987
|
+
|
|
988
|
+
# If matrix is empty (0 rows) and array provided, create rows first
|
|
989
|
+
if self.rows == 0 and array_id and len(array_id) > 0:
|
|
990
|
+
self.rows = len(array_id)
|
|
991
|
+
self.data = [[] for _ in range(self.rows)]
|
|
992
|
+
|
|
993
|
+
for i in range(self.rows):
|
|
994
|
+
if array_id and i < len(array_id):
|
|
995
|
+
self.data[i].insert(col_idx, array_id[i])
|
|
996
|
+
else:
|
|
997
|
+
self.data[i].insert(col_idx, NA(T))
|
|
998
|
+
|
|
999
|
+
self.cols += 1
|