Skip to content

HyperTools 1.0: architecture refactor + bug hunt#272

Open
jeremymanning wants to merge 151 commits into
dev-1.0from
dev-1.0-refactor
Open

HyperTools 1.0: architecture refactor + bug hunt#272
jeremymanning wants to merge 151 commits into
dev-1.0from
dev-1.0-refactor

Conversation

@jeremymanning

@jeremymanning jeremymanning commented Jul 5, 2026

Copy link
Copy Markdown
Member

HyperTools 1.0 — architecture refactor + bug hunt

This PR merges the completed HyperTools 1.0 class-based refactor into dev-1.0, together with a broad bug hunt (open-issue triage + two animation-rendering fixes you reported + a legend-clipping fix) and the CI fixes needed to get all platforms green.

Note: originally developed under the working name "HyperTools 2.0"; renumbered to 1.0 (package version 1.0.0.dev0, branches dev-1.0/dev-1.0-refactor) per project decision. Older commit messages/notes may still say 2.0.

⚠️ Please do not merge. Opened for your review; I'll proceed however you decide. Base: dev-1.0 ← Head: dev-1.0-refactor.

Supersedes #271, which GitHub auto-closed when its head branch was renamed dev-2.0-refactordev-1.0-refactor; all commits, evidence, and CI history carry over unchanged.


1. Architecture refactor (Plans 1–8)

Reorganized the working dev-1.0 code into the class-based structure, on modern deps, with DataGeometry removed from the public API:

  • Deps modernized: numpy ≥2, pandas ≥3, scikit-learn ≥1.4, matplotlib ≥3.8, plotly 6.x, via datawrangler 0.5 (funnel/stack/unstack/format-detection/model-dispatch). Supports pandas 3.0+.
  • Class-based modules: core (eval-free apply_model), external (vendored PPCA + brainiak SRM/DetSRM/RSRM), manip (Normalize/ZScore/Smooth/Resample), reduce/align/cluster (generic sklearn dispatch by name), io, plot (matplotlib + plotly backends). Old tools/ names remain as shims.
  • DataGeometry removed from the public API (kept only as a hidden unpickle shim so legacy hosted .geo datasets still load). plot() returns a Figure / (fig, ani); plot(..., return_model=True) returns {'fig','xform_data','models','animation'}; load() returns raw data.
  • Docs/gallery/tutorials migrated to the 1.0 API and rebuilt.

2. Reported animation fixes

Animated bounding box "crowded / cut off on the right." Animated 3D plots zoomed the cube in too far. Fix: set_box_aspect zoom 1.25→1.125 + full-canvas axes. Min cube-to-edge margin over the full save_movie rotation: right 80→96px, bottom 51→72px.

before (crowded) after (margin)
before after

Duplicate animation-legend entries (GH #207). Faint "trail" artists carried the dataset label, so each label appeared twice. Fix: trails tagged _nolegend_; legend is now the static union of in-focus datasets. Verified ['first','second'].

legend

Clipped static gallery legends. Wide legends (long labels / many entries) clipped off the right edge. Root cause: the fit routine measured the legend under seaborn's (narrower) font, but the figure is saved downstream under the default (wider) font. Fix: _fit_right_legend now measures the rasterized pixels under default rcParams and widens the figure (keeping the plot's size) until the legend has a margin. Regenerated plot_legend/plot_PPCA/plot_missing_data; pixel-based regression test added.


3. Open-issue triage → close-on-merge

The triage below has since been fully executed (2026-07-07): all 45 addressed/obsolete issues are CLOSED with per-issue run-code evidence comments; the 22 remaining feature requests carry research comments + low/medium/high effort labels; 6 issues were migrated from the jeremymanning fork (#273#278, fork tracker now empty); and 6 residual gaps found during re-verification were fixed on this branch (#94, #141, #199, #206, #209, #244). See the audit summary comment. Originally: triaged all 67 open issues against this branch with real repros (see notes/issues-to-close-on-merge.md): 31 addressed/obsolete + 16 fixed or implemented on this branch (incl. §5's #169/#132 and §6's seven features) = 47 to close on merge; 20 stay open (feature requests / design decisions), each documented.

Bugs fixed from triage (all regression-tested):

# bug fix
#259 import hypertools mutated global rcParams['pdf.fonttype'] removed import-time mutation; editable-PDF default set inside manage_backend's snapshot/restore
#223 get_proj crash on 2D labeled plots guard get_proj; match annotate/update tuple shapes for 2D vs 3D
#146/#190 DBSCAN/MeanShift/OPTICS crash on n_clusters inject n_clusters only when the model's signature accepts it; register those clusterers
#148 show=False leaked the figure into pyplot plt.close(fig) for static figures (skipped when the user passed ax, and for animated figures, whose timer must stay alive)
#132 DataFrame columns consumed positionally across datasets format_data aligns named columns BY NAME to the first dataset's order (warns on reorder); mismatched column sets raise a clear ValueError
#214 wiki-model docstring vs wiki_model key docstring corrected
reduce=<class/instance>UnboundLocalError initialize model_params in the custom-estimator branch

4. CI fixes (get all platforms green)

The first CI run surfaced two platform issues (unrelated to the features above); both fixed:

  • Windows (all Pythons) — collection error. datawrangler 0.5.0 evaluates os.getenv('HOME') at import time to build its data dir; HOME is unset on Windows, so os.path.join(None, …) crashed dw's (and hypertools') import. Fixed hypertools-side by setting HOME=expanduser('~') before importing dw; filed upstream as Import crashes on Windows: config datadir evals os.getenv('HOME') which is None data-wrangler#32, fixed in dw 0.5.1 (released; verified import datawrangler works with HOME unset). The pydata-wrangler pin is bumped to >=0.5.1; the one-line HOME guard stays as belt-and-suspenders for environments still on 0.5.0.
  • macOS/Ubuntu Python 3.11+ — 3 tests. matplotlib 3.11 (Python 3.11+ only) resets a figure's canvas after plt.close() (the disabling the figure doesn't work as intended #148 fix), so tests/callbacks that read fig.canvas.renderer/buffer_rgba() failed. Fixed by guarding the renderer in update_position and rendering the affected tests through an explicit Agg canvas. (savefig after close still works, so users are unaffected.)
  • Windows — animated-figure draw crash. On Windows the animate backend switch to TkAgg actually succeeds (headless Linux/mac fall back to Agg), so the disabling the figure doesn't work as intended #148 plt.close() destroyed the FuncAnimation's real Tk timer and any later draw of the returned figure crashed ('NoneType' object has no attribute 'start'). Animated figures are now exempt from the show=False close — the disabling the figure doesn't work as intended #148 complaint was about static figures, and animations need their timer alive for playback.
  • Ubuntu 3.12 — screenshot-verification step. The screenshot harness discovered figures via plt.get_fignums(), which the disabling the figure doesn't work as intended #148 close empties; it now uses the figure(s) returned by plot() (13/13 cases pass).

5. New: hyp.predict + hyp.impute (resolves GH #169)

Two new modules in the established class-based style (base class + one file per model + funnel dispatcher), integrated into hyp.plot/hyp.analyze like cluster/align:

  • hyp.predict(data, model=..., t=...) — timeseries forecasting: Kalman, GaussianProcess, AutoRegressor (any sklearn regressor, recursive multi-step), ARIMA, Laplace (skaters ensemble), Chronos (HuggingFace foundation model, real chronos-t5-tiny test). t follows Use Kalman filter to fill in missing data #169's spec (int steps, or a datetime on time-indexed data — including past-date truncation). One forecast per input dataset, same dimensions, continued index.
  • hyp.impute(data, model=...) — missing data: PPCA (default; clean interface over the vendored implementation — format_data's fill now routes through it, behavior-preserving), SimpleImputer/KNNImputer/IterativeImputer, and Kalman, which fills rows where every feature is NaN — the exact gap Use Kalman filter to fill in missing data #169 describes (PPCA cannot).
  • return_model=True on both → (result, fitted) matching apply_model's convention; the fitted model can be passed back as model= on new data and is applied without re-estimation (verified: fit on A, forecast/impute B).
  • hyp.plot(data, predict='Kalman', t=30) overlays one dashed, low-opacity, same-color forecast tail per dataset (2D + 3D, both backends, no legend duplication, frame always contains the forecasts). impute= selects the missing-data model in the plot/analyze pipeline.
  • Dependencies: new [predict] extra (pykalman, statsmodels, skaters) and [predict-hf] (chronos-forecasting); GaussianProcess/AutoRegressor/sklearn imputers work on the base install; friendly ImportErrors otherwise (fresh-venv verified). yfinance is not a dependency — the tutorial self-installs it.
  • Tutorials (executed, real data):
    • stock_forecasting.ipynb — scrapes 2y of real Yahoo Finance prices for 4 tickers, backtests all models against a 30-day holdout with an honest MAE/MAPE table (spoiler: ARIMA/Kalman ≈ the naive baseline, as efficient-market theory predicts — the tutorial says so).
    • projectile_kalman.ipynb — a real NBA SportVU jump-shot arc (25 Hz optical tracking): Kalman imputation of 5 fully-occluded frames recovers them to RMSE 0.20 ft vs the recorded truth; forecasting the arc's final 20 frames from the first 30 lands within MAE 4.2 ft.
  • Forecast direction fix (review follow-up): the GaussianProcess default kernel is now DotProduct + RBF + WhiteKernel — the old stationary RBF reverted forecasts to the training mean beyond the data (drift −0.026/pt vs observed +0.0019/pt: reversed); the linear term extrapolates trends (+0.002..+0.010/pt: continues). Before/after renders + measurements in this comment.
  • Gallery: plot_predict (helical forecasts) + plot_impute (PPCA-vs-Kalman panels on the Use Kalman filter to fill in missing data #169 case); API reference sections added.

6. Seven long-standing feature requests (GH #95, #100, #108, #109, #127, #142, #177, #191)

All seven implemented in the 1.0 design language, in both rendering backends, each with numeric + screenshot evidence in this comment:

Every graphical feature was adversarially screenshot-reviewed by fresh agents across a {2D,3D}×{mpl,plotly}×{static,animated} grid; their findings drove 6 further fix commits (plotly WebGL surface artifacts, bounding-box containment, MultiIndex colorbars, legend fitting, 3D density visibility, trail-mode warnings). New gallery examples: plot_surface, plot_density, plot_colorbar, plot_multiindex, animate_trails_mix, animate_surface_morph.

Maintainer-feedback rounds (tight hulls + morph, constant rotation speed + plotly parity + lighting controls, axes-clipping + gif corruption + full-sample morphs, multibyte text support, GH #205): hulls now hug the observations (hull-hugging smoothing: post-Taubin pull-back to the hull; cube-cloud oversize 1.63×→1.13×, ≥99% containment; axes box sized from actual meshes incl. mid-morph union bound, both backends) and morphing is a first-class animation style — animate='morph' (Hungarian point-cloud morphs between datasets, tagged-list form for static backdrops) with per-segment rotations lists ([1, 0.25, 2, ...] = per hold/transition). Both shape-morph gallery demos collapsed to single hyp.plot calls. Morph rotation speed is constant (segment duration ∝ rotation count); plotly marker sizes are empirically calibrated to matplotlib (15.1px → 5.0px for markersize=6) and volumetric shading retuned to matplotlib's subtlety; every surface color/lighting/shading knob is verified effective on both backends (incl. new lightdir; silent no-op keys removed). The "cut off bounding box" report was traced to two real rendering bugs, both fixed: 3-D scene artists were clipped at matplotlib's aspect-shrunk square viewport (now unclipped in every animation path), and animation GIFs saved at reduced dpi were corrupted by a matplotlib writer resize through the interactive-backend window (saves now dpi-safe). Morph animations use every dataset's full sample set (duplicate-to-largest, duplicates hidden at holds; hold frames are a 100% pixel match to static plots). GH #205 fixed: full multibyte (CJK) text support in both backends — automatic covering-font detection (excluding placeholder fonts like LastResort) + a font= kwarg (family/path/FontProperties) applied to labels/legends/colorbars/titles; plotly also gained real labels= annotations (was a silent no-op); CI provisions CJK fonts on all platforms with anti-tofu pixel tests.


Testing

  • Local suite: 947 passed, 0 failed (+492 tests across all rounds: surface/density/colorbar/multiindex/trails/meshutil/load/color-alias/morph-animation/hull-tightness suites); 6 plotly→kaleido image-export tests deselected locally only — they deadlock Chromium in this sandbox but run fine in CI.
  • CI: all 12 jobs green (ubuntu/macos/windows × Python 3.10–3.13) — see this PR's checks for the head commit.
  • Docs rebuilt (make html succeeds); gallery regenerated with the 6 new example pages (including two captured animations).

🤖 Generated with Claude Code

jeremymanning and others added 30 commits July 3, 2026 13:17
Reorganize dev-2.0 into the jeremymanning fork's structure (base class per
area, folder per module, one file per child class); remove DataGeometry;
adopt datawrangler for the wrangling core (hybrid); keep dev-2.0's
plotting/animation/streaming/coloring. Classic API names + module aliases;
polars support via dw; strangler migration keeping tests green per commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bite-sized TDD plan: declare pydata-wrangler dep + text extra, probe dw
0.4.0 API surface and behavior (stack/unstack, funnel over numpy/pandas/
polars/list, sklearn text embed), reconcile py3.13/CI, establish the
data-wrangler issue-coordination workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…w API)

Controller recon resolved the environment blocker and confirmed dw 0.4.0's
surface: standardize on .venv (py3.12), pin pandas<3 (dw#30), use verified
dw.wrangle text call, and smoke-check against the 242-pass baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r#30)

HyperTools 2.0 step 0: adopt datawrangler for the wrangling core. Adds
pydata-wrangler>=0.4.0, a hypertools[text] extra -> pydata-wrangler[hf]
(opt-in transformer embeddings), and a temporary pandas<3 ceiling because
dw 0.4.0 type detection breaks on pandas 3.0 (data-wrangler#30).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Canonical list of dw symbols the 2.0 refactor depends on. Missing symbols
become filed data-wrangler issues + xfail-with-link, not silent green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Proves the real round-trips Plans 2-6 depend on: MultiIndex stack/unstack,
funnel generalization over numpy/pandas/list/polars, and sklearn text
embedding via dw.wrangle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the refactor branch to CI triggers, documents the py3.13/dw status, and
starts the data-wrangler issue-coordination log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…strangler

Move exceptions to core (shim _shared); add eval-free unpack_model + RobustDict;
central config.ini via dw configurator; relocate apply_model to core.model as
source of truth (tools shim) + accept fork {model,args,kwargs} dict form.
Behavior-preserving; existing suite stays green. Deep arrays->DataFrames dw
conversion deferred to per-module plans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…form

tools/apply_model.py becomes a shim; core.model is now the source of truth.
Adds {model,args,kwargs} spec support alongside {model,params}. Behavior
otherwise identical; existing apply_model tests unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nalysis

276 tests green. Captures the manip design questions surfaced from reading the
fork sources: arrays vs DataFrames, Smooth=savgol-not-gaussian, Resample needs
core.get, Normalize semantics reconciliation, fork bugs to validate/fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move vendored ppca/srm to external/ (shim _externals); build Manipulator base +
Normalize/ZScore/Smooth/Resample (DataFrame/dw-based, fork ports validated+fixed)
+ hyp.manip dispatcher (funnel-wrapped, applies Manipulator directly, not via
array-based core.apply_model). hyp.normalize compat left untouched. Adds core.get.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al; shim _externals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fork ports validated/fixed: np.clip bounds, core.shared.get import, Series dtypes.
Gaussian-smooth mode still owed (Plan 6, weights pipeline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Normalize/ZScore/Smooth)

The axis==1 branch of each transformer self-called the module-level
transformer name, which is the @dw.decorate.apply_stacked-decorated
function. Because apply_stacked unconditionally re-stacks its input
(adding a synthetic 'ID' row-index level even for a single DataFrame),
transposing that already-stacked frame leaked the ID level into the
columns, and the fitted params (keyed by the original, pre-stacking
row labels) could no longer be looked up -- raising "key of type
tuple not found and not a MultiIndex" for ZScore(axis=1),
Normalize(axis=1), and Smooth(axis=1).

Fix: keep @dw.decorate.apply_stacked only on the inner, always-axis==0
per-column core (renamed _transform_stacked); make the public
transformer an undecorated dispatcher that transposes the raw,
not-yet-stacked data and recurses into itself for the axis==1 case,
so the stacking machinery never sees a transposed frame.
resample.py's transformer/fitter carry no such decorator and were
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndas 3.0)

dw 0.5.0 fixes data-wrangler#30 (pandas-3 type detection). Ceiling lifted:
pandas>=2.2.0 (no upper), pydata-wrangler>=0.5.0, text extra [hf]>=0.5.0. CI
gains a pinned-pandas-3 acceptance gate (ubuntu/py3.12). Validated: full suite
293 passed on dw0.5.0/pandas3.0.3/numpy2.3.5 (== 2.3.3 baseline, no regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m tools

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tools/procrustes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot carried)

RobustSharedResponseModel omitted (external.brainiak vendors SRM+DetSRM only).
test_rsrm_not_exported uses `from hypertools.align import srm` because the classic
hyp.align callable shadows the hypertools.align attribute (chained-attribute
import form unsupported by design; see Plan 7 top-level API item).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move hypertools/tools/cluster.py to hypertools/cluster/cluster.py, fix the
format_data import to go through tools.format_data, and add
hypertools/cluster/__init__.py exposing cluster/models/mixture_models.
Recreate hypertools/tools/cluster.py as a re-export shim so
core.model._build_registry (which imports models/mixture_models from
hypertools.tools.cluster) keeps resolving. Classic hyp.cluster is
unchanged (still resolves via the tools/ shim per __init__.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 7, 2026
…ated 6, tagged 22)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremymanning

Copy link
Copy Markdown
Member Author

Full issue audit executed: both trackers verified, updated, and organized

Every open issue on ContextLab/hypertools (67) and jeremymanning/hypertools (13) was re-verified against this branch by actually running the code (no verdicts from reading source alone — each issue's comment carries copy-pasted commands + output, and rendered-image checks where the question was visual). Outcomes:

ContextLab (67 issues → 28 remain open, all organized)

jeremymanning fork (13 issues → 0 remain)

Verification

  • Fix-wave suite: 947 passed locally; CI status on the head commit shows on this PR's checks.
  • The audit's per-issue evidence (exact commands, outputs, and rendered-image checks) is in each issue's comment; the posting log recorded 0 failed GitHub operations across the 80 issue updates.

The ContextLab tracker is now the single organized backlog for deciding what else goes into 1.0: 28 open issues, every one labeled and documented.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant