mumpy-toolkit 0.2.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.
- mumpy/__init__.py +128 -0
- mumpy/_core.py +218 -0
- mumpy/_math.py +283 -0
- mumpy/_parallel.py +142 -0
- mumpy/db.py +289 -0
- mumpy/fft.py +132 -0
- mumpy/frame.py +428 -0
- mumpy/io.py +196 -0
- mumpy/linalg.py +62 -0
- mumpy/metrics.py +187 -0
- mumpy/ml.py +443 -0
- mumpy/nn.py +278 -0
- mumpy/preprocessing.py +259 -0
- mumpy/random.py +99 -0
- mumpy/stats.py +122 -0
- mumpy/utils.py +105 -0
- mumpy/viz.py +81 -0
- mumpy_toolkit-0.2.0.dist-info/METADATA +200 -0
- mumpy_toolkit-0.2.0.dist-info/RECORD +22 -0
- mumpy_toolkit-0.2.0.dist-info/WHEEL +5 -0
- mumpy_toolkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- mumpy_toolkit-0.2.0.dist-info/top_level.txt +1 -0
mumpy/_parallel.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""mumpy parallel engine: chunked multithreaded execution for memory-bound ops.
|
|
2
|
+
|
|
3
|
+
Why faster than plain numpy for many ops:
|
|
4
|
+
- numpy element-wise ufuncs are single-threaded. We split large arrays
|
|
5
|
+
into chunks and run the ufunc in a ThreadPool (GIL is released in C loops).
|
|
6
|
+
- reductions are done per-chunk in parallel then combined.
|
|
7
|
+
- fft uses scipy.fft with all workers when available.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import numpy as np
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
|
|
15
|
+
_ncpu = os.cpu_count() or 4
|
|
16
|
+
MAX_WORKERS = max(1, min(32, _ncpu))
|
|
17
|
+
|
|
18
|
+
# Only parallelize above this many elements (avoid thread overhead).
|
|
19
|
+
PARALLEL_THRESHOLD = 50_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_workers(n_elements: int | None = None) -> int:
|
|
23
|
+
if n_elements is not None and n_elements < PARALLEL_THRESHOLD:
|
|
24
|
+
return 1
|
|
25
|
+
return MAX_WORKERS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _split_first_axis(n: int, workers: int) -> list[tuple[int, int]]:
|
|
29
|
+
chunk = (n + workers - 1) // workers
|
|
30
|
+
return [(i, min(i + chunk, n)) for i in range(0, n, chunk)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parallel_ewise(func, *arrays, out=None):
|
|
34
|
+
"""Apply element-wise C func over chunks in parallel.
|
|
35
|
+
|
|
36
|
+
func: callable (*views, out_view) executed on numpy views.
|
|
37
|
+
arrays: broadcastable arrays (broadcasted first, then chunked on flat view).
|
|
38
|
+
"""
|
|
39
|
+
# strip subclasses: work on plain ndarrays to avoid __array_ufunc__ recursion
|
|
40
|
+
arrays = [np.asanyarray(a).view(np.ndarray) for a in arrays]
|
|
41
|
+
if out is not None:
|
|
42
|
+
out = np.asanyarray(out).view(np.ndarray)
|
|
43
|
+
# fast path: all same shape & contiguous -> zero-copy ravel views
|
|
44
|
+
try:
|
|
45
|
+
shape0 = arrays[0].shape
|
|
46
|
+
if all(a.shape == shape0 for a in arrays):
|
|
47
|
+
size = arrays[0].size
|
|
48
|
+
workers = get_workers(size)
|
|
49
|
+
if workers <= 1 or size < PARALLEL_THRESHOLD:
|
|
50
|
+
if out is None:
|
|
51
|
+
res = np.empty(shape0, dtype=np.result_type(*arrays))
|
|
52
|
+
func(*arrays, out=res)
|
|
53
|
+
return res
|
|
54
|
+
func(*arrays, out=out)
|
|
55
|
+
return out
|
|
56
|
+
flats = [a.reshape(-1) if a.flags["C_CONTIGUOUS"] or a.flags["F_CONTIGUOUS"]
|
|
57
|
+
else np.ascontiguousarray(a).reshape(-1) for a in arrays]
|
|
58
|
+
if out is None:
|
|
59
|
+
res_flat = np.empty(size, dtype=np.result_type(*arrays))
|
|
60
|
+
else:
|
|
61
|
+
res_flat = np.asanyarray(out).reshape(-1)
|
|
62
|
+
ranges = _split_first_axis(size, workers)
|
|
63
|
+
|
|
64
|
+
def _job(r):
|
|
65
|
+
s, e = r
|
|
66
|
+
views = [f[s:e] for f in flats]
|
|
67
|
+
func(*views, out=res_flat[s:e])
|
|
68
|
+
|
|
69
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
70
|
+
list(ex.map(_job, ranges))
|
|
71
|
+
if out is None:
|
|
72
|
+
return res_flat.reshape(shape0)
|
|
73
|
+
return out
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
try:
|
|
77
|
+
bcast = np.broadcast_arrays(*arrays)
|
|
78
|
+
except ValueError:
|
|
79
|
+
# fall back to plain call (e.g. matmul-like)
|
|
80
|
+
return func(*arrays, out=out) if out is not None else func(*arrays)
|
|
81
|
+
shape = bcast[0].shape
|
|
82
|
+
size = bcast[0].size
|
|
83
|
+
workers = get_workers(size)
|
|
84
|
+
if workers <= 1 or size < PARALLEL_THRESHOLD:
|
|
85
|
+
outs = [np.ascontiguousarray(b) for b in bcast]
|
|
86
|
+
if out is None:
|
|
87
|
+
res = np.empty(shape, dtype=np.result_type(*arrays))
|
|
88
|
+
func(*outs, out=res.reshape(-1) if res.ndim else res)
|
|
89
|
+
return res
|
|
90
|
+
func(*outs, out=np.asanyarray(out).reshape(-1))
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
# flatten broadcasted (copies only if needed for non-contiguous)
|
|
94
|
+
flats = [np.ascontiguousarray(b).reshape(-1) for b in bcast]
|
|
95
|
+
if out is None:
|
|
96
|
+
res_flat = np.empty(size, dtype=np.result_type(*arrays))
|
|
97
|
+
else:
|
|
98
|
+
res_flat = np.asanyarray(out).reshape(-1)
|
|
99
|
+
ranges = _split_first_axis(size, workers)
|
|
100
|
+
|
|
101
|
+
def _job(r):
|
|
102
|
+
s, e = r
|
|
103
|
+
views = [f[s:e] for f in flats]
|
|
104
|
+
func(*views, out=res_flat[s:e])
|
|
105
|
+
|
|
106
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
107
|
+
list(ex.map(_job, ranges))
|
|
108
|
+
if out is None:
|
|
109
|
+
return res_flat.reshape(shape)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parallel_reduce(op, a, axis=None, dtype=None, keepdims=False, op_name="sum"):
|
|
114
|
+
"""Parallel reduction over the flattened array when axis is None,
|
|
115
|
+
else chunked along the given axis."""
|
|
116
|
+
a = np.asanyarray(a)
|
|
117
|
+
workers = get_workers(a.size)
|
|
118
|
+
_no_dtype = op_name in ("min", "max", "any", "all")
|
|
119
|
+
if workers <= 1 or a.size < PARALLEL_THRESHOLD or axis is not None:
|
|
120
|
+
if _no_dtype:
|
|
121
|
+
return op(a, axis=axis, keepdims=keepdims)
|
|
122
|
+
return op(a, axis=axis, dtype=dtype, keepdims=keepdims)
|
|
123
|
+
# flat parallel reduce: partials then combine
|
|
124
|
+
flats = np.ascontiguousarray(a).reshape(-1)
|
|
125
|
+
ranges = _split_first_axis(flats.size, workers)
|
|
126
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
127
|
+
if _no_dtype:
|
|
128
|
+
partials = list(ex.map(lambda r: op(flats[r[0]:r[1]]), ranges))
|
|
129
|
+
else:
|
|
130
|
+
partials = list(ex.map(lambda r: op(flats[r[0]:r[1]], dtype=dtype), ranges))
|
|
131
|
+
partials = np.asanyarray(partials)
|
|
132
|
+
if op_name in ("min", "max", "any", "all"):
|
|
133
|
+
res = op(partials)
|
|
134
|
+
elif op_name in ("sum", "prod"):
|
|
135
|
+
res = op(partials, dtype=dtype)
|
|
136
|
+
elif op_name == "mean":
|
|
137
|
+
res = np.mean(partials, dtype=dtype) if False else np.sum(partials, dtype=dtype) / flats.size
|
|
138
|
+
else:
|
|
139
|
+
res = op(partials)
|
|
140
|
+
if keepdims:
|
|
141
|
+
res = np.asanyarray(res).reshape((1,) * a.ndim)
|
|
142
|
+
return res
|
mumpy/db.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""mumpy.db: unified database layer for analysts & developers.
|
|
2
|
+
|
|
3
|
+
Zero-dependency core: sqlite3 (stdlib) — full CRUD, bulk insert from numpy,
|
|
4
|
+
read to numpy / dict / pandas (optional).
|
|
5
|
+
Optional backends (auto-detected): sqlalchemy (postgres/mysql/sqlite),
|
|
6
|
+
duckdb (analytical OLAP, parquet direct).
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
import mumpy as cp
|
|
10
|
+
db = cp.db.connect("data.db")
|
|
11
|
+
db.create_table("users", {"id": "INTEGER PRIMARY KEY", "name": "TEXT", "age": "INTEGER"})
|
|
12
|
+
db.insert_many("users", [{"name": "salim", "age": 30}])
|
|
13
|
+
arr = db.read_numpy("SELECT age FROM users")
|
|
14
|
+
df = db.read_pandas("SELECT * FROM users")
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sqlite3
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
__all__ = ["Database", "connect", "has_sqlalchemy", "has_duckdb", "has_pandas"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def has_sqlalchemy() -> bool:
|
|
25
|
+
try:
|
|
26
|
+
import sqlalchemy # noqa: F401
|
|
27
|
+
return True
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def has_duckdb() -> bool:
|
|
33
|
+
try:
|
|
34
|
+
import duckdb # noqa: F401
|
|
35
|
+
return True
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def has_pandas() -> bool:
|
|
41
|
+
try:
|
|
42
|
+
import pandas # noqa: F401
|
|
43
|
+
return True
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Database:
|
|
49
|
+
"""Thin, fast wrapper over sqlite3 (+ optional sqlalchemy/duckdb engines)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, path=":memory:", engine="sqlite", echo=False, **kw):
|
|
52
|
+
self.path = path
|
|
53
|
+
self.engine = engine
|
|
54
|
+
self.echo = echo
|
|
55
|
+
self._sa_engine = None
|
|
56
|
+
self._duck = None
|
|
57
|
+
if engine == "sqlite":
|
|
58
|
+
self.conn = sqlite3.connect(path, **{k: v for k, v in kw.items()
|
|
59
|
+
if k in ("timeout", "detect_types",
|
|
60
|
+
"isolation_level", "check_same_thread")})
|
|
61
|
+
self.conn.row_factory = sqlite3.Row
|
|
62
|
+
elif engine == "sqlalchemy":
|
|
63
|
+
if not has_sqlalchemy():
|
|
64
|
+
raise ImportError("pip install sqlalchemy")
|
|
65
|
+
from sqlalchemy import create_engine
|
|
66
|
+
url = kw.pop("url", f"sqlite:///{path}" if path != ":memory:" else "sqlite://")
|
|
67
|
+
self._sa_engine = create_engine(url, echo=echo, **kw)
|
|
68
|
+
self.conn = self._sa_engine.connect()
|
|
69
|
+
elif engine == "duckdb":
|
|
70
|
+
if not has_duckdb():
|
|
71
|
+
raise ImportError("pip install duckdb")
|
|
72
|
+
import duckdb
|
|
73
|
+
self._duck = duckdb.connect(path if path != ":memory:" else ":memory:", **kw)
|
|
74
|
+
self.conn = None
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"unknown engine: {engine}")
|
|
77
|
+
|
|
78
|
+
# ---------- core execution ----------
|
|
79
|
+
def execute(self, sql, params=None):
|
|
80
|
+
if self.echo:
|
|
81
|
+
print(sql, params or "")
|
|
82
|
+
if self.engine == "duckdb":
|
|
83
|
+
return self._duck.execute(sql, params or [])
|
|
84
|
+
if self.engine == "sqlalchemy":
|
|
85
|
+
from sqlalchemy import text
|
|
86
|
+
if params:
|
|
87
|
+
return self.conn.execute(text(sql), params)
|
|
88
|
+
return self.conn.execute(text(sql))
|
|
89
|
+
cur = self.conn.cursor()
|
|
90
|
+
cur.execute(sql, params or [])
|
|
91
|
+
self.conn.commit()
|
|
92
|
+
return cur
|
|
93
|
+
|
|
94
|
+
def executemany(self, sql, seq):
|
|
95
|
+
if self.engine == "duckdb":
|
|
96
|
+
self._duck.executemany(sql, seq)
|
|
97
|
+
return None
|
|
98
|
+
if self.engine == "sqlalchemy":
|
|
99
|
+
from sqlalchemy import text
|
|
100
|
+
self.conn.execute(text(sql), seq)
|
|
101
|
+
try:
|
|
102
|
+
self.conn.commit()
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return None
|
|
106
|
+
cur = self.conn.cursor()
|
|
107
|
+
cur.executemany(sql, seq)
|
|
108
|
+
self.conn.commit()
|
|
109
|
+
return cur
|
|
110
|
+
|
|
111
|
+
# ---------- DDL helpers ----------
|
|
112
|
+
def create_table(self, name, schema: dict, if_not_exists=True):
|
|
113
|
+
cols = ", ".join(f'"{k}" {v}' for k, v in schema.items())
|
|
114
|
+
ine = "IF NOT EXISTS " if if_not_exists else ""
|
|
115
|
+
self.execute(f'CREATE TABLE {ine}"{name}" ({cols})')
|
|
116
|
+
|
|
117
|
+
def drop_table(self, name, if_exists=True):
|
|
118
|
+
ie = "IF EXISTS " if if_exists else ""
|
|
119
|
+
self.execute(f'DROP TABLE {ie}"{name}"')
|
|
120
|
+
|
|
121
|
+
def tables(self):
|
|
122
|
+
if self.engine == "duckdb":
|
|
123
|
+
rows = self._duck.execute(
|
|
124
|
+
"SELECT table_name FROM information_schema.tables").fetchall()
|
|
125
|
+
return [r[0] for r in rows]
|
|
126
|
+
cur = self.execute(
|
|
127
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
128
|
+
try:
|
|
129
|
+
return [r[0] for r in cur.fetchall()]
|
|
130
|
+
except Exception:
|
|
131
|
+
return [r[0] for r in cur]
|
|
132
|
+
|
|
133
|
+
# ---------- writes ----------
|
|
134
|
+
def insert_many(self, table, rows):
|
|
135
|
+
"""rows: list[dict] | list[tuple] | 2D ndarray (+ columns kw)."""
|
|
136
|
+
if isinstance(rows, np.ndarray):
|
|
137
|
+
raise ValueError("use write_numpy(table, arr, columns=...) for ndarrays")
|
|
138
|
+
if not rows:
|
|
139
|
+
return 0
|
|
140
|
+
if isinstance(rows[0], dict):
|
|
141
|
+
cols = list(rows[0].keys())
|
|
142
|
+
placeholders = ", ".join(["?"] * len(cols))
|
|
143
|
+
sql = f'INSERT INTO "{table}" ({", ".join(chr(34)+c+chr(34) for c in cols)}) VALUES ({placeholders})'
|
|
144
|
+
seq = [tuple(r[c] for c in cols) for r in rows]
|
|
145
|
+
else:
|
|
146
|
+
n = len(rows[0])
|
|
147
|
+
placeholders = ", ".join(["?"] * n)
|
|
148
|
+
sql = f'INSERT INTO "{table}" VALUES ({placeholders})'
|
|
149
|
+
seq = [tuple(r) for r in rows]
|
|
150
|
+
self.executemany(sql, seq)
|
|
151
|
+
return len(seq)
|
|
152
|
+
|
|
153
|
+
def write_numpy(self, table, arr, columns=None, dtypes=None):
|
|
154
|
+
"""Bulk-write 2D numpy array as a table (creates table if needed)."""
|
|
155
|
+
arr = np.asanyarray(arr)
|
|
156
|
+
if arr.ndim == 1:
|
|
157
|
+
arr = arr.reshape(-1, 1)
|
|
158
|
+
ncols = arr.shape[1]
|
|
159
|
+
if columns is None:
|
|
160
|
+
columns = [f"c{i}" for i in range(ncols)]
|
|
161
|
+
if dtypes is None:
|
|
162
|
+
# infer SQL types from numpy dtype
|
|
163
|
+
def _t(dt):
|
|
164
|
+
dt = np.dtype(dt)
|
|
165
|
+
if np.issubdtype(dt, np.integer):
|
|
166
|
+
return "INTEGER"
|
|
167
|
+
if np.issubdtype(dt, np.floating):
|
|
168
|
+
return "REAL"
|
|
169
|
+
return "TEXT"
|
|
170
|
+
if arr.ndim == 2:
|
|
171
|
+
dtypes = [_t(arr.dtype)] * ncols
|
|
172
|
+
else:
|
|
173
|
+
dtypes = ["REAL"] * ncols
|
|
174
|
+
schema = {c: t for c, t in zip(columns, dtypes)}
|
|
175
|
+
self.create_table(table, schema)
|
|
176
|
+
placeholders = ", ".join(["?"] * ncols)
|
|
177
|
+
sql = f'INSERT INTO "{table}" ({", ".join(chr(34)+c+chr(34) for c in columns)}) VALUES ({placeholders})'
|
|
178
|
+
self.executemany(sql, [tuple(r) for r in arr.tolist()])
|
|
179
|
+
return arr.shape[0]
|
|
180
|
+
|
|
181
|
+
def from_csv(self, table, csv_path, delimiter=",", header=True):
|
|
182
|
+
from .io import load_csv
|
|
183
|
+
data, names = load_csv(csv_path, delimiter=delimiter, header=header,
|
|
184
|
+
dtype=str)
|
|
185
|
+
if names is None:
|
|
186
|
+
names = [f"c{i}" for i in range(data.shape[1])] if data.size else []
|
|
187
|
+
# try numeric conversion per column
|
|
188
|
+
cols = {}
|
|
189
|
+
for j, name in enumerate(names):
|
|
190
|
+
col = data[:, j] if data.size else np.array([])
|
|
191
|
+
try:
|
|
192
|
+
cols[name] = col.astype(float)
|
|
193
|
+
except Exception:
|
|
194
|
+
cols[name] = col
|
|
195
|
+
import numpy as _np
|
|
196
|
+
n = data.shape[0] if data.size else 0
|
|
197
|
+
schema = {}
|
|
198
|
+
for name in names:
|
|
199
|
+
v = cols[name]
|
|
200
|
+
try:
|
|
201
|
+
v.astype(float)
|
|
202
|
+
schema[name] = "REAL"
|
|
203
|
+
except Exception:
|
|
204
|
+
schema[name] = "TEXT"
|
|
205
|
+
self.create_table(table, schema)
|
|
206
|
+
if n:
|
|
207
|
+
placeholders = ", ".join(["?"] * len(names))
|
|
208
|
+
sql = f'INSERT INTO "{table}" VALUES ({placeholders})'
|
|
209
|
+
self.executemany(sql, [tuple(data[i, j] for j in range(len(names)))
|
|
210
|
+
for i in range(n)])
|
|
211
|
+
return n
|
|
212
|
+
|
|
213
|
+
# ---------- reads ----------
|
|
214
|
+
def query(self, sql, params=None):
|
|
215
|
+
"""Return list[dict]."""
|
|
216
|
+
if self.engine == "duckdb":
|
|
217
|
+
cur = self._duck.execute(sql, params or [])
|
|
218
|
+
names = [d[0] for d in cur.description]
|
|
219
|
+
return [dict(zip(names, r)) for r in cur.fetchall()]
|
|
220
|
+
if self.engine == "sqlalchemy":
|
|
221
|
+
from sqlalchemy import text
|
|
222
|
+
res = self.conn.execute(text(sql), params or {})
|
|
223
|
+
cols = list(res.keys())
|
|
224
|
+
return [dict(zip(cols, r)) for r in res.fetchall()]
|
|
225
|
+
cur = self.conn.cursor()
|
|
226
|
+
cur.execute(sql, params or [])
|
|
227
|
+
rows = cur.fetchall()
|
|
228
|
+
return [dict(r) for r in rows]
|
|
229
|
+
|
|
230
|
+
def read_numpy(self, sql, params=None):
|
|
231
|
+
rows = self.query(sql, params)
|
|
232
|
+
if not rows:
|
|
233
|
+
return np.empty((0, 0))
|
|
234
|
+
keys = list(rows[0].keys())
|
|
235
|
+
try:
|
|
236
|
+
arr = np.array([[r[k] for k in keys] for r in rows], dtype=float)
|
|
237
|
+
except Exception:
|
|
238
|
+
arr = np.array([[r[k] for k in keys] for r in rows], dtype=object)
|
|
239
|
+
try:
|
|
240
|
+
from ._core import _wrap as _w
|
|
241
|
+
return _w(arr)
|
|
242
|
+
except Exception:
|
|
243
|
+
return arr
|
|
244
|
+
|
|
245
|
+
def read_pandas(self, sql, params=None):
|
|
246
|
+
if not has_pandas():
|
|
247
|
+
raise ImportError("pip install pandas")
|
|
248
|
+
import pandas as pd
|
|
249
|
+
if self.engine == "duckdb":
|
|
250
|
+
return self._duck.execute(sql).df()
|
|
251
|
+
if self.engine == "sqlalchemy":
|
|
252
|
+
return pd.read_sql(sql, self._sa_engine, params=params)
|
|
253
|
+
return pd.read_sql_query(sql, self.conn, params=params)
|
|
254
|
+
|
|
255
|
+
def to_parquet(self, sql, path, params=None):
|
|
256
|
+
rows = self.query(sql, params)
|
|
257
|
+
if not has_pandas():
|
|
258
|
+
raise ImportError("pip install pandas pyarrow")
|
|
259
|
+
import pandas as pd
|
|
260
|
+
pd.DataFrame(rows).to_parquet(path, index=False)
|
|
261
|
+
return path
|
|
262
|
+
|
|
263
|
+
# ---------- maintenance ----------
|
|
264
|
+
def vacuum(self):
|
|
265
|
+
try:
|
|
266
|
+
self.execute("VACUUM")
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
def close(self):
|
|
271
|
+
try:
|
|
272
|
+
if self.engine == "duckdb":
|
|
273
|
+
self._duck.close()
|
|
274
|
+
elif self.engine == "sqlalchemy":
|
|
275
|
+
self.conn.close()
|
|
276
|
+
else:
|
|
277
|
+
self.conn.close()
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
def __enter__(self):
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def __exit__(self, *a):
|
|
285
|
+
self.close()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def connect(path=":memory:", engine="sqlite", **kw):
|
|
289
|
+
return Database(path, engine=engine, **kw)
|
mumpy/fft.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""mumpy.fft: numpy.fft-compatible but uses scipy.fft workers when available."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import scipy.fft as _sf
|
|
6
|
+
_HAS_SCIPY = True
|
|
7
|
+
except Exception:
|
|
8
|
+
_sf = None
|
|
9
|
+
_HAS_SCIPY = False
|
|
10
|
+
|
|
11
|
+
import numpy as _np
|
|
12
|
+
import numpy.fft as _nf
|
|
13
|
+
from numpy.fft import fftfreq, rfftfreq, fftshift, ifftshift # noqa: F401
|
|
14
|
+
|
|
15
|
+
__all__ = ["fft", "ifft", "rfft", "irfft", "fft2", "ifft2", "fftn", "ifftn",
|
|
16
|
+
"rfftn", "irfftn", "hfft", "ihfft",
|
|
17
|
+
"fftfreq", "rfftfreq", "fftshift", "ifftshift",
|
|
18
|
+
"next_fast_len", "convolve", "has_scipy"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_scipy() -> bool:
|
|
22
|
+
return _HAS_SCIPY
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def next_fast_len(n):
|
|
26
|
+
try:
|
|
27
|
+
if _HAS_SCIPY:
|
|
28
|
+
return _sf.next_fast_len(n)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
# fallback: next pow2-ish 5-smooth
|
|
32
|
+
m = 1
|
|
33
|
+
while m < n:
|
|
34
|
+
m *= 2
|
|
35
|
+
return m
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _call(name, a, **kw):
|
|
39
|
+
if _HAS_SCIPY:
|
|
40
|
+
kw.setdefault("workers", -1)
|
|
41
|
+
kw.setdefault("overwrite_x", False)
|
|
42
|
+
try:
|
|
43
|
+
return getattr(_sf, name)(a, **{k: v for k, v in kw.items()
|
|
44
|
+
if k in ("n", "axis", "norm", "workers",
|
|
45
|
+
"overwrite_x", "s", "axes", "shape")})
|
|
46
|
+
except TypeError:
|
|
47
|
+
pass
|
|
48
|
+
return getattr(_nf, name)(a, **{k: v for k, v in kw.items()
|
|
49
|
+
if k in ("n", "axis", "norm", "s", "axes", "shape")})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fft(a, n=None, axis=-1, norm=None, workers=-1, overwrite_x=False):
|
|
53
|
+
return _call("fft", a, n=n, axis=axis, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ifft(a, n=None, axis=-1, norm=None, workers=-1, overwrite_x=False):
|
|
57
|
+
return _call("ifft", a, n=n, axis=axis, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def rfft(a, n=None, axis=-1, norm=None, workers=-1, overwrite_x=False):
|
|
61
|
+
return _call("rfft", a, n=n, axis=axis, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def irfft(a, n=None, axis=-1, norm=None, workers=-1, overwrite_x=False):
|
|
65
|
+
return _call("irfft", a, n=n, axis=axis, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def fft2(a, s=None, axes=(-2, -1), norm=None, workers=-1, overwrite_x=False):
|
|
69
|
+
return _call("fft2", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ifft2(a, s=None, axes=(-2, -1), norm=None, workers=-1, overwrite_x=False):
|
|
73
|
+
return _call("ifft2", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fftn(a, s=None, axes=None, norm=None, workers=-1, overwrite_x=False):
|
|
77
|
+
return _call("fftn", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ifftn(a, s=None, axes=None, norm=None, workers=-1, overwrite_x=False):
|
|
81
|
+
return _call("ifftn", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def rfftn(a, s=None, axes=None, norm=None, workers=-1, overwrite_x=False):
|
|
85
|
+
try:
|
|
86
|
+
return _call("rfftn", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
87
|
+
except AttributeError:
|
|
88
|
+
return _nf.rfftn(a, s=s, axes=axes, norm=norm)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def irfftn(a, s=None, axes=None, norm=None, workers=-1, overwrite_x=False):
|
|
92
|
+
try:
|
|
93
|
+
return _call("irfftn", a, s=s, axes=axes, norm=norm, workers=workers, overwrite_x=overwrite_x)
|
|
94
|
+
except AttributeError:
|
|
95
|
+
return _nf.irfftn(a, s=s, axes=axes, norm=norm)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def hfft(a, n=None, axis=-1, norm=None):
|
|
99
|
+
try:
|
|
100
|
+
if _HAS_SCIPY:
|
|
101
|
+
return _sf.hfft(a, n=n, axis=axis, norm=norm)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
return _nf.hfft(a, n=n, axis=axis, norm=norm)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def ihfft(a, n=None, axis=-1, norm=None):
|
|
108
|
+
try:
|
|
109
|
+
if _HAS_SCIPY:
|
|
110
|
+
return _sf.ihfft(a, n=n, axis=axis, norm=norm)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
return _nf.ihfft(a, n=n, axis=axis, norm=norm)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def convolve(a, b, mode="full"):
|
|
117
|
+
"""FFT-based convolution (fast for large signals)."""
|
|
118
|
+
a = _np.asanyarray(a)
|
|
119
|
+
b = _np.asanyarray(b)
|
|
120
|
+
n = a.size + b.size - 1
|
|
121
|
+
N = next_fast_len(n)
|
|
122
|
+
FA = fft(a, n=N)
|
|
123
|
+
FB = fft(b, n=N)
|
|
124
|
+
full = _np.real(ifft(FA * FB))[:n]
|
|
125
|
+
if mode == "full":
|
|
126
|
+
return full
|
|
127
|
+
if mode == "same":
|
|
128
|
+
start = (n - max(a.size, b.size)) // 2
|
|
129
|
+
return full[start:start + max(a.size, b.size)]
|
|
130
|
+
# valid
|
|
131
|
+
start = b.size - 1
|
|
132
|
+
return full[start:start + (a.size - b.size + 1)]
|