Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
import tomllib
from pathlib import Path
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
assert project["name"] == "polyplot", project["name"]
assert project["name"] == "polyrender", project["name"]
print("package:", project["name"])
print("version:", project["version"])
PY
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
import tomllib
from pathlib import Path
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
assert project["name"] == "polyplot", project["name"]
assert project["name"] == "polyrender", project["name"]
print("package:", project["name"])
print("version:", project["version"])
PY
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ dist/
build/

# Caches and local artifacts
.polyplot/
.polyrender/
.polyplot/ # legacy cache dir name
.DS_Store

# Editor / local agent config (not part of the published package)
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# polyplot
# polyrender

`Three Fiber`–based [anywidget](https://anywidget.dev/) for **2.5D** polygons: preprocess stacked 2D outlines (per `cell_id` and `ZIndex`), build triangulated meshes, export spatial GLB tiles, and view them in the browser with a local tile server and WebGL widget.

![polyplot viewer showing colorful 3D cell meshes](assets/viewer.jpg)
![polyrender viewer showing colorful 3D cell meshes](assets/viewer.jpg)

**Documentation (Zensical):** [ckmah.github.io/polyplot](https://ckmah.github.io/polyplot/)
**Documentation (Zensical):** [ckmah.github.io/polyrender](https://ckmah.github.io/polyrender/)

## Requirements

Expand Down Expand Up @@ -38,18 +38,18 @@ uv sync --group dev
uv run marimo edit quickstart.py
```

[![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/ckmah/polyplot/blob/main/quickstart.py)
[![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/ckmah/polyrender/blob/main/quickstart.py)

## Usage

```python
import polyplot as po
import polyrender as po

tiles_info = po.meshify(gdf) # default cache: ./.polyplot/<hash>/
tiles_info = po.meshify(gdf) # default cache: ./.polyrender/<hash>/
po.plot(gdf) # meshifies from cache if needed; wireframe / opacity / BG in the viewer UI
```

- **`meshify`**: preprocess a GeoDataFrame (`cell_id`, `ZIndex`, `geometry`), write `tiles/` and `tiles.json` under `.polyplot/<content hash>/` by default (override with `out_dir=...`). Use `smooth=False` for no Taubin smoothing, `use_cache=False` to force a rebuild.
- **`meshify`**: preprocess a GeoDataFrame (`cell_id`, `ZIndex`, `geometry`), write `tiles/` and `tiles.json` under `.polyrender/<content hash>/` by default (override with `out_dir=...`). Use `smooth=False` for no Taubin smoothing, `use_cache=False` to force a rebuild.
- **`plot`**: calls `meshify` when needed, starts or reuses a local tile server, and returns a marimo `anywidget` viewer. Wireframe, opacity, and background are adjusted in the widget toolbar, not via Python.

## Full demo (development notebook)
Expand All @@ -70,10 +70,10 @@ uv run python scripts/make_liver_subset.py

## Development

The Python package lives in the `polyplot/` directory. Optional: install [gltfpack](https://github.com/zeux/meshopt) on your PATH for smaller GLB files (compression is enabled inside `meshify`).
The Python package lives in the `polyrender/` directory. Optional: install [gltfpack](https://github.com/zeux/meshopt) on your PATH for smaller GLB files (compression is enabled inside `meshify`).

## Repository

[github.com/ckmah/polyplot](https://github.com/ckmah/polyplot)
[github.com/ckmah/polyrender](https://github.com/ckmah/polyrender)

To publish pre-rendered molab sessions, run from the repo root: `uvx marimo export session quickstart.py` (and similarly for `dev/full_demo.py` if desired).
2 changes: 1 addition & 1 deletion autoresearch/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Autoresearch

Experiment drivers and benchmarks for the meshify pipeline. This tree is **not** part of the installable `polyplot` package.
Experiment drivers and benchmarks for the meshify pipeline. This tree is **not** part of the installable `polyrender` package.

| Path | Role |
|------|------|
Expand Down
6 changes: 3 additions & 3 deletions autoresearch/scripts/autoresearch_1k.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
REPO_ROOT = AUTORESEARCH_DIR.parent
_LOG_DIR = AUTORESEARCH_DIR / "logs"
_LOG_DIR.mkdir(parents=True, exist_ok=True)
MB = REPO_ROOT / "polyplot" / "_mesh_build.py"
MB = REPO_ROOT / "polyrender" / "_mesh_build.py"
RESULTS = AUTORESEARCH_DIR / "results.tsv"
LOG = _LOG_DIR / "autoresearch_1k.log"
MEASURE = _SCRIPT_DIR / "meshify_benchmark_measure.py"
Expand Down Expand Up @@ -150,10 +150,10 @@ def main() -> None:
_append(exp_id, "-", 0.0, "skip", desc + " (no regex match)")
continue
MB.write_text(new, encoding="utf-8")
_git("add", "polyplot/_mesh_build.py")
_git("add", "polyrender/_mesh_build.py")
c = _git("commit", "-m", f"experiment {exp_id}: mesh W={win} C={cap} F={fft_th}")
if c.returncode != 0:
_git("checkout", "HEAD", "--", "polyplot/_mesh_build.py")
_git("checkout", "HEAD", "--", "polyrender/_mesh_build.py")
_append(exp_id, "-", 0.0, "skip", desc + " commit_fail")
continue
commit = (_git("rev-parse", "--short", "HEAD").stdout or "").strip()
Expand Down
4 changes: 2 additions & 2 deletions autoresearch/scripts/autoresearch_alg.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
REPO_ROOT = AUTORESEARCH_DIR.parent
_LOG_DIR = AUTORESEARCH_DIR / "logs"
_LOG_DIR.mkdir(parents=True, exist_ok=True)
MB = REPO_ROOT / "polyplot" / "_mesh_build.py"
TP = REPO_ROOT / "polyplot" / "_tile_export.py"
MB = REPO_ROOT / "polyrender" / "_mesh_build.py"
TP = REPO_ROOT / "polyrender" / "_tile_export.py"
RESULTS = AUTORESEARCH_DIR / "results.tsv"
LOG = _LOG_DIR / "autoresearch_200.log"
MEASURE = _SCRIPT_DIR / "meshify_benchmark_measure.py"
Expand Down
56 changes: 28 additions & 28 deletions autoresearch/scripts/run_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
REPO_ROOT = AUTORESEARCH_DIR.parent
_LOG_DIR = AUTORESEARCH_DIR / "logs"
_LOG_DIR.mkdir(parents=True, exist_ok=True)
MB = REPO_ROOT / "polyplot" / "_mesh_build.py"
MB = REPO_ROOT / "polyrender" / "_mesh_build.py"
RESULTS = AUTORESEARCH_DIR / "results.tsv"
LOG = _LOG_DIR / "autoresearch_run.log"
MEASURE = _SCRIPT_DIR / "meshify_benchmark_measure.py"
Expand Down Expand Up @@ -474,7 +474,7 @@ def _cap_flat(ring_xy, base_idx, want_positive_z):

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

# ================================================================
# BATCH 5: Numba warmup, shapely coords bypass, strip vectorise,
Expand Down Expand Up @@ -750,8 +750,8 @@ def _largest_polygon(geom):

]

CA = REPO_ROOT / "polyplot" / "_cache.py"
TE = REPO_ROOT / "polyplot" / "_tile_export.py"
CA = REPO_ROOT / "polyrender" / "_cache.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

# ================================================================
# BATCH 6: Profile-driven optimizations after exp 149.
Expand Down Expand Up @@ -848,8 +848,8 @@ def _cell_max_turning(rings: list[np.ndarray]) -> float:
# --------------------------------------------------------
("gdf_cache_key vectorised WKB + repr-bulk id/ZIndex hash", [
(CA,
'_CACHE_VERSION = b"polyplot_cache_v5\\n"',
'_CACHE_VERSION = b"polyplot_cache_v6\\n"'),
'_CACHE_VERSION = b"polyrender_cache_v5\\n"',
'_CACHE_VERSION = b"polyrender_cache_v6\\n"'),
(CA,
"""\
def gdf_cache_key(gdf, smooth: bool) -> str:
Expand Down Expand Up @@ -1120,7 +1120,7 @@ def _arc_resample_closed(ring: np.ndarray, n_points: int) -> np.ndarray:

]

PP = REPO_ROOT / "polyplot" / "_preprocess.py"
PP = REPO_ROOT / "polyrender" / "_preprocess.py"

# ================================================================
# BATCH 7: curvature_resample_nb + simplify topology + cell_max_turning
Expand Down Expand Up @@ -1448,7 +1448,7 @@ def _cell_max_turning(rings: list[np.ndarray]) -> float:
]


PP = REPO_ROOT / "polyplot" / "_preprocess.py"
PP = REPO_ROOT / "polyrender" / "_preprocess.py"

EXPERIMENTS_8 = [

Expand Down Expand Up @@ -2319,7 +2319,7 @@ def _refine_shift_nb(prev: np.ndarray, curr: np.ndarray, k0: int, win: int) -> i

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

EXPERIMENTS_12 = [

Expand Down Expand Up @@ -2463,8 +2463,8 @@ def _collect_score(cid):

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
MB = REPO_ROOT / "polyplot" / "_mesh_build.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"
MB = REPO_ROOT / "polyrender" / "_mesh_build.py"

EXPERIMENTS_13 = [

Expand Down Expand Up @@ -2525,7 +2525,7 @@ def _build_glb_bytes(
pmax = positions_f32.max(axis=0).tolist()
n_idx = int(len(indices_u32))
gltf_json = _json.dumps({
"asset": {"version": "2.0", "generator": "polyplot"},
"asset": {"version": "2.0", "generator": "polyrender"},
"scene": 0, "scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "COLOR_0": 2, "NORMAL": 3}, "indices": 1, "mode": 4}]}],
Expand Down Expand Up @@ -2646,7 +2646,7 @@ def _arc_resample_nb(ring: np.ndarray, n_points: int) -> np.ndarray:"""),

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

EXPERIMENTS_14 = [

Expand Down Expand Up @@ -2735,7 +2735,7 @@ def _build_glb_bytes(
pmax = positions_f32.max(axis=0).tolist()
n_idx = int(len(indices_u32))
gltf_json = _json.dumps({
"asset": {"version": "2.0", "generator": "polyplot"},
"asset": {"version": "2.0", "generator": "polyrender"},
"scene": 0, "scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "COLOR_0": 2, "NORMAL": 3}, "indices": 1, "mode": 4}]}],
Expand Down Expand Up @@ -2805,7 +2805,7 @@ def _build_glb_bytes(

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

# ================================================================
# BATCH 15: Two-phase parallel gltfpack (sequential build + parallel compress)
Expand Down Expand Up @@ -3009,7 +3009,7 @@ def _build_glb_bytes(
pmax = positions_f32.max(axis=0).tolist()
n_idx = int(len(indices_u32))
gltf_json = _json.dumps({
"asset": {"version": "2.0", "generator": "polyplot"},
"asset": {"version": "2.0", "generator": "polyrender"},
"scene": 0, "scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "COLOR_0": 2, "NORMAL": 3}, "indices": 1, "mode": 4}]}],
Expand Down Expand Up @@ -3099,8 +3099,8 @@ def _build_glb_bytes(

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
MB = REPO_ROOT / "polyplot" / "_mesh_build.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"
MB = REPO_ROOT / "polyrender" / "_mesh_build.py"

# ================================================================
# BATCH 16: ring_target reduction + bulk array extract for collect_rings
Expand Down Expand Up @@ -3141,8 +3141,8 @@ def _build_glb_bytes(
" return rings_2d, zs\n\n\n@njit(cache=True, fastmath=True, nogil=True)\ndef _cell_max_turning_nb(",
" return rings_2d, zs\n\n\ndef _collect_rings_arrays(\n geoms: np.ndarray, zvals: np.ndarray, z_scale: float\n) -> tuple[list[np.ndarray], list[float]]:\n \"\"\"Like _collect_rings_for_cell but accepts pre-extracted numpy arrays.\"\"\"\n polys: list = []\n zi_keep: list[float] = []\n for geom, zi in zip(geoms, zvals, strict=True):\n p = _largest_polygon(geom)\n if p is not None:\n polys.append(p)\n zi_keep.append(zi)\n if not polys:\n return [], []\n ext_rings = _shapely.get_exterior_ring(np.asarray(polys, dtype=object))\n npts = _shapely.get_num_coordinates(ext_rings)\n coords_all = _shapely.get_coordinates(ext_rings, include_z=False)\n rings_2d: list[np.ndarray] = []\n zs: list[float] = []\n offset = 0\n for n, zi in zip(npts, zi_keep):\n nc = int(n)\n if nc < 4:\n offset += nc\n continue\n pts = coords_all[offset:offset + nc - 1]\n offset += nc\n if len(pts) < 3:\n continue\n area2 = (pts[:-1, 0] * pts[1:, 1] - pts[1:, 0] * pts[:-1, 1]).sum()\n area2 += pts[-1, 0] * pts[0, 1] - pts[0, 0] * pts[-1, 1]\n if area2 < 0:\n pts = pts[::-1]\n rings_2d.append(pts)\n zs.append(float(zi) * float(z_scale))\n return rings_2d, zs\n\n\n@njit(cache=True, fastmath=True, nogil=True)\ndef _cell_max_turning_nb("),
(TE,
"from polyplot._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_for_cell,\n build_loft_mesh_from_rings,\n cell_color,\n)",
"from polyplot._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_arrays,\n build_loft_mesh_from_rings,\n cell_color,\n)"),
"from polyrender._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_for_cell,\n build_loft_mesh_from_rings,\n cell_color,\n)",
"from polyrender._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_arrays,\n build_loft_mesh_from_rings,\n cell_color,\n)"),
(TE,
" # Group once: avoids O(n_cells * n_rows) boolean slicing.\n groups = {cid: df for cid, df in gdf_render.groupby(\"cell_id\", sort=False)}\n\n # Pre-extract rings once per cell (used for adaptive sizing + meshing).\n z_scale = cfg.get(\"z_scale\", 2.0)\n rings_by_cid: dict = {}\n zs_by_cid: dict = {}\n scores_by_cid: dict = {}\n for cid in all_cell_ids:\n rings, zs = _collect_rings_for_cell(groups[cid], z_scale)\n rings_by_cid[cid] = rings\n zs_by_cid[cid] = zs\n scores_by_cid[cid] = _cell_max_turning(rings) if rings else 0.0",
" # Bulk-extract geometry/ZIndex arrays once (avoids 1000 slow pandas __getitem__ calls\n # and eliminates the groupby overhead).\n z_scale = cfg.get(\"z_scale\", 2.0)\n _geoms_col = gdf_render.geometry.values\n _zvals_col = gdf_render[\"ZIndex\"].to_numpy(dtype=np.float64)\n _cids_col = gdf_render[\"cell_id\"].values\n _geoms_map: dict = defaultdict(list)\n _zvals_map: dict = defaultdict(list)\n for _cid, _g, _z in zip(_cids_col, _geoms_col, _zvals_col):\n _geoms_map[_cid].append(_g)\n _zvals_map[_cid].append(_z)\n rings_by_cid: dict = {}\n zs_by_cid: dict = {}\n scores_by_cid: dict = {}\n for cid in all_cell_ids:\n rings, zs = _collect_rings_arrays(\n np.asarray(_geoms_map[cid]),\n np.asarray(_zvals_map[cid], dtype=np.float64),\n z_scale,\n )\n rings_by_cid[cid] = rings\n zs_by_cid[cid] = zs\n scores_by_cid[cid] = _cell_max_turning(rings) if rings else 0.0"),
Expand Down Expand Up @@ -3170,7 +3170,7 @@ def _build_glb_bytes(

]

TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

# ================================================================
# BATCH 17: Key fix - only patch _tile_export.py to avoid Numba cache invalidation.
Expand Down Expand Up @@ -3256,8 +3256,8 @@ def _build_glb_bytes(
# ---------------------------------------------------------------
("tile_export: inline bulk ring extraction (skip groupby + pandas per-cell)", [
(TE,
"from polyplot._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_for_cell,\n build_loft_mesh_from_rings,\n cell_color,\n)",
"import shapely as _shapely\n\nfrom polyplot._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _largest_polygon,\n build_loft_mesh_from_rings,\n cell_color,\n)"),
"from polyrender._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _collect_rings_for_cell,\n build_loft_mesh_from_rings,\n cell_color,\n)",
"import shapely as _shapely\n\nfrom polyrender._mesh_build import (\n _adaptive_ring_targets_from_scores,\n _cell_max_turning,\n _largest_polygon,\n build_loft_mesh_from_rings,\n cell_color,\n)"),
(TE,
_BULK_OLD,
_BULK_NEW),
Expand Down Expand Up @@ -3304,7 +3304,7 @@ def _build_glb_bytes(
]


TE = REPO_ROOT / "polyplot" / "_tile_export.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"

# ================================================================
# BATCH 18: Continue ring_target reduction + misc TE-only optimizations.
Expand Down Expand Up @@ -3586,9 +3586,9 @@ def _build_glb_bytes(
]


TE = REPO_ROOT / "polyplot" / "_tile_export.py"
PP = REPO_ROOT / "polyplot" / "_preprocess.py"
CA = REPO_ROOT / "polyplot" / "_cache.py"
TE = REPO_ROOT / "polyrender" / "_tile_export.py"
PP = REPO_ROOT / "polyrender" / "_preprocess.py"
CA = REPO_ROOT / "polyrender" / "_cache.py"

# ================================================================
# BATCH 19: Profiling reveals:
Expand Down Expand Up @@ -3767,7 +3767,7 @@ def _build_glb_bytes(

]

_API = REPO_ROOT / "polyplot" / "_api.py"
_API = REPO_ROOT / "polyrender" / "_api.py"

# ================================================================
# BATCH 20: Key findings:
Expand Down
8 changes: 4 additions & 4 deletions dev/full_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# "marimo>=0.22",
# "geopandas>=1.0",
# "pyarrow",
# "polyplot @ git+https://github.com/ckmah/polyplot.git@ec17ca181279b30b4b2befddb15d5c15d118b3b0",
# "polyrender @ git+https://github.com/ckmah/polyrender.git@ed2f47e954ecf0ac846bca10668a2754b1a7725e",
# ]
# ///

Expand All @@ -19,11 +19,11 @@ def _():
import os

import geopandas as gpd
import polyplot as po
import polyrender as po

parquet_url = os.getenv(
"POLYPLOT_PARQUET_URL",
"https://huggingface.co/datasets/ckmah/polyplot/resolve/92678be92f8e0b06fc2a32b53885c4fdf3419ee3/liver_crop.parquet",
"POLYRENDER_PARQUET_URL",
"https://huggingface.co/datasets/ckmah/polyrender/resolve/92678be92f8e0b06fc2a32b53885c4fdf3419ee3/liver_crop.parquet",
)
return gpd, parquet_url, po

Expand Down
10 changes: 5 additions & 5 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
## Install

```bash
git clone https://github.com/ckmah/polyplot.git
cd polyplot
git clone https://github.com/ckmah/polyrender.git
cd polyrender
uv sync
```

Expand Down Expand Up @@ -38,13 +38,13 @@ Larger local parquets in `sample_data/` are not tracked by git (see `sample_data

## Quick start (marimo)

The shortest path is [`quickstart.py`](https://github.com/ckmah/polyplot/blob/main/quickstart.py) at the repository root (marimo). From a clone of the repository:
The shortest path is [`quickstart.py`](https://github.com/ckmah/polyrender/blob/main/quickstart.py) at the repository root (marimo). From a clone of the repository:

```bash
uv run marimo edit quickstart.py
```

A **“Open in molab”** badge in the [README](https://github.com/ckmah/polyplot#readme) links to a hosted run once the file is on the default branch.
A **“Open in molab”** badge in the [README](https://github.com/ckmah/polyrender#readme) links to a hosted run once the file is on the default branch.

## Full demo

Expand All @@ -64,4 +64,4 @@ Open the URL printed in the terminal (by default [http://127.0.0.1:8000](http://

## Publish

Documentation for this site is built with [Zensical](https://zensical.org/docs/) in CI and published to **GitHub Pages** at the [`site_url`](https://ckmah.github.io/polyplot/) configured in `zensical.toml`.
Documentation for this site is built with [Zensical](https://zensical.org/docs/) in CI and published to **GitHub Pages** at the [`site_url`](https://ckmah.github.io/polyrender/) configured in `zensical.toml`.
Loading
Loading