HyperTools 1.0.0 — release to master (review & documentation; do not merge yet)#281
Open
jeremymanning wants to merge 61 commits into
Open
HyperTools 1.0.0 — release to master (review & documentation; do not merge yet)#281jeremymanning wants to merge 61 commits into
jeremymanning wants to merge 61 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…audit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erator, dev notebook - scripts/screenshot_harness.py: headless PNG capture per function/use-case - scripts/generate_baseline_screenshots.py: 13 baseline cases, all passing on v0.8.2 - dev/hypertools_2.0_dev.ipynb: interactive test matrix, one section per public function - Roadmap updated with design decisions mined from fork issue tracker (incl. comments) - tests/screenshots/ gitignored (reviewed locally / CI artifacts, not committed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mixture models to first-class 2.0 features; record approved backend='auto' policy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…models, robust coloring Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lazy heavy imports - Migrate to PEP 621 pyproject.toml (v2.0.0.dev0, py3.10+); delete setup.py, requirements.txt, MANIFEST.in, stale .travis.yml - CI: py3.10-3.13 matrix, setup-python@v5, cache@v4, codecov@v4, screenshot artifact upload; readthedocs python 3.9->3.11 - Remove memoize entirely (user requirement): str()-keyed cache truncated numpy arrays -> cache collisions returned wrong results (fork issue #3) - Lazy-import umap, seaborn, scipy.interpolate: import hypertools 5.1s -> 1.46s - 136/136 tests pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re-formatting, scope plot styling (fixes #259) - Replace external hdbscan package with sklearn.cluster.HDBSCAN (always available); drop the SyntaxWarning filter that existed only for it - plot(): pass format_data=False to the post-analyze reduction (data was already formatted; avoids a redundant format_data/PPCA pass per plot) - plot(): apply seaborn palette/style inside plt.rc_context() so plotting no longer permanently mutates matplotlib rcParams (GH #259) - verified with a real before/after rcParams diff - 136/136 tests pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lors) - cluster() supports GaussianMixture, BayesianGaussianMixture, LDA, NMF and returns (n_samples, n_components) membership proportions (rows sum to 1); hard-clustering behavior unchanged - New hypertools/tools/colors.py: mat2colors maps categorical labels, continuous 1D values, or 2D matrices (soft assignments / arbitrary numeric matrices) to RGB; colors2groups quantizes per-point colors into traces for the matplotlib renderer - plot() accepts cluster='GaussianMixture' etc. (points colored by proportion-weighted blends) and matrix-valued hue - 145/145 tests pass (9 new: real GaussianMixture/BGM/LDA/NMF calls, color-blend math, end-to-end mixture plot) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- plot([[a, b], [c]]) flattens arbitrarily nested dataset lists, coloring every leaf by its outermost group and rendering deeper leaves thinner and fainter (summary -> detail, per fork design issues #14/#16) - Nested string lists (text corpora) are explicitly excluded and keep their existing text-pipeline behavior - 156/156 tests pass (6 new, incl. rendered-line color/width assertions) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New hypertools/plot/interactive.py: plotly renderer mirroring _draw's contract (2D/3D traces, fmt-string mode mapping, per-trace colors/labels, hypertools no-ticks aesthetic, matplotlib elev/azim -> plotly camera) - Animations: sliding-window frames (animate=True) and camera spin (animate='spin') with play/pause controls - hyp.plot(..., backend='auto'|'matplotlib'|'plotly'): auto uses plotly ONLY on Colab/Kaggle (approved policy); matplotlib default everywhere else - Screenshot harness exports plotly figures via kaleido - 169/169 tests pass (13 new: policy resolution incl. Colab/Kaggle markers, fmt mapping, camera math, end-to-end plotly figure/animation assertions) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ok, backend fix, README 2.0 docs - scripts/generate_verification_screenshots.py: 44/44 cases pass covering every public function (plot/reduce/align/normalize/cluster/analyze/ describe/format_data/load/text) on both backends; INDEX.md manifest; curated copy committed to docs/images/v2.0-verification/ for PR evidence - dev notebook executed end-to-end with 0 errors via nbclient (dev/hypertools_2.0_dev_executed.ipynb); notebook cells updated to exercise implemented 2.0 APIs - backend.py: catch ValueError from mpl.use() -- matplotlib >=3.9 raises it (not ImportError) for missing ipympl; likely root cause of Colab animate=True failures (#235) - README: What's new in 2.0 + modernized requirements; ipykernel in [dev] - Full suite re-verified: 169/169 tests, 13/13 baselines, import 1.5-1.7s 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>
… time GitHub's windows/py3.13 runners ship a broken Tcl/Tk: TkAgg imports fine (so backend probing selects it) but window creation raises _tkinter.TclError. manage_backend now retries the plot once on the original backend after an interactive-backend TclError instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- plotly renderer now reproduces the matplotlib aesthetic exactly: black
wireframe cube (3D) / square frame (2D), hidden axes, unit-cube range,
matched camera (elev/azim), pt->px line/marker sizing, full fmt-string
support (marker symbols + dash styles, with 3D symbol fallbacks)
- MULTICOLORED LINES: continuous or matrix hue + line fmt colors each
trajectory continuously along its length (matplotlib Line3DCollection /
LineCollection; plotly per-point line colors in 3D, segment traces in 2D)
- Fix long-standing is_line() bug: '' in Line2D.markers made it return
False for every fmt string, silently disabling line interpolation on
modern matplotlib; also parse linestyles before marker chars ('-.')
- Re-mapped per-point labels onto interpolated trajectories (fixes latent
IndexError that interpolation re-enablement exposed)
- Parity montage generator (scripts/generate_parity_screenshots.py):
matplotlib|plotly side-by-side for 22 identical calls
- 173/173 tests pass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Removed (previously deprecated, now retired for 2.0): plot's group/model/ model_params; reduce's model/model_params/normalize/align; align's method/normalize/ndims and the ambiguous align=True form (now a clear ValueError with migration hint); cluster's ndims - DataGeometry.plot translates/drops retired kwargs when replaying geos saved by hypertools < 2.0 (group -> hue), so old files still load - New hyp.apply_model: the stack/unstack core from the revamp design -- one model fit across stacked datasets then unstacked to input structure (stack=False for per-dataset fits); model specs as registry name / dict / sklearn instance / pipeline list; mode auto|fit_transform|fit_predict| predict_proba; return_model for reuse on held-out data; explicit whitelist registry (no eval) - 185/185 tests pass (12 new apply_model tests) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s for all 2.0 features - docs/images/v2.0-parity/: 22 side-by-side (matplotlib | plotly) montages of identical calls -- line/marker styles, dashes, sizing, colors, hue variants, clustering, mixtures, nested lists, multicolored lines - docs/images/v2.0-verification/: refreshed 75-case matrix (was 44) now covering every plotting feature on BOTH backends, incl. multicolored lines, mixture models, nested lists, marker/line styles, animations, and apply_model - 5 new gallery examples (interactive backend, mixture models, multicolored lines, nested lists, apply_model), all executing cleanly; gallery rebuilt; apply_model added to the API reference - dev notebook updated for all implemented 2.0 features and re-executed end-to-end with 0 errors - README documents multicolored lines, apply_model, backend parity, and the retired legacy arguments - 185/185 tests pass; import 1.41s Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ments, plotly gallery - Animation EXPORT on both backends, format by extension: .gif (Pillow), .png/.apng (animated PNG), .mp4/.mov/.avi (ffmpeg). plotly animations render each frame via kaleido then assemble; exported frames no longer include the play/pause controls; frame counts scale with duration. 7 new tests save real files and verify frame counts. Sample GIFs from BOTH backends committed to docs/images/v2.0-animations/. - Mixture demos now use OVERLAPPING clusters (1.5 sd apart) so multi-class membership is visible as blended colors (examples, screenshots, parity, notebook); new test asserts a substantial fraction of genuinely mixed assignments. - Backend parity refinements: centered black 12pt title (matching matplotlib), default 640x480 canvas, 2D frame fills the canvas like matplotlib (no forced square), 3D box uses matplotlib's 4:4:3 aspect, camera distance tuned so cube sizes match (r=1.95). - Sphinx gallery renders plotly figures (plotly_sg_scraper + kaleido); new animate_plotly example with an animated GIF thumbnail wired into post_build; interactive-backend example shows the plotly figure inline. - Dev notebook displays animations inline (to_jshtml + plotly frames) and demonstrates gif export; re-executed end-to-end with 0 errors. - Evidence regenerated: 22/22 parity montages, 75/75 verification cases. - 192/192 tests pass (185 + 7 animation-export) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the macos/py3.11 CI failure: Google Drive answered a dataset request with an HTML rate-limit page (200 status), which load() cached as the dataset -- poisoning every subsequent text-data test on that runner with UnpicklingError. - _download_example_data: raise_for_status; detect HTML error pages before caching (all example datasets are pickles, which never start with '<') - _load_example_data: on a corrupt cache, delete it and retry the download once before failing; never leave a poisoned cache behind - Regression test poisons the real cache with the actual Drive error page and verifies recovery (or clean failure with the cache removed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… CI jobs - _download_example_data retries up to 4 times (2s/6s/18s backoff) when the host rate-limits, instead of failing on the first error page - CI caches ~/hypertools_data (immutable datasets, one cross-OS entry) so 24 concurrent jobs stop re-downloading the same files from Google Drive every run -- the root cause of the intermittent text-test failures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…multi-panel + cache verification - SVG export on both backends: static (.svg) plus ANIMATED vector SVG via SMIL (frames stitched with discrete display switching; verified frame advance in headless Chrome by scrubbing setCurrentTime). matplotlib frames captured through a public AbstractMovieWriter subclass with frame subsampling (<=60 frames) - plotly window animations now rotate the camera while the window advances, matching matplotlib's behavior - plotly titles centered over the plot area (xref='paper') with a matplotlib-matched font stack - hyperalign: n_iter argument (default 10) iteratively re-estimates the common template; dict form no longer returns None; removed leftover 'method' reference that raised NameError for unknown align strings - shapes zoo: bunny/cube/dragon/sphere/teapot/vase/biplane + datasaurus registered with their Dropbox sources (direct-URL download support in the loader; tolerant unpickler for dill/legacy-pandas formats; dill added as a dependency). 'egyption_mask' excluded: upstream file is an empty (0,3) array - Multi-panel figures verified (hyp.plot(..., ax=...) into user subplot grids, 3D + 2D panels) - Re-download hygiene verified: repeated loads leave the cache byte-stable (no duplication / storage leak) - Reconstructed the classic readthedocs hyperaligned-weights animation (docs/images/v2.0-animations/weights_hyperaligned.gif) - 9 new tests (tests/test_round3.py), all real calls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… regen - Docs: modern pydata-sphinx-theme (replacing sphinx_bootstrap_theme); full site + gallery build verified, screenshots committed to docs/images/v2.0-theme/. nbsphinx_execute='never': tutorial notebooks ship pre-executed, and 'auto' was re-executing every gallery notebook (doubling build time and hanging on plotly exports in the nbsphinx kernel) - Fixed zoom: Axes3D.dist was removed in matplotlib >= 3.8, silently disabling animation zoom; replaced with set_box_aspect(zoom=...) using the exact legacy scale mapping (10 / (9 - zoom)) - Reconstructed the classic readthedocs hyperaligned-weights animation (docs/images/v2.0-animations/weights_hyperaligned.gif): 36 subjects, align='hyper', smooth interpolated trajectories, working zoom - Modern demos: gallery examples plot_shapes_zoo + plot_datasaurus; executed tutorial notebooks hugging_face_embeddings (sentence- transformers + HF ag_news, mixture soft clustering, UMAP, animated spin gif) and modern_sklearn_dynamics (HDBSCAN, GaussianMixture, Lorenz attractor multicolored line, animated gif); registered in the tutorials toctree - Animation evidence regenerated with the rotation + zoom fixes; parity montages regenerated (22/22) with the title-font change - 202/202 tests pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ality fixes, embedding demos - hyperalign now REPEATEDLY applies the full procedure (n_iter passes, default 10): each pass's aligned output feeds the next, compounding convergence (mean distance to group mean 0.43 -> 0.004 over 10 passes on rotated copies). The classic readthedocs weights animation is regenerated with the corrected pipeline (normalize='across', align='hyper', zoom=3.5, rotations=1, frame_rate=50, linewidth=3) - NEW animate='serial' mode (both backends): datasets appear one at a time in list order, each growing point-by-point while earlier ones stay fixed, never connected -- built for conversation-turn visualizations; tests assert sequential reveal on both backends - Animation quality: full-canvas animated axes + skip tight_layout fixes cube/data clipping at rotation angles (border-pixel regression test); linewidth is now a plot() argument and animations no longer hardcode linewidth=1; markersize is now a plot() argument - EXACT per-point colors for markers: matrix hue / mixture-model scatter renders true per-observation blends via scatter (was: quantized color groups); plotly path already carried per-point colors - Shapes morph: 3510 frames @ 30fps (117s, 13 rotations), committed as mp4 + 20s preview gif - Demos: wikipedia embeddings (BAAI/bge-small-en-v1.5 + UMAP + 10-way GaussianMixture soft clustering, markersize=2) and reddit conversation trajectories (convokit reddit-small, sliding-window SBERT, per-speaker colors, animate='serial', 30s/3 rotations) -- both executed 0 errors with committed gifs - 205 tests passing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tax, 30fps standard, rebuilt demos
WEIGHTS (the classic readthedocs animation) — fully diagnosed via Jeremy's
2020 pieman_trajectory_demo notebook (hypertools 0.6.2 + timecorr):
gaussian temporal smoothing (var=300) -> hyp.align repeated n_iter=20
(SRM) -> smooth again -> UMAP -> animate
Two additional findings were required to reproduce it on modern deps:
- align('SRM') now supports n_iter (re-fits SRM on each pass's output;
inter-subject correlation plateaus ~0.87 = the data's shared-signal
ceiling, matching era behavior since the vendored SRM is unchanged
since v0.6.2)
- modern umap-learn's default n_neighbors=15 keeps neighborhoods
within-subject and DISPERSES the aligned bundle; n_neighbors=150 merges
same-timepoint rows across subjects and reproduces the tight looping
rope of the original. Recipe scripted in
scripts/generate_weights_trajectory.py; gif regenerated (900 frames,
30fps, tight rope verified against the reference render)
Also in this round:
- repeated-hyperalignment scale collapse fixed (procrustes' optimal
scaling < 1 shrinks data geometrically across passes; per-pass output
rescaling keeps norms stable through n_iter=50)
- single-call soft-cluster coloring:
hyp.plot(x, '.', markersize=2, reduce='UMAP',
cluster={'model': GaussianMixture, 'n_clusters': 10})
(dict accepts top-level n_clusters and model classes, in both cluster()
and plot(); colors flow from mixture proportions automatically)
- 30fps animation standard: plotly frame density raised to 30/s (cap
600); all tutorial gifs re-rendered at fps=30 with no conversion
downsampling (wikipedia 300 frames/10s/1 rotation; conversation 900
frames/30s/1 rotation; lorenz + hf spin 900 frames/30s); shapes morph
3510-frame mp4 + 30fps preview gif; matplotlib evidence gifs at 450
frames (plotly evidence gifs kept from the prior render -- kaleido
makes 450-frame exports impractically slow; noted)
- conversation demo rebuilt: 3-sentence windows WITHIN utterances (true
disconnection), repeating per-speaker colors, animate='serial',
rotations=1, no frame clipping
- 206 tests passing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The n_neighbors=150 recipe over-globalized the UMAP embedding and flattened the trajectory into a near-straight line; download.png shows a tightly-bundled bundle with a dramatic loop. Swept n_neighbors against the reference: 15 disperses into a hairball, 150 flattens the loop, and 36 (min_dist=0.1) is the sweet spot -- one tight rope that keeps the loop. Verified this is a pure UMAP-neighborhood effect, not a dependency version: the modern SRM branch is byte-identical to v0.6.2, and era umap-learn 0.4.6 also hairballs the same aligned data at its default n_neighbors=15. Also switched the animation from a rolling window (animate=True + tail_duration) to animate='spin': the window only ever showed a ~4s fragment (a tangle), while spin draws the whole bundle and orbits it so the loop is visible from every angle. Tightened the gif encode (scale=340, 48-colour palette) to ~8MB, in line with the other animation gifs, still 900 frames at 30fps with no downsampling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch back from animate='spin' to the classic animate=True sliding window, per review. The critical property -- the space inside the cube is fixed for the whole animation, independent of which window is visible -- is guaranteed by the pipeline (helpers.scale normalizes once from the full stacked dataset and the window updater never touches axis limits) and verified programmatically: axis limits are identical across frames while the visible fragment's extent slides along the loop. (The earlier claim that window mode showed a static tangle was a frame- extraction bug in the verification harness -- PIL's ImageSequence yields one re-seeked image object, so materializing it with list() produced N copies of the final frame. Measured correctly, the window render rotates and the comet travels: mean inter-second frame motion 8.3, 0 clipped frames.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Streams are a first-class data type -- no flag. hyp.plot() detects Python iterators/generators and Hugging Face IterableDatasets (load_dataset(..., streaming=True)) from the structure of the input: - the first stream_init samples (default 10,000) ESTIMATE the normalization/reduction parameters; those fitted models are then APPLIED to every subsequent sample, which is added to the plot dynamically (the fit-on-head/transform-forever semantics from the issue thread) - stream_chunk (default 100) is the per-fetch batch size; each chunk renders as one live redraw / saved animation frame - stream_max (default None) streams continually; infinite streams render incoming data indefinitely, and Ctrl-C cleanly finalizes any save_path animation and returns the geometry - stream_window optionally shows only the trailing samples (comet style) while everything consumed stays on the geometry - reduction models must support transform() (IncrementalPCA default, PCA, UMAP; TSNE raises); align/cluster raise for streams (cluster planned) - dict rows: numeric fields concatenated in insertion order, strings ignored (use .select_columns() for control); datasets added to [dev] 14 real tests (tests/test_streaming.py) incl. an actual HF iris stream, interrupt finalization, and a fitted-on-head-only assertion. New executed tutorial docs/tutorials/streaming_data.ipynb with two streaming animations. Docs theme: pydata-sphinx-theme -> Furo with the ContextLab brand ported from ContextLab/scheduler (Nunito Sans, lowercase 300-weight headings with 0.6px letter-spacing, green #007030 / dark #4CAF50), screenshot-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gallery: sphinx-gallery previously executed only plot_*-named examples, leaving chemtrails/animate*/precog/explore/save_*/analyze pages with code but no rendered output. All examples now execute (filename_pattern), matplotlib animations render as embedded mp4 video (matplotlib_animations + sphinxcontrib-video; animation examples expose `ani = ani_geo.line_ani` for the scraper), save_* examples write to temp files, and every example page gets a branch-aware "Open in Colab" badge + .ipynb link (post_build.py) so gallery examples open as runnable notebooks. Dual-backend audit (scripts/audit_gallery_backends.py): every example runs under BOTH matplotlib and plotly in subprocesses; 78/78 pass (save_movie under plotly needs a long timeout -- kaleido per-frame mp4 export). The audit caught a real, years-latent bug: per-dataset fmt lists (['-','--']) routed each dataset through interp_array_list (plural), silently replacing 2D arrays with lists of per-row interpolations; latent because is_line() always returned False before its round-2 fix. Fixed with interp_array + regression test. Plotly parity for animation extras: chemtrails/precog/bullettime draw low-opacity trail traces on window animations, tail_duration sets the window length, and zoom moves the camera (r = 1.95*(9-zoom)/8, mirroring the matplotlib zoom semantics); previously none of these were forwarded to the plotly renderer. explore maps to plotly's native hover. Universal loader: hyp.load (and DataGeometry.plot/transform) resolve strings by trying, in order: built-in dataset name -> local file (npy/npz/csv/tsv/txt/json/parquet/mat/pickle) -> Hugging Face dataset (split=/streaming=; streaming feeds straight into hyp.plot) -> Google Drive URL or bare id -> Dropbox URL or shared-link path -> any URL with or without https://. Lists of strings return lists of datasets. Raw text (whitespace) still flows to the text-embedding pipeline. Also fixes df2mat for pandas>=2 (get_dummies bool dtype made mixed DataFrames produce object arrays that crashed np.isnan). 19 new tests (12 loader incl. real HF/Drive/Dropbox/URL fetches, 6 plotly trails, 1 interp regression); 232 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-layer fix for example pages showing code but no output: 1. nbsphinx was claiming the gallery pages: sphinx-gallery writes a downloadable .ipynb next to each generated .rst, and nbsphinx rendered the UNEXECUTED notebook instead of the gallery page. auto_examples/*.ipynb is now excluded from the document build (downloads still work). 2. matplotlib animations render as embedded HTML5 video: the sphinxcontrib.video extension is registered (required by matplotlib_animations=(True, 'mp4')) and all animation example pages (chemtrails, precog, animate*, save_movie) now embed a playable 30s mp4, verified visually. Includes the regenerated gallery artifacts (all 39 examples executed, 9m39s total build execution). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…report Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…round 6.5) NEW ANIMATION STANDARD (both backends): frame_rate=30, duration=30s, rotations=1 -- one revolution every 30 seconds. Three layers had to change for plotly to actually match matplotlib's pacing: - library defaults (frame_rate 50->30, rotations 2->1) and the plotly frame math: n_frames = frame_rate * duration exactly like matplotlib (600-frame cap removed); parity test asserts identical 900 frames at ~33ms on both backends - DataGeometry.plot no longer replays animation-pacing kwargs baked into saved .geo files (old-era defaults like frame_rate=50 and rotations=2 silently overrode the current standard in every gallery example that calls geo.plot); explicit caller overrides still win - docs builds: plotly's sphinx-gallery renderer serialized every animation frame through kaleido for one static png (a 900-frame figure took ~an hour); the show path now writes a frame-stripped png plus an interactive html with embedded frames capped at 150, each shown proportionally longer -- total duration and rotation speed unchanged, pages ~0.1MB Streaming stability: the data->box transform is FROZEN from the head (the center+scale affine is captured once); every future sample goes through the same transform and out-of-range samples are clamped to the closest point on the box surface. Axis limits never change once set -- no more per-chunk rescale "twitch" (verified: zero vanishing ink across tutorial animation frames + exact-position regression tests). Legends (both backends): rendered to the RIGHT of the plot, vertically centered on the box (mpl bbox_to_anchor; plotly x=1.02/y=0.5 with a reserved right margin). Screenshot-verified in 2D and 3D. Gallery UX: thumbnail clicks were dead (sphinx-gallery >= 0.17 no longer wraps the thumb <img> in an anchor; the old gallery-fixes.js targeted extinct .xref markup) -- thumbnails now open the example's notebook on Colab while title text opens the example page. Animated thumbnails were squashed into 200x200 squares from 4:3 sources; all 7 regenerated letterboxed at the correct aspect from the new 30fps mp4s (scripts/generate_gallery_thumbs.py). Plotly evidence gifs re-rendered at the pacing standard. DataGeometry.plot/transform docstrings document the universal string-loading behavior for the API pages. 237 tests passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A kaleido subprocess wedged during test_animated_svg_plotly on one Windows runner and burned the full 6-hour Actions job timeout. No test legitimately takes over 20 minutes; a hung native call now fails fast with a stack dump instead of holding a runner hostage (thread method, since the hangs are inside native calls). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every documentation notebook (39 gallery + 13 tutorials) now opens with a
branch-aware install cell so it runs standalone in Google Colab. On a
preview branch it installs that branch from GitHub
(`%pip install "hypertools[interactive] @ git+...@dev-2.0"`, verified in a
clean venv: imports as 2.0.0.dev0 with 2.0-only features working); on
master it installs the released package. scripts/add_colab_install_cell.py
injects the line idempotently into the hand-authored tutorial notebooks,
and conf.py's first_notebook_cell emits the same line for gallery
notebooks on rebuild.
Gallery examples:
- shapes zoo: plots EVERY zoo shape (bunny, cube, dragon, sphere, teapot,
vase, biplane) as small black dots (',' pixel marker), one panel each
- datasaurus: plots ALL THIRTEEN datasets of the dozen as small black
dots ('.' point marker), one panel each
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squash-merge of the dev-1.0-refactor line into dev-1.0. HyperTools 1.0.0 keeps the familiar one-call API (plot/analyze/reduce/align/cluster/normalize/ describe/load) but rebuilds the internals, packaging, and feature set. Highlights: pyproject packaging (Python 3.10-3.13, NumPy 2); the monolithic tools module split into focused subpackages on a shared stack->fit-once->unstack core (hyp.apply_model, backed by pydata-wrangler); plot() returns a matplotlib Figure / HyperAnimation / plotly Figure and load() returns raw data (DataGeometry retired to an internal unpickle-only shim); interactive plotly backend; mixture clustering; hull surfaces; density shading; colorbars; multicolored lines; new animation styles + 2-D animation + morph; hyp.Pipeline; hyp.manip; hyp.predict/ impute; autoencoder + gensim + LSL + kaggle integrations; expanded hyp.load sources; built-in datasets re-hosted as non-executable, SHA-256-pinned formats with atomic downloads and trust-gated remote deserialization; ~2,400 tests across Linux/Windows/macOS x py3.10-3.13 with a zero-warning bar and a clean-docs gate. Squash-merged (not a normal merge) so the branch's large-binary development history stays out of dev-1.0 (see CONTRIBUTING "Merging"). Full change documentation is in the dev-1.0 -> master pull request. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ve image paths - The featured example at the top of the README is now the "story trajectories" animation (hyperaligned fMRI activity of 36 subjects moving together through a spoken story, examples/plot_story_trajectories.py) rather than the random-walk hypertools.gif -- a far more compelling demonstration of what HyperTools does. Rendered (460px) from the committed docs/images/v1.0-round17/story_trajectories.mp4. - Switched all README example images from commit-pinned raw.githubusercontent URLs back to plain relative paths (images/...). Relative paths render correctly both on this branch (in the PR) and on master after merge, so the previous "flip the pinned SHA to master after release" task is no longer needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four small verified figures demonstrating 1.0 features not shown by the existing README images, for embedding in the dev-1.0 -> master release PR: density shading (density=True), multicolored lines + colorbar (continuous hue), the interactive plotly backend (backend='plotly', rendered via kaleido), and trajectory forecasting (hyp.predict, Kalman). Each was generated and visually verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… datasets hyp.plot([A, B], hue=['A']*len(A) + ['B']*len(B)) drew a spurious straight line from A's last point to B's first point. The per-category regroup (reshape_data) merges rows by category and patch_lines then bridged EVERY group to the next -- including when consecutive groups are separate input datasets (distinct trajectories that must not be joined). GH #291. reshape_data now optionally flags each group whose first row begins a new input dataset; patch_lines skips the bridge INTO those groups. Bridging WITHIN a single trajectory (one dataset colored by contiguous categories) is unchanged, so such lines still render continuously through colour transitions. The same fix covers the cluster line path, which shares patch_lines. Tests: unit tests for reshape_data(return_boundaries=) and patch_lines(breaks=) in test_helpers.py; end-to-end regressions in test_hue_color.py asserting no A->B bridge for two datasets and a preserved bridge for a single trajectory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier featured-example change switched the README images to relative ](images/...) paths, which 404 when the readme is rendered as the PyPI long-description -- breaking test_wheel_metadata_has_no_relative_links. Repin all 8 images to absolute raw.githubusercontent URLs at dev-1.0 commit fc2429c (which carries every referenced image, including the new story_trajectories.gif featured example). All 8 verified HTTP 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first GH #291 fix only handled one unique contiguous category per dataset in dataset order. reshape_data still GLOBALLY merges every observation of a category into one array before any line is drawn, which destroys both dataset identity and within-trajectory run order -- so a line could still join separate trajectories or collapse a recurring category into a tangled polyline. A per-group boolean cannot recover information that global merging already discarded. Reported cases: * two datasets sharing ONE category -> merged into a single line * categories in reverse numeric order -> reverse bridge (ds2 -> ds1) * A A B B A A within one trajectory -> run order lost Fix: for LINE-bearing formats, replace the global merge with contiguous-run segmentation (new helpers.segment_by_run), preserving observation order and input-dataset identity. Each run is drawn as its own trace, coloured by its category, bridged only to runs adjacent WITHIN the same input dataset (never across a dataset boundary), and every category gets exactly ONE legend/colorbar entry (dedup via '_nolegend_', which _build_colorbar_info already collapses). MARKER-only plots keep the global grouping (scatter has no edges to fuse). Applied at the shared frontend so both backends and static+animated paths inherit it, and to BOTH the explicit-hue and cluster paths (cluster+line was equally affected). The superseded reshape_data(return_boundaries=) plumbing is reverted. Tests: segment_by_run units in test_helpers.py; a full GH #291 regression matrix in test_hue_color.py (two datasets same/different/reverse-numeric category, category repeated across datasets, A->B->A run order, 3+ datasets, single-point runs, marker/line/'o-' formats, matplotlib + Plotly, spin/window/True animations, and cluster lines) asserting no cross-trajectory bridge, shared per-category colour, and deduped legends. Full suite 2396 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…doc fixes
Follow-up to the GH #291 categorical/cluster line-run segmentation, resolving
three reviewer findings:
* Per-INPUT-dataset style lists broke on segmentation. Splitting each dataset
into one trace per contiguous run made the drawn-trace count exceed the
input-dataset count, so a caller's fmt=['.','-'] / linewidth=[1,3] failed
the one-value-per-trace length check. segment_by_run now also returns each
run's source-dataset index, and a new _expand_styles_to_runs propagates a
style list given at INPUT-dataset length to every run that dataset produced
(a list already at run length is used verbatim). Wired into all three line
regroup sites (hue + both cluster paths).
* Tuple-valued fmt (e.g. fmt=('.','-')) passed the list/tuple validation but
then hit list-only downstream branches and surfaced an unrelated internal
error whose occurrence depended on the hue pattern. fmt tuples are now
normalized to lists up front.
* Documentation contract corrected: the automatic per-dataset propagation is
narrowed to categorical/cluster LINE-run segmentation only (marker-only
grouping merges observations across datasets into one per-category trace, so
a per-dataset style is not well defined there, and MultiIndex expansion
likewise needs trace-count-matched lists). Dropped the incorrect claim that
alpha= is a broadcast named style -- it is generic **kwargs passthrough,
applied verbatim and never broadcast. parse_kwargs' length-mismatch error
now says 'trace(s) to draw' rather than the misleading 'dataset(s) to plot'.
Tests: seg_dataset unit assertions; per-dataset fmt/linewidth/marker
propagation (matplotlib + plotly), explicit per-run lists, cluster lines,
mismatch-still-errors; tuple fmt across plain/one-run-hue/multi-run-hue/
plotly/animation; and the marker-only trace-count contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n't hang CI test_plotly_mp4_export repeatedly stalled Windows/py3.13 CI for the full 20-min per-test timeout inside kaleido's sync-server call_function. kaleido 1.x's own per-render timeout only wraps the figure CALC (asyncio.wait_for around tab._calc_fig), NOT browser launch or tab acquisition, and its shared sync server blocks on unbounded Queue.get -- so a wedged headless Chrome hangs a to_image() call forever (the traceback sat 1200s at _sync_server.py join/get). Make the animation export resilient: * _bounded_to_image runs each frame's to_image in a daemon worker thread with a wall-clock watchdog (120s), raising TimeoutError on a wedge and abandoning the stuck (daemon) worker -- bounding the WHOLE call, not just the calc. * _reset_kaleido_server tears the server down within a bounded time, and if a clean stop is itself wedged (server thread stuck in Chrome) clears the singleton's running flag directly so a fresh session can start. * _retry_kaleido_export retries the whole export: the first attempt uses the fast shared browser session, and after a wedge it falls back to per-call one-shot rendering (a fresh browser per frame, watchdog-bounded) -- which an abandoned wedged thread (still holding the singleton's queues) cannot corrupt. Applied to both the video/gif (PNG) and SVG frame-render loops; the shared session's teardown now uses the bounded reset. A genuine failure now surfaces in bounded time instead of a 20-min hang. Tests: watchdog times out a hanging render / returns bytes when fast / propagates real errors; retry falls back shared->one-shot after a wedge and raises the last error when exhausted; and an end-to-end test injects a wedge into the first frame and verifies the export still completes via the one-shot retry (real kaleido/Chrome). Normal export path unchanged (12 export tests, same timing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…edes thread watchdog) Review of the previous thread-based kaleido watchdog (96ba486) found a real cross-export hazard: after a wedge it abandoned a live worker thread and, if a clean stop also blocked, cleared kaleido's private _initialized flag directly. Those abandoned threads still reference the process-global GlobalKaleidoServer singleton, so a LATER export in the same process could start a fresh server on that singleton while the stale closer eventually wakes and joins/deletes the new server's thread/queues -- hangs, deleted queues, leaked Chrome/threads. A blocked native/browser call simply cannot be safely interrupted or reclaimed from a Python thread. Fix at a process boundary instead: frame rendering runs in a KILLABLE subprocess (hypertools/plot/_kaleido_export_worker.py) that owns its OWN kaleido singleton + Chrome. The parent serializes exports (_EXPORT_LOCK), serializes the figure to JSON, and enforces a hard wall-clock deadline scaled to the frame count; on overrun it kills the whole process tree (Chrome included, via killpg on POSIX / taskkill /T on Windows) and retries in a fresh subprocess. The parent process now starts NO shared kaleido server, abandons no threads, and never touches kaleido's private state, so a wedged export cannot corrupt a later one. The shared-session speed optimization is kept INSIDE the worker (safe -- if it wedges the whole process is killed). Applies to both the video/gif (PNG) and SVG export paths. Tests replace the thread-watchdog unit tests with subprocess-lifecycle ones: deadline scales with frame count; _kill_process_tree terminates a genuinely blocked child in bounded time; a real render subprocess forced past a 0.5s deadline is killed + retried + raises bounded; the worker renders every frame to its own file; and -- the key guard -- a REAL export succeeds in the same parent process AFTER a wedged-and-killed export (proving no cross-export corruption). Full suite 2426 passed / 0 failed; 17 export tests pass end-to-end via the subprocess. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…path, not in-process test_export_worker_renders_all_frames_to_files called the worker's main() in the TEST process, which has no timeout -- so a transient headless-Chrome wedge hung the whole test (it did, wedged in kaleido's sync-server call_function, on the ubuntu-3.11 CI runner and hit pytest's 1200s cap). That reintroduced exactly the unbounded hang the subprocess boundary exists to prevent. Notably the real export tests (test_plotly_mp4_export etc.) PASSED on the same runner because they spawn the worker as a killable subprocess -- validating the architecture; only the in-process test was unsafe. Replace it with test_render_frames_via_subprocess_returns_all_frames, which drives the production path ( -> worker subprocess with a hard deadline + retry) and asserts one non-empty image blob per frame. The worker's CLI contract stays covered end-to-end by every plotly export test, all of which go through that same spawn path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The whole-export deadline was sized against the test suite's 60-frame exports, not real usage. The DEFAULT animation is duration=30 x frame_rate=30 = ~900 frames, so max(180, n_frames*6) gave 5400s per attempt = ~3 HOURS across two attempts -- far past pytest's 1200s cap and GitHub's job limits, so a wedged Chrome on a default-sized export would still hang CI for hours before recovery engaged. The old docstring's "two attempts stay well under 1200s" was false for anything over ~100 frames. Root problem: a frame-count-scaled whole-export deadline cannot distinguish "a long export that is steadily rendering" from "Chrome is wedged". Any value generous enough for a healthy 900-frame export is hours long. Replace it with a PROGRESS watchdog (_wait_with_progress). The worker already renames each finished frame into place atomically; the parent now counts completed frames and resets an inactivity timer whenever a new one lands. A wedge is caught in _KALEIDO_STALL_TIMEOUT (120s) REGARDLESS of frame count, while a healthy export runs as long as it needs. For the default ~900-frame animation that is ~4 minutes to detect+retry instead of ~3 hours. A generous absolute ceiling (_export_ceiling) remains as a backstop against pathological slow-drip progress. In-flight ".part" files are never counted as progress. Retries now RESUME: the parent keeps ONE frames dir for the whole export and the worker skips frames whose output already exists, so a wedge on frame 800 of 900 no longer redoes 800 successful renders. Also (low severity, same review): bound Windows process-tree termination -- taskkill is itself a subprocess that can stall, so it now runs with timeout=15 and falls back to proc.kill() plus a bounded wait. And the export workdir is mkdtemp + shutil.rmtree(ignore_errors=True) rather than TemporaryDirectory: on Windows a just-killed worker/Chrome can still hold frame files open, and a cleanup exception would otherwise REPLACE the TimeoutError we want to report. Tests: ceiling is a backstop not the primary guard; a stalled render is killed within the stall timeout; a slow-but-PROGRESSING render outlives the stall timeout and is NOT killed (the property that makes a 900-frame export safe); the absolute ceiling still trips under continuous slow progress; a forced stall in the real render path kills+retries+raises bounded; and the worker skips already-rendered frames (resume) without launching Chrome. Full suite 2431 passed / 0 failed; 21 export tests pass end-to-end.
…asons
Three review follow-ups on the export watchdog.
(1) Medium -- a COMPLETE export could still be reported as a failure. The worker
entered _shared_kaleido_session() before checking whether anything was left to
render. So if a previous attempt rendered every frame and then wedged during
browser TEARDOWN (after the last frame landed), the retry would start Chrome
again just to discover there was nothing to draw -- and if that startup or
teardown also wedged, hypertools reported failure while holding every requested
frame. Fixed on both sides:
* worker: compute the expected output paths and return IMMEDIATELY, before
touching a browser, when they all exist;
* parent: after every attempt, accept and load the frames whenever the set is
complete -- however the worker ended (killed, non-zero exit, or clean).
The old test did not actually prove Chrome was avoided (the session was opened
before the skip loop). Replaced with one that puts a stub `kaleido` on
PYTHONPATH which records any start_sync_server call: with all frames present the
worker must exit 0 with NO marker written. Verified non-vacuous -- with frames
absent the same stub records a start.
(2) Low -- ceiling hits were misreported as inactivity stalls. _wait_with_progress
returned a bare bool, so the caller always said "no new frame for 120s (headless
Chrome wedged)" even when frames were arriving steadily and the absolute ceiling
was the real cause. It now returns 'exited' / 'stalled' / 'ceiling' and the
caller raises a message matching what actually happened -- these need different
diagnosis (browser fault vs. an export that is rendering far too slowly).
(3) Low -- an unsuccessful taskkill was treated as success. The Windows branch
bounded taskkill but ignored its return code, so a prompt non-zero exit (access
denied, partially-exited tree) skipped the proc.kill() fallback and could leave
the worker or its Chrome alive as a retry began. It now falls back on a non-zero
returncode as well as on TimeoutExpired.
Full suite 2432 passed / 0 failed; 22 export tests pass end-to-end.
…animation buttons Maintainer feedback (Andy's review, relayed by Jeremy). BUTTONS (plotly animation controls) The Play/Pause controls sat at paper (0, 0) anchored bottom-left. In 3-D the scene floats above that corner so it merely looked cramped, but in 2-D the axes fill the paper area and the controls were drawn ON TOP of the chart's bottom-left corner. They now hang below the plotting area (negative paper y, top-anchored), laid out horizontally, with margin.b opened up so they are never clipped, plus light theming: symmetric padding (the default padding left 'Play' visibly off-center), subtle background/border, and the figure's own font. FONTS (both backends) Previously hypertools set no font at all unless non-ASCII text triggered a scan for a single covering font, so the default look was whatever each machine had, and mixed-script text was frequently broken: with emoji + CJK + dingbats in one plot NO single font covers everything, so the scan gave up and the text rendered as "tofu" boxes -- with a per-glyph UserWarning storm behind it. - Bundle Noto Sans Regular (SIL OFL 1.1, ~570 KB) under hypertools/external alongside the other vendored third-party material, with its license and a provenance README, and ship it via package-data. Plots now look the same on every platform. Bundling broader scripts is infeasible (a pan-CJK face alone is ~16 MB), so those still come from system fonts. - Resolve fonts through a per-glyph FALLBACK STACK instead of one face: matplotlib walks a font.family list, a browser walks a CSS stack. Text mixing scripts now renders completely from several faces (Latin from Noto Sans, Japanese from an installed CJK face, math symbols from DejaVu Sans). The stack is filtered to INSTALLED families, because naming a missing one makes matplotlib log 'findfont: ... not found' per text artist, and is anchored by DejaVu Sans (shipped with matplotlib) so it can never resolve to nothing. - Applied inside the existing scoped rc_context, so the user's own matplotlib rcParams are still left untouched (covered by a new test). - Set BOTH font.family and font.sans-serif: artists created with an explicit generic family='sans-serif' resolve through the latter, so setting only one left them on matplotlib's stock list. - Point labels no longer force family="serif". That hardcoded generic both clashed with the sans used everywhere else and bypassed the stack, so a label character the serif faces lacked (U+2726 '✦') rendered as tofu even though an installed font had a glyph for it. - The covering-font warning now fires only for characters NOTHING available can draw, instead of whenever no SINGLE font covered all the text -- it was firing on ordinary accented/Greek text that renders perfectly. Verified by rendering and visually inspecting every text surface Jeremy listed, on BOTH backends: titles, axis labels, colorbar tick labels, legends, point labels, user-added text, and unicode throughout (CJK, emoji, dingbats, Greek, math, accented Latin) -- plus 2-D and 3-D animation buttons before/after. Full suite 2442 passed / 0 failed (the one failure in the local run is tests/test_lsl_streaming.py picking up a FOREIGN LSL outlet broadcasting from another app on this machine; this delta contains no LSL/IO code and that test is green on CI).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HyperTools 1.0.0 is a ground-up modernization of the toolbox. The familiar one-call API is preserved (
plot,analyze,reduce,align,cluster,normalize,describe,load), but the internals, packaging, feature set, and test/security posture are new.Diff scale (
git diff master…dev-1.0): 991 files changed, +83,330 / −15,246. Thehypertools/package alone grows from a flattools/module set into ten focused subpackages built on one shared stack → fit-once → unstack model-application core.This description was assembled from six parallel subsystem audits of the actual
master→dev-1.0diff, plus directly-verified demonstration figures/animations (every image below was rendered from 1.0 and visually inspected).See it in action
Hyperaligned "story trajectories" — 36 subjects' whole-brain fMRI activity, hyperaligned in the 100-hub space then reduced to 3-D, tracing a shared path through a spoken story (
examples/plot_story_trajectories.py, the new featured example):Alignment — the same datasets before vs. after hyperalignment (
hyp.plot(list, align='hyper')):New-in-1.0 plotting — soft (mixture-model) clustering with blended memberships, lit convex-hull surfaces, KDE density shading, continuous multicolored lines + colorbars, the interactive plotly backend, and trajectory forecasting:
hyp.predict)Animated plotting (
hyp.plot(list, animate=True)) andhyp.describe:What changed, by subsystem
1. Plotting (
hypertools/plot/)Grew from 4 files to 17: low-level drawing split into two parallel renderers —
matplotlib_backend.py(~2.4k LOC) andplotly_backend.py(~2.5k LOC) — sharing backend-agnostic helpers (meshutil,surface,density,morph,trails,multiindex,colors,fonts,animate) so both stay in visual lockstep.draw.pyis now a 2-line shim.New
plot()keywords:backend='matplotlib'|'plotly'|'auto'(auto = plotly on Colab/Kaggle);surface=(lit, smoothed 3-D convex-hull "blob" / filled 2-D outline, Blinn-Phong lighting);density=(2-D heatmap / 3-D volumetric KDE);colorbar=(continuous or segmented-discrete); matrix/continuoushue=(multicolored lines; mixture proportions → blended RGB viamat2colors/color_reduce);names=(per-trace legend); animation stylesanimate='spin'|'serial'|'window'|'morph'+ per-dataset lists + 2-D animation (focused=,morph_samples=); per-datasetchemtrails/precog/bullettimelists;font=/label_alpha=(CJK-aware);xlabel/ylabel/zlabel; pipeline integration (manip=,pipeline=,impute=,resample=,predict=/t=,random_state=,return_model=True→{'fig','xform_data','animation','pipeline','models','predict'}); streaming (stream_init/chunk/max/window); MultiIndex expansion; arbitrary matplotlib-prop passthrough.Behavior: animated matplotlib
plot()now returns aHyperAnimation(.to_html5_video()/.save()/notebook autoplay); defaultsrotations 2→1,frame_rate 50→30; data-faithful PCHIP line interpolation; per-dataset list-kwarg length mismatches now raise rather than silently degrade.set_interactive_backend()accepts render backends.Retired:
group=(→hue=, now raises),model=/model_params=(→reduce={'model':…,'kwargs':…}).2. Reduce / cluster / align / manip (
hypertools/{reduce,cluster,align,manip}/)Each rebuilt as a subpackage (main module +
common.pybase class); 22 new files, +6,145 LOC. Four sklearn-compatible base classes (Reducer,Clusterer,Aligner,Manipulator), eachfit/transform/fit_transform/is_fitted.hyp.manipis new —Normalize,ZScore,Smooth(savgol|gaussian|boxcarkernels),Resample, with chaining (model=[…]builds aPipeline).manip/normalize/reduce/align/cluster=+ndims=) assemble a pipeline in canonical order manip → normalize → reduce → align → cluster.return_model=Trueeverywhere; a fitted model handed back is reused via.transform, never refit;random_state=injection.sklearn.cluster(no externalhdbscandep). New aligners: HyperAlign, SharedResponseModel/Det/Robust, Procrustes, NullAlign. Mixture/soft-clustering reducers (GaussianMixture/BayesianGaussianMixture/LDA/NMF) return membership proportions via the same code path ashyp.cluster. Six torch autoencoder reducers (Autoencoder,Deep/Sparse/Convolutional/Sequence/Variational).Retired:
reducemodel_params;alignmethod=andalign=True('hyper'is now a deprecated alias for'HyperAlign');clusterndims=no longer silently reduces (warns withoutreduce=).3. IO, data loading & dataset security (
hypertools/io/)The whole
io/package is new (load.py,sources.py,streaming.py,save.py,lsl.py), superseding master's 190-linetools/load.py.hyp.loadresolves an 11-step chain: built-in examples → scikit-learn bundled (iris/digits/…) → seaborn →fivethirtyeight/<slug>→kaggle/<owner>/<dataset>→ local files (.npy/.npz/.csv/.tsv/.txt/.json/.parquet/.mat/.xlsx/.gz) → Hugging Face ids (split=,streaming=) → Google Sheets → Google Drive → Dropbox → any URL. Failed loads emit a "tried, in order" digest with a near-miss suggestion.hyp.saveis new — extension-aware (.csv/.npz/.parquet/.mat/.xlsx/…, else pickle), atomic (temp-file +os.replace, mode-preserving), narrowed signature (protocol=only).Dataset security (the focus of a six-round pre-release review) — expand
.npz/.parquet/.json.gz) reconstructed with no unpickling (allow_pickle=False); master fetched Google-Drive pickles andpickle.loads'd everything..parttemp → hash-check →os.replace, best-effort per-datasetfilelock, rate-limit-aware retry).trust=True(HypertoolsTrustErrorat a single chokepoint covering extension/magic-byte/protocol-0 paths);allow_pickle=Falsefor remote npy/npz.sotusmigrated off the datawrangler corpus-loader bypass to a plain non-executablejson.gz.*_modelsklearn pipelines are the one documented pickle exception — hash-pinned before unpickling.Also new: streaming (
io.plot_stream, issue #101) andio.lsl_stream(Lab Streaming Layer input,[lsl]extra). Vendoredexternal/ppca.py+external/brainiak.py(SRM family) replace bare deps.4. Forecasting, imputation & the model-application core (
hypertools/{predict,impute,core}/)Entirely new (23 files, +4,774 LOC).
hyp.predict—Kalman,GaussianProcess,AutoRegressor,ARIMA(base install),Laplace([predict]),Chronos([predict-hf]); int/datetimet=horizon semantics; one model fit per dataset;return_model=Truereuse (predict_newreuses learned params).hyp.impute—PPCA,SimpleImputer,KNNImputer,IterativeImputer,Kalman; splices (observed values byte-identical, only NaNs filled); the Kalman imputer fills fully-missing rows.hyp.apply_model+hyp.Pipeline— the stack → fit-once → unstack core over an explicit, eval-free model whitelist (replacing a prioreval()over sklearn's namespace); picklable, reuse-capablePipelineunifying manip/normalize/reduce/align/cluster in canonical order.HypertoolsError/HypertoolsBackendError/HypertoolsIOError;config.inipublished as a queryable mirror of signature defaults.5. Public API, packaging & CI
__all__added to the top-level package (star-import no longer leaks internals). New exports:manip,predict,impute,save,apply_model,supported_models,Pipeline,HyperAnimation, theiosubmodule, and the three exception classes.DataGeometryremoved from the public surface;load/reduce/align/describe/clusterrelocated to their subpackages (same public names).setup.py/requirements.txt/.travis.ymldeleted → singlepyproject.toml, Python 3.10–3.13, NumPy 2, PEP 639 SPDX license.pykalman/statsmodelsmoved into the base install (sopredict/imputework out of the box). Optional extras:interactive, text, predict, predict-hf, io, density3d, torch, kaggle, lsl, gensim, dev.config.inishipped as package data..github/workflows/test.yml), all SHA-pinned: a 12-job matrix (Linux/Windows/macOS × py3.10–3.13) + a pandas-3 acceptance gate;wheel-smoke(installs the built wheel and sdist into fresh venvs);docs-clean(pristinegit archive,sphinx -W -E -a);dataset-gate(uncached fresh download of every hosted dataset)..readthedocs.yamlupdated (Python 3.11, ffmpeg + pandoc,plotly_get_chrome).6. Examples, docs & tests
plot_datasaurus,plot_surface,plot_density,plot_colorbar,plot_multicolored_lines,plot_mixture_models,plot_predict,plot_impute,plot_autoencoders,plot_gensim_text,plot_apply_model,plot_pipelines_return_model,plot_interactive_backend,animate_plotly,animate_surface_morph,plot_story_trajectories, ….rsttutorials became executable.ipynbnotebooks, +8 new (Hugging Face / Wikipedia embeddings, conversation trajectories, streaming, LSL, stock forecasting, projectile Kalman, …). Docs theme → Furo (ContextLab branding); sphinx-gallery plotly scraper + mp4 animations;api.rstreorganized into canonical pipeline order.-Wbar.Backward compatibility
The one-call API is preserved. Breaking changes are limited to long-deprecated arguments that now error instead of being silently ignored:
group=→hue=;model=/model_params=→reduce=;align(method=…)/align=True→align='hyper';cluster(ndims=…)no longer reduces.plot()returns aFigure/HyperAnimation/plotlyFigureandload()returns raw data — there is no centralDataGeometryobject anymore (it survives only as an internal unpickle-only shim, so.geofiles saved by hypertools ≥ 0.8 still load). Pre-0.8 deepdish/HDF5 geo files need a one-time out-of-process conversion (documented).Release status
1.0.0(final; not.dev0). Wheel + sdist build clean,twine-valid, and both install into a fresh venv reportinghyp.__version__ == "1.0.0"(verified locally and by the CIwheel-smokejob).-W).Merging
Please squash-merge this PR (per
CONTRIBUTING.md→ Merging pull requests): the 1.0 development history contains large generated binaries (rendered galleries, audit evidence, dataset payloads) that were later removed from the tree; a normal merge would copy that entire history — and every large blob in it — intomasterpermanently. Squash-merging brings only the final tree.Two small post-merge follow-ups (non-blocking): tag a signed
v1.0.0on the merge commit and publish only the final1.0.0artifacts to PyPI; the README already uses relative image paths, so nothing to re-point.🤖 Generated with Claude Code