Skip to content

HyperTools 1.0: architecture refactor + bug hunt#272

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

HyperTools 1.0: architecture refactor + bug hunt#272
jeremymanning wants to merge 193 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.


7. Round 17 — every non-deferred open issue addressed (GH #103 #116 #123 #130 #138 #153 #154 #159 #161 #162 #174 #187 #198 #227 #273 #274 #275 #276 #277 #278)

Following the per-issue "current status (1.0 update)" triage, this round implements all 20 non-deferred open issues (the 5 marked defer/don't-implement were left untouched). Highlights: a unified cross-module API (every dispatcher takes manip=/normalize=/reduce=/align=/cluster=/return_model=, canonical order documented as a flowchart in docs/pipeline_order.rst) + public hyp.Pipeline with pipeline= reuse (#138 #153 #227 #161 #174); manip list-chaining and the story-trajectories animation — animate='window'/focused=/duration= (#274 #275, post-align inter-subject correlation −0.004→0.33, jumps 18.96→0.73); label_alpha=/axis labels/animate= dict/2-D animations (#103 #154 #123); six torch autoencoder reducers, sklearn/seaborn/538/kaggle loaders, LSL streaming, gensim text wrappers (#162 #273 #116 #130 #198, all opt-in extras); and a full docs pass — docstring coverage 131→0 with an enforcement test, complete api.rst, 7 new tutorials, regenerated README media (#276 #278 #159 #187 #277).

Executed as 19 review-gated tasks; the reviews caught and fixed five real reuse-contract bugs (silent pipeline refit, Aligner/SRM/Resample transform replaying fit-time data). Per-issue evidence comments (code + exact new API + numeric/screenshot proof) are posted on all 20 issues; see the round-17 summary comment for details.


Testing

  • Local suite: 1271 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 x Python 3.10-3.13) on head 8c40499a -- run 28903254424
  • 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>
jeremymanning and others added 12 commits July 7, 2026 15:54
Resolves a live Lab Streaming Layer stream (pylsl.resolve_byprop by
name/type, else resolve_streams for any) and returns a plain iterator of
per-sample vectors that plugs directly into the existing streaming
machinery (is_stream/row_to_vector, GH #101), so
hyp.plot(hyp.io.lsl_stream(type='EEG'), stream_init=..., stream_chunk=...)
just works. pylsl is a new optional extra ([lsl], also added to [dev]);
its wheels bundle liblsl for all three CI platforms (verified during this
task), so no separate native-library install step is needed in the
workflow -- documented inline in .github/workflows/test.yml.

Tests spin up a real in-process pylsl.StreamOutlet on a background thread
(no mocks) and exercise name/type resolution, the timeout error path, an
end-to-end plot_stream smoke test, and the pylsl-absent ImportError
message via a real import-blocking subprocess.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 13 review follow-ups: a stalled/disconnected LSL source now raises
HypertoolsIOError after ~timeout seconds of consecutive silent pulls
instead of blocking the consumer forever; the inlet is closed via
try/finally on generator exit; new tests cover the any-stream resolution
fallback and the stalled-source error path (real outlet, no mocks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m->HF parse order (GH #198)

Add hypertools/tools/gensim_models.py: sklearn-API (BaseEstimator/
TransformerMixin) wrappers for gensim's Word2Vec/Doc2Vec/FastText
(vectorizer stage) and LdaModel/LsiModel/HdpModel (semantic stage, over an
internally-built BoW corpus). text2mat's vectorizer=/semantic= string
resolution now tries the scikit-learn registry first, then these gensim
wrappers, then a HuggingFace/data-wrangler fallback -- exactly Jeremy's
directive on GH #198 -- with zero gensim-specific special-casing needed in
the existing fit/transform dispatch, since every wrapper exposes the same
fit/transform/fit_transform surface as a scikit-learn estimator. Also
fixes semantic=None silently defaulting to LDA (broke vectorizer='Word2Vec',
semantic=None; test_transform_no_text_model's name always implied "skip
semantic"), and lets dict specs use the canonical 'kwargs' key (falling
back to legacy 'params'). New `gensim` extra in pyproject.toml (added to
dev too); tests/test_gensim_text.py trains real gensim models on a real
50-sentence two-topic corpus (no mocks), with a CI guard mirroring
test_lsl_streaming.py.

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

Task 14 review follow-ups (MEDIUM+LOW): a transform-time document whose
tokens were all unseen at fit time now has a direct regression test for
the zero-vector fallback (sparse output densified for the assertion), and
Doc2VecVectorizer's docstring notes that repeated transform() calls on the
same fitted instance are not bitwise-identical (gensim infer_vector
advances the model rng per call).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dence (round17 Task 15, GH #275/#274)

Adds scripts/round17_evidence/story_trajectories.py: runs Jeremy's exact
GH #275 snippet (manip= chain [Smooth(kernel_width=25), Resample(1000),
ZScore], align={'model': 'HyperAlign', 'kwargs': {'n_iter': 10}},
animate='window', reduce='UMAP', duration=30, focused=4) against the full
36-subject 'weights' dataset, saves the animation, and measures the
acceptance criteria: post-align inter-subject trajectory correlation (0.33)
far exceeds pre-align (-0.004), and mean path turning angle (0.36 rad) is
clearly non-linear. Screenshots 3 frames (different timepoints/camera
angles) extracted directly from the saved mp4 via ffmpeg -- the frame
selected is exactly what a user who opens the file sees, sidestepping an
observed Axes3D/FuncAnimation redraw-ordering quirk when re-invoking the
animation's own update function well after .save() has already played it
through once.

Also generates GH #274 jumps evidence: same 3 subjects/view under no-manip
(jumpy baseline, max inter-frame jump 18.96), chained-manip/savgol (2.79),
and Smooth(kernel='gaussian') (0.73) -- confirming the manip chain fixes
the discontinuous-jump regression.

tests/test_story_trajectories.py exercises the same manip/align/reduce/
plot code paths on a fast synthetic proxy (3 subjects on a shared latent
path, no network/UMAP) for the default test run, plus one
@pytest.mark.bigdata opt-in test that runs Jeremy's literal snippet against
the real dataset end to end.

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

story_trajectories.py's worst-case frame search could pick a frame near
the very start of the animation whose focused window isn't fully filled
yet (e.g. a single-point sliver), which is a valid "small margin" answer
but a poor representative screenshot. Candidates now start at
window_frames so every candidate's trajectory segment is fully populated
before the margin search runs -- regenerates story_frame_early/mid/late.png
(fresh-viewed: all three show real, tangled, loosely-aligned multi-subject
paths, not blank/degenerate frames).

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

The 538 loader's folder-listing calls hit api.github.com, whose
unauthenticated quota is 60 requests/hour PER IP. With ~12 concurrent CI
matrix jobs sharing a runner IP pool, the 538 tests deterministically
failed with HTTP 403 rate-limit-exceeded (not a transient flake).
_github_api_headers() now sends a Bearer token when GITHUB_TOKEN or
GH_TOKEN is present (Actions provides GITHUB_TOKEN automatically), raising
the quota to 5000/hour; the CI workflow passes secrets.GITHUB_TOKEN to
every pytest step. Unauthenticated local use is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rnals/ (GH #276)

AST scan found 131 public function/method/class definitions package-wide
(excluding hypertools/_externals/) with missing or empty docstrings,
concentrated in align/, predict/, manip/, impute/, external/ppca.py,
reduce/{describe,autoencoders}.py, plot/{matplotlib,plotly}_backend.py,
core/{exceptions,pipeline}.py, and _shared/helpers.py. Documented all of
them (accurate numpydoc descriptions read from each function's actual
body, no filler text) and added tests/test_docstrings.py: a real AST
regression gate asserting zero public defs/classes are undocumented
outside _externals/, plus confirming hyp.manip specifically now has a
substantive docstring. No behavior changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…owchart, 7 new tutorials (GH #278 #159 #153 #187 #275 #116 #273 #227 #161 #162 #198 #130)

- api.rst: add manip, save, impute, set_interactive_backend, Pipeline, the
  6 autoencoder reducers, text2mat + 6 gensim vectorizers, io.lsl_stream --
  every public export now documented. Fix autosummary.import_cycle warnings
  repo-wide by using bare names under currentmodule.
- Remove orphaned hypertools.tools.align.rst / hypertools.tools.normalize.rst
  (dead docs from the module reorg, confirmed unreferenced).
- Add docs/pipeline_order.rst (#153): canonical manip->normalize->reduce->
  align->cluster->plot/predict order + rationale, rendered as a committed
  SVG flowchart (docs/_static/pipeline_order.svg, matplotlib-generated via
  scripts/round17_evidence/pipeline_order_diagram.py -- no graphviz system
  dependency). Wired into index.rst toctree.
- New sphinx-gallery examples (real-executed): plot_story_trajectories.py
  (#275, Simony et al. 2016 ncomms12141/PieMan/HTFA-k=100 background),
  plot_datasets_tour.py (#116/#273), plot_pipelines_return_model.py
  (#227/#161), plot_autoencoders.py (#162), plot_gensim_text.py (#198).
- New/extended tutorial notebooks (real-executed with outputs):
  wikipedia_embeddings.ipynb gains a live Wikipedia-API keyword-fetch
  section with hue-colored point clouds + volumetric density overlay
  (#187); lsl_streaming.ipynb is new, streaming from a synthetic
  pylsl outlet (#130).
- conf.py: numpydoc_class_members_toctree = False (the new class entries
  were the first in api.rst; default was emitting ~100 broken per-method
  stub-link warnings).
- Fix a real pre-existing RST bug in set_interactive_backend's docstring
  (Markdown code fences, duplicate list numbering) surfaced by adding it
  to api.rst.
- doc_requirements.txt: add torch/gensim/kagglehub (needed by autodoc +
  the new gallery scripts).

make html: 0 toc.not_included / import_cycle / stub warnings (all three
target categories eliminated); remaining ~50 warning lines verified
pre-existing in untouched files. Full pytest suite: 1270 passed, 4 skipped
(1 confirmed-transient network flake, unrelated file).

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

Task 17 review follow-up (LOW): the datasets tour fetches from seaborn,
FiveThirtyEight, and Kaggle at run time, so a transient outage could abort
the whole readthedocs gallery build. Each source is now loaded independently
with a printed skip note on failure, and the panel grid is drawn from
whichever sources succeeded -- the real hyp.load(name) calls stay visible as
the demonstration. Verified all four still load and the script exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…text (GH #277)

Add a committed, re-runnable scripts/round17_evidence/readme_media.py that
regenerates every README-embedded image/GIF with current 1.0 code: hero
(hyperaligned animate='window' demo), plot.gif (animate=True), align
before/after (hyp.align(align='hyper')), a mixture-model soft-cluster PNG,
a describe PNG, and a new-in-1.0 hull-surface PNG. All assets regenerate
at their existing paths so no doc/reference changes were needed, and total
images/ footprint dropped (~10.8MB -> 6.9MB) despite adding one new asset.

Update README.md: swap in the regenerated media, update the Plot/Cluster/
Describe code samples to the current API (hue=/animate=/cluster=/
hyp.describe, replacing retired group=/n_clusters=/hyp.tools.describe),
add a Surfaces section, replace the defunct Travis/Gitter/mybinder badges
with GitHub Actions + readthedocs + PyPI badges, and drop/fix stale
2017-era text (dead Kaggle blog link -> archive.org snapshot, 0.5.0 cache
migration note, mybinder "Try it!" instructions).

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

The first CI fix authenticated the api.github.com folder-LISTING call, but the
CSV DOWNLOADS still went to raw.githubusercontent.com, which has its own
per-IP anonymous rate limit -- 12 concurrent CI jobs sharing a runner IP pool
then failed with HTTP 429. When a token is present, _fetch_538_csv now
downloads each file through the authenticated GitHub contents API
(Accept: application/vnd.github.raw, 5000/hr) instead; without a token it
still falls back to raw.githubusercontent.com. Verified both paths locally.

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

Copy link
Copy Markdown
Member Author

Round 17 — every non-deferred open issue addressed

Following your per-issue "current status (1.0 update)" comments, this round implements all 20 non-deferred open ContextLab issues (the 5 you marked defer/don't-implement — #111, #112, #113, #163, #225 — were left untouched). Each issue now carries its own evidence comment (code + the exact new API + numeric/screenshot proof); all 20 are being closed against this PR.

The work was driven by a single API-consistency audit whose outcome is the binding "design language" for the round: one model-spec grammar everywhere (str / class / instance / {'model','args','kwargs'} / legacy {'model','params'}+DeprecationWarning / list-as-pipeline / fitted-model reuse), return_model= on every dispatcher, cross-module stage kwargs in one documented canonical order, and a public hyp.Pipeline.

What landed, by theme

Process

Executed as 19 review-gated tasks (fresh implementer + independent reviewer per task; every graphical change verified by a fresh subagent viewing the rendered media at worst-case frames). The review gates caught and fixed five real bugs the reuse contract would otherwise have shipped — most notably build_pipeline silently refitting on every reuse, Aligner.transform/SRM replaying fit-time data instead of transforming new data, and Resample.transform ignoring its input.

Verification

  • Local suite: 1271 passed, 0 failed (947 → 1271 this round; 6 kaleido image-export tests deselected locally only, run in CI).
  • CI: all 12 jobs green on head CIHEAD — CIRUNLINK. (Two CI-only fixes this round: the fivethirtyeight/ loader now authenticates its GitHub API listing + CSV downloads via GITHUB_TOKEN so 12 concurrent jobs don't exhaust the anonymous 60/hr + raw.githubusercontent rate limits.)
  • New optional extras: torch, gensim, lsl, kaggle (all opt-in; import hypertools never requires them).

PR remains open for your review — nothing merged.

🤖 Generated with Claude Code

jeremymanning and others added 8 commits July 7, 2026 23:02
Correct pytest cwd/testpaths, [dev] extras scope, DataGeometry's
internal-shell status, the public API list, tools/ vs new reduce/
cluster/align/manip/io/predict/impute/core/ subpackages, plot/draw.py
being a shim over matplotlib_backend.py, and the Python 3.10+ floor.

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

QC audit found stale Sphinx docs: 8 legacy .rst tutorial files (align,
analyze, cluster, geo, normalize, plot, reduce, text) duplicated their
already-migrated .ipynb equivalents, causing "multiple files found for
the document" build warnings and risking Sphinx publishing the retired
0.x-API rst instead of the current executed notebook. Also removes the
DataGeometry tutorial (geo.ipynb), which documents a 0.x object not in
the 1.0 public API, and its tutorials.rst entry. Fixes the index.rst
title-underline-too-short warning and rewords pipeline_order.rst to
point at the story-trajectories gallery example instead of calling it
a "tutorial".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wargs-only dict spec

Two QC bugs found by running the 1.0 verification notebook:

P0-1: hyp.normalize(x, return_model=True).transform(new_data) raised IndexError
(tools/normalize.py) for every mode -- Normalizer.transform/fit assumed a list
of 2-D arrays, but the documented reuse pattern passes a bare 2-D array.
_as_list_2d now coerces either shape and returns single-in -> single-out.

P0-2: hyp.manip(x, model={'model':'Smooth','kwargs':{...}}) raised 'unknown
model' because unpack_model required all of model/args/kwargs; a kwargs-only
(or args-only, or model-only) canonical dict now resolves, matching how
reduce/cluster accept 'args' in x or 'kwargs' in x.

Regression tests added for both (bare-array reuse across/within/row + fit-time
stat reuse; single dict spec with optional args/kwargs + kwargs actually applied).

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

Fixes broken hypertools.tools.colors.mat2colors reference (real path is
hypertools.plot.colors.mat2colors) and adds coverage for Pipeline reuse,
manip chaining, Autoencoder reducers, gensim vectorizers, LSL streaming,
predict/impute, window/spin/serial + 2-D animation, and the Kaggle loader,
plus the full list of pyproject.toml extras in Requirements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsform round-trips through them (QC P1-1)

Manipulator gains an optional inverter (added to the fitter/transformer
triple) + an inverse_transform method: ZScore reconstructs data*std+mean,
Normalize reverses the min-max map, both from their fitted params (axis=0/
column-wise; the ambiguous row-wise case raises). Smooth/Resample are lossy
and stay non-invertible with a clear NotImplementedError. hyp.Pipeline can
now inverse_transform through a leading ZScore/Normalize step (it previously
raised). Updated the obsolete pipeline test (which asserted ZScore blocked
inversion) to assert the new capability + that a lossy step still raises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tead of crashing (QC P1-2)

hyp.plot(docs, vectorizer='Word2Vec') (default semantic) raised
'Negative values in data passed to LatentDirichletAllocation' -- gensim
embedding vectorizers (Word2Vec/Doc2Vec/FastText) emit continuous, often-
negative document vectors that LDA/NMF can't consume, and running a topic
model on embeddings is not meaningful anyway. text2mat now detects that
combination, warns, and returns the embeddings directly; pass semantic=None
to silence. Explicit non-embedding vectorizer+LDA (e.g. CountVectorizer)
is unaffected. Regression tests added.

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

Fixes across plot/analyze/load/describe/model/backend docstrings:
- align: documented default was 'hyper' but the real default is None (no
  alignment) -- corrected in plot/analyze/load (the worst inaccuracy).
- plot: ndims default 'None->3d' -> '3'; zoom default 0 -> 1; normalize
  self-contradictory default -> None; cluster example used reduce=+legacy
  'params' -> cluster=+'kwargs'; return_model Returns bundle now lists the
  'pipeline' key.
- reduce model lists (plot/analyze/load/describe) now include UMAP, the
  mixture reducers, and the six autoencoders; cluster list adds MeanShift/
  DBSCAN/OPTICS/AffinityPropagation/mixtures; dict examples use canonical
  'kwargs'.
- apply_model dict form documents the canonical {'model','args','kwargs'}.
- set_interactive_backend example used geo.plot() (removed in 1.0) -> hyp.plot(data, ...).

Docstrings only; no behavior change.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jeremymanning added a commit that referenced this pull request Jul 8, 2026
Every item from the PR #272 QC audit is fixed on dev-1.0-refactor and
independently re-verified (verify_code.md / verify_docs.md):
- P0-1 normalize reuse (5403530), P0-2 manip dict spec (5403530),
  P1-1 ZScore/Normalize invertibility (2badcf9), P1-2 Word2Vec semantic (7e42095)
- A1 CLAUDE.md (6201e42), A2/A3 API docstrings (376e1d9),
  A4 README (3c5c816), A5 Sphinx (6397a24, 724ad5f)
tasklist.md gains a resolution table; the notebook now demonstrates the
corrected behavior; qc_console.html regenerated.

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