Skip to content

Commit 49dd7b0

Browse files
OMC reaches into Python: pyo3 embed + np/pd integration written in OMC
The hard half of integration: OMC programs can now drive numpy, pandas, requests, sqlite, anything pip-installable. The integration LIBRARIES are written in OMC itself — not Rust. py_* primitives are the only Rust-side surface; everything else is OMC. What landed: * `python-embed` feature on omnimcode-core (off by default — building doesn't require libpython unless you want Python integration). Bumped to pyo3 0.23 + PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 for Python 3.14 support. * src/python_embed.rs: 8 builtins covering the whole py surface: py_import / py_call / py_get / py_call_fn / py_eval / py_exec / py_repr / py_clear_registry. Bidirectional Value ↔ PyObject conversion: scalars / lists / tuples / dicts / numpy ndarrays convert automatically; everything else becomes an opaque handle. Handle IDs start at 10^15 to never collide with regular OMC ints (caught by a passing-int-array bug in the smoke test). * maybe_register_python() in main.rs: gated on OMC_PYTHON=1 env var AND the python-embed feature flag. Two-gate so the same binary serves users who don't want the Python runtime cost. Bug fixed along the way: * Aliased imports (`import "lib" as alias`) were renaming each fn to `alias.fn` but NOT rewriting intra-module calls. So a helper pattern like `fn _np() {...}` called from `fn array(items) { return py_call(_np(), ...)` broke under aliasing — `_np` looked up unprefixed but only `pd._np` existed in the function table. Added rewrite_module_calls() AST walk on import: every Call expression whose name is in the module's defined-set gets the alias prefix. Required for the np.omc / pd.omc helpers to work. OMC integration libraries (written in OMC): * examples/lib/np.omc — numpy bridge. 30 lines. Exposes array / zeros / ones / arange / linspace / mean / median / std / dot / sort / argsort / percentile / quantile / corrcoef and pi/e/inf constants. Each fn is one or two lines on top of py_call. * examples/lib/pd.omc — pandas bridge. 60 lines. Exposes read_csv / read_json / read_parquet / read_excel / read_table loaders, plus shape / columns / col / head / tail / select_cols / group_by / agg_mean / agg_count / describe / to_dict. Demo: * examples/datascience/movielens_harmonic.omc — the real thing. Pipeline: pd.read_csv (10k rows in 276ms) → numpy stats on ratings → harmonic_partition (OMC-distinctive — 6 attractor buckets in 1ms) → numpy mean/std per bucket → pandas group_by + numpy argsort for top-10 movies. End-to-end real data, real libraries, real OMC. Strategic positioning: integration scripts get written in OMC. Users fork np.omc / pd.omc to add wrapping for new methods, no Rust knowledge needed. Same playbook as Lua embedding — small C API, ecosystem in Lua. omnimcode-python (the legacy "Python embeds OMC" wrapper) excluded from the workspace because its `extension-module` pyo3 feature conflicts with python-embed at link time. Build it separately via `cargo build -p omnimcode-python` if needed; functionally eclipsed by the inverse direction landing here. 43/43 functional examples produce identical output under tree-walk and VM. 92/92 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent cb769e9 commit 49dd7b0

11 files changed

Lines changed: 981 additions & 86 deletions

File tree

Cargo.lock

Lines changed: 14 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
members = [
33
"omnimcode-core",
44
"omnimcode-ffi",
5-
"omnimcode-python"
65
]
6+
# omnimcode-python kept around but excluded from the default workspace.
7+
# It was the "Python embeds OMC" wrapper (extension-module mode); now
8+
# eclipsed by the python-embed feature on omnimcode-core, which goes
9+
# the other way (OMC embeds Python). Building it would conflict with
10+
# python-embed because both crates would `links = "python"`. Build it
11+
# separately via `cargo build -p omnimcode-python` if needed.
12+
exclude = ["omnimcode-python"]
713
resolver = "2"
814

915
[workspace.package]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# =============================================================================
2+
# Real-world demo: pandas-loads-MovieLens → OMC-harmonic-clusters → numpy-stats
3+
# =============================================================================
4+
# What this proves:
5+
# 1. OMC can ingest any dataset pandas can read (CSV here; parquet,
6+
# JSON, Excel, SQL all just one line in pd.* away)
7+
# 2. OMC can drive numpy for the math it doesn't natively do
8+
# 3. The OMC-distinctive part — Fibonacci-attractor bucketing — runs
9+
# ON TOP of the standard ecosystem, not as a replacement for it
10+
#
11+
# Pipeline:
12+
# pd.read_csv → OMC arrays
13+
# → harmonic_partition by attractor (OMC-side)
14+
# → numpy stats per bucket (mean/std/percentile)
15+
# → printed report
16+
#
17+
# Run:
18+
# PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 cargo build --release \
19+
# --bin omnimcode-standalone --features python-embed
20+
# OMC_PYTHON=1 ./target/release/omnimcode-standalone \
21+
# examples/datascience/movielens_harmonic.omc
22+
# =============================================================================
23+
24+
import "examples/lib/pd.omc" as pd;
25+
import "examples/lib/np.omc" as np;
26+
27+
println("=== OMC + pandas + numpy: harmonic clustering of MovieLens ===");
28+
println("");
29+
30+
# ---- 1. Load the full 10k-row sample via pandas ---------------------------
31+
32+
h t0 = now_ms();
33+
h df = pd.read_csv("examples/recommend/sample_10k.csv");
34+
h t1 = now_ms();
35+
36+
println(concat_many("loaded ", pd.nrows(df), " rows in ",
37+
t1 - t0, " ms via pandas"));
38+
println(concat_many("columns: ", pd.columns(df)));
39+
println("");
40+
41+
# ---- 2. Pull the rating column as an OMC array ----------------------------
42+
43+
h ratings = pd.col(df, "rating");
44+
h t2 = now_ms();
45+
println(concat_many("ratings array materialised in ",
46+
t2 - t1, " ms (", arr_len(ratings), " values)"));
47+
println("");
48+
49+
# ---- 3. Quick stats via numpy (the part OMC doesn't natively do) ---------
50+
51+
println("=== numpy stats on the rating column ===");
52+
println(concat_many(" mean: ", np.mean(ratings)));
53+
println(concat_many(" median: ", np.median(ratings)));
54+
println(concat_many(" std: ", np.std(ratings)));
55+
println(concat_many(" min/max: ", np.np_min(ratings), " / ", np.np_max(ratings)));
56+
println(concat_many(" p25: ", np.percentile(ratings, 25)));
57+
println(concat_many(" p75: ", np.percentile(ratings, 75)));
58+
println("");
59+
60+
# ---- 4. Harmonic clustering: bucket by Fibonacci attractor ---------------
61+
# Multiply ratings by 100 and feed through OMC's harmonic_partition. The
62+
# resulting buckets group ratings that fold to the same attractor —
63+
# something neither pandas nor numpy expose as a primitive.
64+
65+
fn ratings_x100(arr) {
66+
h out = [];
67+
h i = 0;
68+
while i < arr_len(arr) {
69+
arr_push(out, to_int(arr_get(arr, i) * 100));
70+
i = i + 1;
71+
}
72+
return out;
73+
}
74+
75+
h scaled = ratings_x100(ratings);
76+
h t3 = now_ms();
77+
h buckets = harmonic_partition(scaled);
78+
h t4 = now_ms();
79+
println(concat_many("harmonic_partition: ", t4 - t3,
80+
" ms, ", arr_len(buckets), " attractor buckets"));
81+
println("");
82+
83+
# ---- 5. For each bucket, hand back to numpy for stats --------------------
84+
# This is the join: OMC bucketed harmonically, numpy summarises each
85+
# group. Round-trip through both runtimes works seamlessly.
86+
87+
println("=== Per-attractor bucket stats (mean / std / count via numpy) ===");
88+
h k = 0;
89+
while k < arr_len(buckets) {
90+
h bucket = arr_get(buckets, k);
91+
if arr_len(bucket) > 0 {
92+
h label = fold(arr_get(bucket, 0));
93+
h n = arr_len(bucket);
94+
h m = np.mean(bucket);
95+
h sd = np.std(bucket);
96+
println(concat_many(
97+
" attractor ~", label, " ",
98+
"n=", n, " ",
99+
"mean=", m, " ",
100+
"std=", sd
101+
));
102+
}
103+
k = k + 1;
104+
}
105+
println("");
106+
107+
# ---- 6. Demonstrate the inverse: argsort via numpy, lookup via OMC -------
108+
# Sort the unique movies by avg rating using numpy's argsort, then build
109+
# a top-10 with OMC indexing.
110+
111+
h grouped = pd.group_by(df, "movieId");
112+
h means_dict = pd.agg_mean(grouped);
113+
h movie_ids = dict_get(means_dict, "movieId");
114+
h avg_ratings = dict_get(means_dict, "rating");
115+
println(concat_many("aggregated ", arr_len(movie_ids),
116+
" movies (pandas group_by + mean)"));
117+
118+
# numpy argsort returns indices that would sort the array. We negate
119+
# the ratings to get descending order.
120+
fn negate(arr) {
121+
h out = [];
122+
h i = 0;
123+
while i < arr_len(arr) {
124+
arr_push(out, 0 - arr_get(arr, i));
125+
i = i + 1;
126+
}
127+
return out;
128+
}
129+
h order = np.argsort(negate(avg_ratings));
130+
131+
println("");
132+
println("=== Top 10 movies by avg rating (numpy sort, OMC indexing) ===");
133+
h j = 0;
134+
while j < 10 {
135+
h idx = arr_get(order, j);
136+
println(concat_many(
137+
" movie ", arr_get(movie_ids, idx),
138+
" avg=", arr_get(avg_ratings, idx)
139+
));
140+
j = j + 1;
141+
}
142+
143+
println("");
144+
println("=== Done ===");

0 commit comments

Comments
 (0)