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/_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)]