mathcraft-window-tools 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.
- mathcraft/__init__.py +30 -0
- mathcraft/_cursor.py +79 -0
- mathcraft/_cursor_guard.py +99 -0
- mathcraft/_cursor_scheme.py +95 -0
- mathcraft/_desktop_cursor.py +226 -0
- mathcraft/_input.py +325 -0
- mathcraft/mathematics.py +134 -0
- mathcraft/terminal.py +485 -0
- mathcraft_window_tools-0.1.0.dist-info/METADATA +359 -0
- mathcraft_window_tools-0.1.0.dist-info/RECORD +12 -0
- mathcraft_window_tools-0.1.0.dist-info/WHEEL +5 -0
- mathcraft_window_tools-0.1.0.dist-info/top_level.txt +1 -0
mathcraft/_input.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Opt-in, foreground-only hook experiment, not an input security boundary."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes as w
|
|
4
|
+
import queue
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
LRESULT = ctypes.c_ssize_t
|
|
9
|
+
HOOKPROC = ctypes.WINFUNCTYPE(LRESULT, ctypes.c_int, w.WPARAM, w.LPARAM)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KeyboardData(ctypes.Structure):
|
|
13
|
+
_fields_ = [('vk', w.DWORD), ('scan', w.DWORD), ('flags', w.DWORD),
|
|
14
|
+
('time', w.DWORD), ('extra', ctypes.c_size_t)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MouseData(ctypes.Structure):
|
|
18
|
+
_fields_ = [('point', w.POINT), ('data', w.DWORD), ('flags', w.DWORD),
|
|
19
|
+
('time', w.DWORD), ('extra', ctypes.c_size_t)]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ForegroundInput:
|
|
23
|
+
"""Suppress later LL hooks for supported keys/buttons in our client area.
|
|
24
|
+
|
|
25
|
+
Modifiers, system shortcuts and unsupported input pass through.
|
|
26
|
+
Raw Input and kernel observers are outside this experiment's scope.
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self, hwnd, virtual_mouse=False):
|
|
29
|
+
self.hwnd = hwnd
|
|
30
|
+
self.virtual_mouse = virtual_mouse
|
|
31
|
+
self.cursor_hwnd = None
|
|
32
|
+
self._anchor = None
|
|
33
|
+
self._position = None
|
|
34
|
+
self.events = queue.Queue(maxsize=512)
|
|
35
|
+
self.enabled = True
|
|
36
|
+
self.heartbeat = time.monotonic()
|
|
37
|
+
self._stop = threading.Event()
|
|
38
|
+
self._ready = threading.Event()
|
|
39
|
+
self._error = None
|
|
40
|
+
self._keys = set()
|
|
41
|
+
self._buttons = set()
|
|
42
|
+
self.user = ctypes.WinDLL('user32', use_last_error=True)
|
|
43
|
+
self.kernel = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
44
|
+
specs = {
|
|
45
|
+
'SetWindowsHookExW': ([ctypes.c_int, HOOKPROC, w.HINSTANCE, w.DWORD], w.HANDLE),
|
|
46
|
+
'UnhookWindowsHookEx': ([w.HANDLE], w.BOOL),
|
|
47
|
+
'CallNextHookEx': ([w.HANDLE, ctypes.c_int, w.WPARAM, w.LPARAM], LRESULT),
|
|
48
|
+
'SetThreadDpiAwarenessContext': ([w.HANDLE], w.HANDLE),
|
|
49
|
+
'GetForegroundWindow': ([], w.HWND),
|
|
50
|
+
'GetAsyncKeyState': ([ctypes.c_int], ctypes.c_short),
|
|
51
|
+
'GetKeyState': ([ctypes.c_int], ctypes.c_short),
|
|
52
|
+
'GetWindowThreadProcessId': ([w.HWND, ctypes.POINTER(w.DWORD)], w.DWORD),
|
|
53
|
+
'GetKeyboardLayout': ([w.DWORD], w.HANDLE),
|
|
54
|
+
'ToUnicodeEx': ([w.UINT, w.UINT, ctypes.POINTER(w.BYTE), w.LPWSTR,
|
|
55
|
+
ctypes.c_int, w.UINT, w.HANDLE], ctypes.c_int),
|
|
56
|
+
'ScreenToClient': ([w.HWND, ctypes.POINTER(w.POINT)], w.BOOL),
|
|
57
|
+
'ClientToScreen': ([w.HWND, ctypes.POINTER(w.POINT)], w.BOOL),
|
|
58
|
+
'GetCursorPos': ([ctypes.POINTER(w.POINT)], w.BOOL),
|
|
59
|
+
'SetCursorPos': ([ctypes.c_int, ctypes.c_int], w.BOOL),
|
|
60
|
+
'GetClientRect': ([w.HWND, ctypes.POINTER(w.RECT)], w.BOOL),
|
|
61
|
+
'WindowFromPoint': ([w.POINT], w.HWND),
|
|
62
|
+
'GetAncestor': ([w.HWND, w.UINT], w.HWND),
|
|
63
|
+
'PeekMessageW': ([ctypes.POINTER(w.MSG), w.HWND, w.UINT, w.UINT, w.UINT], w.BOOL),
|
|
64
|
+
'TranslateMessage': ([ctypes.POINTER(w.MSG)], w.BOOL),
|
|
65
|
+
'DispatchMessageW': ([ctypes.POINTER(w.MSG)], LRESULT),
|
|
66
|
+
}
|
|
67
|
+
for name, (args, result) in specs.items():
|
|
68
|
+
function = getattr(self.user, name)
|
|
69
|
+
function.argtypes, function.restype = args, result
|
|
70
|
+
self.kernel.GetModuleHandleW.argtypes = [w.LPCWSTR]
|
|
71
|
+
self.kernel.GetModuleHandleW.restype = w.HMODULE
|
|
72
|
+
self._keyboard_callback = HOOKPROC(self._keyboard)
|
|
73
|
+
self._mouse_callback = HOOKPROC(self._mouse)
|
|
74
|
+
self._thread = threading.Thread(target=self._run, name='mathcraft-input', daemon=True)
|
|
75
|
+
|
|
76
|
+
def start(self):
|
|
77
|
+
self._thread.start()
|
|
78
|
+
if not self._ready.wait(3):
|
|
79
|
+
self.close()
|
|
80
|
+
raise RuntimeError('Input hook startup timed out')
|
|
81
|
+
if self._error:
|
|
82
|
+
raise self._error
|
|
83
|
+
|
|
84
|
+
def close(self):
|
|
85
|
+
self.enabled = False
|
|
86
|
+
self._stop.set()
|
|
87
|
+
if self._thread.is_alive():
|
|
88
|
+
self._thread.join(timeout=3)
|
|
89
|
+
if self._thread.is_alive():
|
|
90
|
+
raise RuntimeError('Input hook worker did not stop')
|
|
91
|
+
|
|
92
|
+
def disable(self, reason):
|
|
93
|
+
self.enabled = False
|
|
94
|
+
self.reason = reason
|
|
95
|
+
|
|
96
|
+
def _active(self):
|
|
97
|
+
return (self.enabled and time.monotonic()-self.heartbeat < 0.5
|
|
98
|
+
and self.user.GetForegroundWindow() == self.hwnd)
|
|
99
|
+
|
|
100
|
+
def _put(self, event):
|
|
101
|
+
try:
|
|
102
|
+
self.events.put_nowait(event)
|
|
103
|
+
return True
|
|
104
|
+
except queue.Full:
|
|
105
|
+
self.disable('Input queue overflow; interception disabled')
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def _next(self, code, message, address):
|
|
109
|
+
return self.user.CallNextHookEx(None, code, message, address)
|
|
110
|
+
|
|
111
|
+
def _down(self, key):
|
|
112
|
+
return bool(self.user.GetAsyncKeyState(key) & 0x8000)
|
|
113
|
+
|
|
114
|
+
def _keyboard(self, code, message, address):
|
|
115
|
+
try:
|
|
116
|
+
if code < 0:
|
|
117
|
+
return self._next(code, message, address)
|
|
118
|
+
data = ctypes.cast(address, ctypes.POINTER(KeyboardData)).contents
|
|
119
|
+
key = data.vk
|
|
120
|
+
down = message in (0x100, 0x104)
|
|
121
|
+
if down and key == 0x7B and self._down(0x11) and self._down(0x12):
|
|
122
|
+
self.disable('Ctrl+Alt+F12: interception disabled')
|
|
123
|
+
return self._next(code, message, address)
|
|
124
|
+
if not down and key in self._keys:
|
|
125
|
+
self._keys.discard(key)
|
|
126
|
+
return 1
|
|
127
|
+
if down and key in self._keys and not self._active():
|
|
128
|
+
self._keys.discard(key)
|
|
129
|
+
# Keep OS modifier state and shell shortcuts working normally.
|
|
130
|
+
supported = (key in (8, 9, 13, 27, 32, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46)
|
|
131
|
+
or 0x30 <= key <= 0x5A or 0x60 <= key <= 0x6F
|
|
132
|
+
or 0xBA <= key <= 0xE2)
|
|
133
|
+
if (not down or not supported or data.flags & 0x10 or not self._active()
|
|
134
|
+
or self._down(0x12) or self._down(0x5B) or self._down(0x5C)):
|
|
135
|
+
return self._next(code, message, address)
|
|
136
|
+
shift, control = self._down(0x10), self._down(0x11)
|
|
137
|
+
if control and key not in (65, 67, 86, 88, 89, 90):
|
|
138
|
+
# Let Tk handle shortcuts we do not implement rather than drop them.
|
|
139
|
+
return self._next(code, message, address)
|
|
140
|
+
state = (w.BYTE * 256)()
|
|
141
|
+
state[key] = 0x80
|
|
142
|
+
state[0x10] = 0x80 if shift else 0
|
|
143
|
+
state[0x11] = 0x80 if control else 0
|
|
144
|
+
state[0x14] = self.user.GetKeyState(0x14) & 1
|
|
145
|
+
thread = self.user.GetWindowThreadProcessId(self.hwnd, None)
|
|
146
|
+
layout = self.user.GetKeyboardLayout(thread)
|
|
147
|
+
buffer = ctypes.create_unicode_buffer(16)
|
|
148
|
+
count = self.user.ToUnicodeEx(key, data.scan, state, buffer, 16, 4, layout)
|
|
149
|
+
if count < 0:
|
|
150
|
+
self.disable('Dead-key layout: interception disabled')
|
|
151
|
+
return self._next(code, message, address)
|
|
152
|
+
text = buffer[:count] if count > 0 else ''
|
|
153
|
+
if self._put(('key', key, text, shift, control)):
|
|
154
|
+
self._keys.add(key)
|
|
155
|
+
return 1
|
|
156
|
+
except Exception:
|
|
157
|
+
self.disable('Keyboard hook error; interception disabled')
|
|
158
|
+
return self._next(code, message, address)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def pointer_position(self):
|
|
162
|
+
return self._position if self._anchor is not None else None
|
|
163
|
+
|
|
164
|
+
def _client_bounds(self):
|
|
165
|
+
origin, rect = w.POINT(), w.RECT()
|
|
166
|
+
if (not self.user.ClientToScreen(self.hwnd, ctypes.byref(origin))
|
|
167
|
+
or not self.user.GetClientRect(self.hwnd, ctypes.byref(rect))):
|
|
168
|
+
return None
|
|
169
|
+
return origin.x, origin.y, origin.x+rect.right, origin.y+rect.bottom
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _inside(bounds, x, y):
|
|
173
|
+
return bool(bounds and bounds[0] <= x < bounds[2] and bounds[1] <= y < bounds[3])
|
|
174
|
+
|
|
175
|
+
def _release_pointer(self, restore=True):
|
|
176
|
+
anchor, position = self._anchor, self._position
|
|
177
|
+
self._anchor = None
|
|
178
|
+
self._position = None
|
|
179
|
+
if anchor is not None and restore:
|
|
180
|
+
actual = w.POINT()
|
|
181
|
+
# Do not overwrite a cursor relocation made by another application.
|
|
182
|
+
if (self.user.GetCursorPos(ctypes.byref(actual))
|
|
183
|
+
and (actual.x, actual.y) == anchor):
|
|
184
|
+
if not self.user.SetCursorPos(*position):
|
|
185
|
+
self.disable('Cursor restoration failed; interception disabled')
|
|
186
|
+
|
|
187
|
+
def _sync_pointer(self):
|
|
188
|
+
if self._anchor is not None and not self._active():
|
|
189
|
+
self._release_pointer()
|
|
190
|
+
|
|
191
|
+
def _virtual_move(self, x, y):
|
|
192
|
+
bounds = self._client_bounds()
|
|
193
|
+
if self._anchor is None:
|
|
194
|
+
actual = w.POINT()
|
|
195
|
+
if (not self.user.GetCursorPos(ctypes.byref(actual))
|
|
196
|
+
or not self._inside(bounds, actual.x, actual.y)):
|
|
197
|
+
return False
|
|
198
|
+
hit = self.user.WindowFromPoint(actual)
|
|
199
|
+
if self.user.GetAncestor(hit, 2) not in (self.hwnd, self.cursor_hwnd):
|
|
200
|
+
return False
|
|
201
|
+
self._anchor = (actual.x, actual.y)
|
|
202
|
+
self._position = self._anchor
|
|
203
|
+
ax, ay = self._anchor
|
|
204
|
+
px, py = self._position
|
|
205
|
+
self._position = (px+x-ax, py+y-ay)
|
|
206
|
+
if not self._inside(bounds, *self._position):
|
|
207
|
+
self._release_pointer()
|
|
208
|
+
return True
|
|
209
|
+
if self._buttons:
|
|
210
|
+
if not self._put(('motion', tuple(self._buttons), *self._position)):
|
|
211
|
+
self._release_pointer()
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def _mouse(self, code, message, address):
|
|
215
|
+
# Windows can invoke hooks under a different DPI context than our pump.
|
|
216
|
+
change_dpi = getattr(self.user, 'SetThreadDpiAwarenessContext', None)
|
|
217
|
+
previous = change_dpi(ctypes.c_void_p(-4)) if change_dpi else None
|
|
218
|
+
if change_dpi and not previous:
|
|
219
|
+
self.disable('DPI context unavailable; interception disabled')
|
|
220
|
+
return self._next(code, message, address)
|
|
221
|
+
try:
|
|
222
|
+
return self._mouse_event(code, message, address)
|
|
223
|
+
finally:
|
|
224
|
+
if previous:
|
|
225
|
+
change_dpi(previous)
|
|
226
|
+
|
|
227
|
+
def _mouse_event(self, code, message, address):
|
|
228
|
+
try:
|
|
229
|
+
if code < 0:
|
|
230
|
+
return self._next(code, message, address)
|
|
231
|
+
data = ctypes.cast(address, ctypes.POINTER(MouseData)).contents
|
|
232
|
+
virtual = getattr(self, 'virtual_mouse', False)
|
|
233
|
+
if data.flags & 1:
|
|
234
|
+
if virtual and message == 0x200:
|
|
235
|
+
self._release_pointer(restore=False)
|
|
236
|
+
return self._next(code, message, address)
|
|
237
|
+
pairs = {0x201: (1, True), 0x202: (1, False),
|
|
238
|
+
0x204: (3, True), 0x205: (3, False),
|
|
239
|
+
0x207: (2, True), 0x208: (2, False)}
|
|
240
|
+
if not self._active():
|
|
241
|
+
if virtual:
|
|
242
|
+
self._release_pointer()
|
|
243
|
+
# Pair only releases for button downs we previously consumed.
|
|
244
|
+
if message in pairs:
|
|
245
|
+
button, down = pairs[message]
|
|
246
|
+
if not down and button in self._buttons:
|
|
247
|
+
self._buttons.discard(button)
|
|
248
|
+
return 1
|
|
249
|
+
return self._next(code, message, address)
|
|
250
|
+
x, y = data.point.x, data.point.y
|
|
251
|
+
if virtual and message == 0x200:
|
|
252
|
+
if self._virtual_move(x, y):
|
|
253
|
+
return 1
|
|
254
|
+
return self._next(code, message, address)
|
|
255
|
+
if virtual and self.pointer_position is not None:
|
|
256
|
+
x, y = self.pointer_position
|
|
257
|
+
if message in pairs:
|
|
258
|
+
button, down = pairs[message]
|
|
259
|
+
if not down and button in self._buttons:
|
|
260
|
+
self._buttons.discard(button)
|
|
261
|
+
self._put(('mouse', button, False, x, y, 0))
|
|
262
|
+
return 1
|
|
263
|
+
hit = self.user.WindowFromPoint(w.POINT(x, y))
|
|
264
|
+
point, rect = w.POINT(x, y), w.RECT()
|
|
265
|
+
if (self.user.GetAncestor(hit, 2) not in (self.hwnd, getattr(self, 'cursor_hwnd', None))
|
|
266
|
+
or not self.user.ScreenToClient(self.hwnd, ctypes.byref(point))
|
|
267
|
+
or not self.user.GetClientRect(self.hwnd, ctypes.byref(rect))
|
|
268
|
+
or not (0 <= point.x < rect.right and 0 <= point.y < rect.bottom)):
|
|
269
|
+
if virtual:
|
|
270
|
+
self._release_pointer()
|
|
271
|
+
return self._next(code, message, address)
|
|
272
|
+
if message in pairs:
|
|
273
|
+
button, down = pairs[message]
|
|
274
|
+
if self._put(('mouse', button, down, x, y, 0)):
|
|
275
|
+
if down:
|
|
276
|
+
self._buttons.add(button)
|
|
277
|
+
return 1
|
|
278
|
+
elif message == 0x200 and self._buttons:
|
|
279
|
+
self._put(('motion', tuple(self._buttons), x, y))
|
|
280
|
+
elif message == 0x20A:
|
|
281
|
+
delta = ctypes.c_short(data.data >> 16).value
|
|
282
|
+
if self._put(('mouse', 0, False, x, y, delta)):
|
|
283
|
+
return 1
|
|
284
|
+
except Exception:
|
|
285
|
+
self.disable('Mouse hook error; interception disabled')
|
|
286
|
+
if getattr(self, 'virtual_mouse', False):
|
|
287
|
+
self._release_pointer()
|
|
288
|
+
return self._next(code, message, address)
|
|
289
|
+
|
|
290
|
+
def _run(self):
|
|
291
|
+
handles = []
|
|
292
|
+
previous_dpi = None
|
|
293
|
+
try:
|
|
294
|
+
previous_dpi = self.user.SetThreadDpiAwarenessContext(ctypes.c_void_p(-4))
|
|
295
|
+
if not previous_dpi:
|
|
296
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
297
|
+
module = self.kernel.GetModuleHandleW(None)
|
|
298
|
+
for kind, callback in ((13, self._keyboard_callback), (14, self._mouse_callback)):
|
|
299
|
+
handle = self.user.SetWindowsHookExW(kind, callback, module, 0)
|
|
300
|
+
if not handle:
|
|
301
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
302
|
+
handles.append(handle)
|
|
303
|
+
self._ready.set()
|
|
304
|
+
message = w.MSG()
|
|
305
|
+
while not self._stop.is_set():
|
|
306
|
+
self._sync_pointer()
|
|
307
|
+
while self.user.PeekMessageW(ctypes.byref(message), None, 0, 0, 1):
|
|
308
|
+
if message.message == 0x12:
|
|
309
|
+
return
|
|
310
|
+
self.user.TranslateMessage(ctypes.byref(message))
|
|
311
|
+
self.user.DispatchMessageW(ctypes.byref(message))
|
|
312
|
+
self._stop.wait(0.005)
|
|
313
|
+
except Exception as exc:
|
|
314
|
+
self._error = exc
|
|
315
|
+
self.disable('Input hook worker stopped')
|
|
316
|
+
finally:
|
|
317
|
+
self.enabled = False
|
|
318
|
+
for handle in reversed(handles):
|
|
319
|
+
self.user.UnhookWindowsHookEx(handle)
|
|
320
|
+
try:
|
|
321
|
+
self._release_pointer()
|
|
322
|
+
finally:
|
|
323
|
+
if previous_dpi:
|
|
324
|
+
self.user.SetThreadDpiAwarenessContext(previous_dpi)
|
|
325
|
+
self._ready.set()
|
mathcraft/mathematics.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Standard-library mathematics plus small real-valued numerical utilities.
|
|
2
|
+
|
|
3
|
+
math/statistics exports retain their original Python semantics. Numerical
|
|
4
|
+
methods are approximations; their tolerances do not bound rounding error.
|
|
5
|
+
"""
|
|
6
|
+
import math as _math
|
|
7
|
+
import statistics as _statistics
|
|
8
|
+
|
|
9
|
+
# Reuse the installed standard library rather than vendoring copied sources.
|
|
10
|
+
__all__ = [name for name in dir(_math) if not name.startswith('_')]
|
|
11
|
+
for _name in __all__:
|
|
12
|
+
globals()[_name] = getattr(_math, _name)
|
|
13
|
+
for _name in ('mean', 'fmean', 'median', 'mode', 'multimode', 'variance',
|
|
14
|
+
'pvariance', 'stdev', 'pstdev', 'geometric_mean', 'harmonic_mean'):
|
|
15
|
+
globals()[_name] = getattr(_statistics, _name)
|
|
16
|
+
__all__.append(_name)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dot(a, b):
|
|
20
|
+
"""Dot product of equally sized real vectors (including empty vectors)."""
|
|
21
|
+
a, b = tuple(a), tuple(b)
|
|
22
|
+
if len(a) != len(b):
|
|
23
|
+
raise ValueError('Vector dimensions must match')
|
|
24
|
+
return _math.fsum(x * y for x, y in zip(a, b))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def norm(vector):
|
|
28
|
+
"""Euclidean norm with scaled accumulation from math.hypot."""
|
|
29
|
+
return _math.hypot(*vector)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cross(a, b):
|
|
33
|
+
"""Cross product of two three-dimensional vectors."""
|
|
34
|
+
a, b = tuple(a), tuple(b)
|
|
35
|
+
if len(a) != 3 or len(b) != 3:
|
|
36
|
+
raise ValueError('Cross product requires two 3D vectors')
|
|
37
|
+
return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2],
|
|
38
|
+
a[0]*b[1]-a[1]*b[0]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _matrix(value):
|
|
42
|
+
rows = [list(row) for row in value]
|
|
43
|
+
if not rows or not rows[0] or any(len(row) != len(rows[0]) for row in rows):
|
|
44
|
+
raise ValueError('Matrix must be nonempty and rectangular')
|
|
45
|
+
return rows
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def transpose(matrix):
|
|
49
|
+
"""Return a new transposed rectangular matrix."""
|
|
50
|
+
return [list(column) for column in zip(*_matrix(matrix))]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def matmul(a, b):
|
|
54
|
+
"""Multiply rectangular real matrices; inputs are never modified."""
|
|
55
|
+
a, b = _matrix(a), _matrix(b)
|
|
56
|
+
if len(a[0]) != len(b):
|
|
57
|
+
raise ValueError('Inner matrix dimensions must match')
|
|
58
|
+
columns = list(zip(*b))
|
|
59
|
+
return [[dot(row, column) for column in columns] for row in a]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _finite(value, name):
|
|
63
|
+
value = float(value)
|
|
64
|
+
if not _math.isfinite(value):
|
|
65
|
+
raise ValueError(name + ' must be finite')
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _count(value, name):
|
|
70
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
71
|
+
raise ValueError(name + ' must be a positive integer')
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def derivative(function, x, h=1e-5):
|
|
76
|
+
"""Central finite difference with a positive absolute step."""
|
|
77
|
+
x, h = _finite(x, 'x'), _finite(h, 'h')
|
|
78
|
+
if h <= 0 or x+h == x or x-h == x:
|
|
79
|
+
raise ValueError('h must be positive and resolvable at x')
|
|
80
|
+
left, right = _finite(x-h, 'left sample'), _finite(x+h, 'right sample')
|
|
81
|
+
result = (_finite(function(right), 'f(x+h)') -
|
|
82
|
+
_finite(function(left), 'f(x-h)')) / (2*h)
|
|
83
|
+
return _finite(result, 'derivative')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def integrate(function, a, b, steps=1000):
|
|
87
|
+
"""Composite trapezoidal rule with exactly steps subintervals."""
|
|
88
|
+
a, b = _finite(a, 'a'), _finite(b, 'b')
|
|
89
|
+
steps = _count(steps, 'steps')
|
|
90
|
+
if a == b:
|
|
91
|
+
return 0.0
|
|
92
|
+
width = _finite((b-a)/steps, 'step width')
|
|
93
|
+
if width == 0 or a+width == a:
|
|
94
|
+
raise ValueError('Integration step is too small')
|
|
95
|
+
ends = _finite(function(a), 'f(a)')/2 + _finite(function(b), 'f(b)')/2
|
|
96
|
+
interior = _math.fsum(_finite(function(a+i*width), 'integrand')
|
|
97
|
+
for i in range(1, steps))
|
|
98
|
+
return _finite(width*(ends+interior), 'integral')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def bisect(function, a, b, tolerance=1e-10, max_iterations=1000):
|
|
102
|
+
"""Find a bracketed root of a continuous real function.
|
|
103
|
+
|
|
104
|
+
Return when bracket half-width <= tolerance or an exact zero is sampled.
|
|
105
|
+
Raise RuntimeError on iteration exhaustion or insufficient float precision.
|
|
106
|
+
"""
|
|
107
|
+
a, b = _finite(a, 'a'), _finite(b, 'b')
|
|
108
|
+
tolerance = _finite(tolerance, 'tolerance')
|
|
109
|
+
max_iterations = _count(max_iterations, 'max_iterations')
|
|
110
|
+
if a >= b or tolerance <= 0:
|
|
111
|
+
raise ValueError('Require a < b and tolerance > 0')
|
|
112
|
+
fa, fb = _finite(function(a), 'f(a)'), _finite(function(b), 'f(b)')
|
|
113
|
+
if fa == 0:
|
|
114
|
+
return a
|
|
115
|
+
if fb == 0:
|
|
116
|
+
return b
|
|
117
|
+
if (fa > 0) == (fb > 0):
|
|
118
|
+
raise ValueError('Endpoints must bracket a root')
|
|
119
|
+
for _ in range(max_iterations):
|
|
120
|
+
midpoint = a/2 + b/2
|
|
121
|
+
fm = _finite(function(midpoint), 'f(midpoint)')
|
|
122
|
+
if fm == 0 or b/2-a/2 <= tolerance:
|
|
123
|
+
return midpoint
|
|
124
|
+
if midpoint == a or midpoint == b:
|
|
125
|
+
raise RuntimeError('Floating-point precision exhausted')
|
|
126
|
+
if (fa > 0) == (fm > 0):
|
|
127
|
+
a, fa = midpoint, fm
|
|
128
|
+
else:
|
|
129
|
+
b = midpoint
|
|
130
|
+
raise RuntimeError('Bisection did not converge within max_iterations')
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ += ['dot', 'norm', 'cross', 'transpose', 'matmul', 'derivative',
|
|
134
|
+
'integrate', 'bisect']
|