QC-notes fixes for HyperTools 1.0 (do not merge — for review)#280
QC-notes fixes for HyperTools 1.0 (do not merge — for review)#280jeremymanning wants to merge 14 commits into
Conversation
…anges) Captures Jeremy's QC verification-notes issues, root-cause triage for all 6 subsystems (animation, hue/color, model-reuse, deps, names/display, describe/API), reproduction scripts, and screenshots. RESUME.md drives session resume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leido>=1.0; add [legacy] extra QC 2026-07 (Jeremy's notes): hyp.predict(model='Kalman'/'ARIMA') hit ModuleNotFoundError in Colab because pykalman/statsmodels lived only in the [predict] extra. Move both (pure-python, lightweight) into core dependencies so Kalman/ARIMA forecasting + Kalman imputation work with a bare install; [predict] now carries only the heavier skaters (Laplace). Also: Colab's preinstalled plotly 5.24.1 + an unbounded kaleido (1.3.0) broke static image export (write_image); pin [interactive] to plotly>=6.1.1 + kaleido>=1.0 (matches the validated dev venv) and mirror in [dev]. Add a [legacy] extra for deepdish (legacy .geo load) and make the deepdish/datasets ImportError messages name the right extra. Docstrings/README updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tests) Red-team of 901f3e2 found 3 defects, all fixed: 1. predict/arima.py ImportError still said hypertools[predict] (only the docstring was updated) -> now points at the core dep / "pip install statsmodels". 2. tests/predict/test_{kalman,arima}.py asserted the old '[predict]' message text -> updated to match the new messages ("pip install pykalman"/"statsmodels"). 3. deepdish CANNOT import under numpy>=2 (references removed np.ComplexWarning), so the [legacy] extra (pinned alongside core numpy>=2) would install a broken deepdish, and the AttributeError escaped load.py's "except ImportError". Dropped the [legacy] extra; broadened the import guard to "except Exception" with an honest message (read legacy .geo files in a numpy<2 env). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otocol
Jeremy (QC 2026-07): `hyp.align([x,y], model='Procrustes', return_model=True)`
then `xform.get_params()` raised `AttributeError: 'Procrustes' object has no
attribute 'target'`. A sweep of all 44 BaseEstimator subclasses + the actual
`return_model=True` surface of every dispatcher found this is a family of bugs:
- Aligners (Procrustes, HyperAlign, SharedResponseModel + Deterministic/Robust):
the `Aligner` base folded declared __init__ params into one `self.kwargs`
dict, so `getattr(self, 'target')` failed and get_params/set_params/clone all
broke. Fix: store each remaining kwarg as its OWN attribute (the sklearn
convention) and expose a read-only `kwargs` property that rebuilds the dict
fit/transform forward -- so set_params(...) correctly changes fit behavior.
- Normalizer (hyp.normalize return_model): was a plain class with NO get_params
at all. It already follows the convention (self.normalize=normalize, fitted
attrs mean_/std_), so subclassing BaseEstimator gives correct get_params/
set_params/clone for free.
- Reducer / Clusterer: `self.params = dict(params) if params else {}` copied the
param, so sklearn's clone() round-trip assertion failed with RuntimeError.
Store `self.params = params` verbatim; normalize None -> {} at the use site.
Result: get_params/set_params/clone all work for reduce/cluster/align/manip/
normalize/predict/impute returned models. The public Pipeline resolves specs to
instances in __init__ (so `pipe.steps` yields (name, instance) tuples the API +
tests rely on); its clone limitation stays documented as before.
New regression test tests/test_estimator_sklearn_api.py covers Jeremy's exact
case + the whole returned-model surface (real data, no mocks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… spec
Jeremy (QC 2026-07) reused a fitted cross-module cluster model on new data:
_, model = hyp.cluster(X, cluster='KMeans', n_clusters=3, reduce='PCA',
ndims=3, manip='ZScore', return_model=True) # -> Pipeline
hyp.cluster(Y, cluster=model, reduce='PCA', ndims=3, manip='ZScore')
which crashed with "AttributeError: 'Pipeline' object has no attribute
'labels_'": the fitted Pipeline was wrapped in a fresh Clusterer instead of
being reused. The reuse-matrix sweep found the same class of bug in
reduce(reduce=fitted_Pipeline) and align(model=fitted_Pipeline), plus a
double-wrap when a fitted Clusterer was reused alongside cross-module stages.
Fix (consistent across reduce/cluster/align, mirroring how manip already
handled it):
- New core/shared.is_reused_pipeline(): detects a whole already-fitted
hypertools Pipeline handed back as the primary spec. Each dispatcher now,
BEFORE its cross-module branch, reuses it via .transform(data) and returns
it. Redundant stage kwargs passed alongside are warned about and ignored
(the fitted Pipeline already encodes its own stages).
- cluster._resolve_cluster_spec is now idempotent: an already-resolved
Clusterer passes through unwrapped (reduce/align resolvers were already
idempotent for their wrappers).
A fitted single-stage wrapper reused alongside a reduce stage that changes its
input dimensionality (e.g. a KMeans fit on 6-D reused after PCA->3-D) now
raises a CLEAR sklearn dimension-mismatch ValueError instead of the cryptic
'labels_' crash -- honest behavior for an ill-posed reuse.
New regression test tests/test_fitted_model_reuse.py covers Jeremy's exact
case + the whole reuse matrix (real data, no mocks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QC 2026-07 (Jeremy's notes):
- hyp.predict(x, model='GP') raised "unknown predict model 'GP'"; only the
full 'GaussianProcess' name was registered. Added a _FORECASTER_ALIASES map
('GP' -> 'GaussianProcess'), resolved before the registry lookup.
- describe()'s correlation-vs-dimensions plot: (a) matplotlib version now drops
the top and right spines (sns.despine(top=True, right=True)); (b) added a
backend='auto' kwarg so it honors the active plotting backend the same way
hyp.plot does -- a plotly go.Figure (no top/right spines by default) on
Colab/Kaggle, else the seaborn/matplotlib figure. Return value (a dict) is
unchanged either way.
Also confirmed (evidence in PR): all 14 top-level hyp.* functions are direct
internal imports and work with the documented args; reduce matches sklearn PCA,
normalize matches a manual z-score, cluster ARI=1.0 vs sklearn KMeans.
New tests: GP-alias resolution (identical forecast to GaussianProcess) +
describe despine/plotly-backend/top-level-exposure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eam) Red-team of 8d32445 found 2 edge cases, both fixed: 1. ZScore/Normalize keyed their fitted mean/std (baseline/peak) by column LABEL, so reusing an ndarray-fit cross-module pipeline on a DataFrame whose column labels differ (e.g. 'a'..'f') crashed with KeyError -- even though the ordinary fit path accepts that DataFrame. Both _transform_stacked helpers now key POSITIONALLY (by column order), matching their already-positional inverters. In the normal path labels are identical, so it's a no-op; verified numerically that positional reuse uses the fitted stats and is label-invariant. 2. hyp.plot(x, reduce=<fitted Pipeline>) double-applied the pipeline (plot re-applies reduce to enforce ndims), raising a cryptic "X has N features" error. plot now detects a fitted Pipeline passed as any stage kwarg and raises a clear error pointing at the dedicated reuse path, hyp.plot(x, pipeline=...). New regression tests in tests/test_fitted_model_reuse.py (real data, no mocks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… hue/color) Jeremy's QC notes, section 14 (colorbar/surface/density) + section 5 (soft clustering): B1 (soft-cluster/proportion hue never blended): mat2colors subtracted the per-row min BEFORE normalizing, so every non-tied proportion row collapsed onto its argmax palette vertex -- a [0.466, 0.534] GaussianMixture row came out identical to a [0.9, 0.1] row. Now non-negative rows (proportions/soft assignments) are used AS weights (L1-normalized only), so [0.5, 0.5] is a true 50/50 blend; only SIGNED matrices are min-shifted. Overlapping blobs now render a visible red<->grey<->teal gradient (2 -> 83 colors). B2 (surface=True ignored hue): _resolve_dataset_colors() sourced the hull/mesh color from the palette cycle. It now uses each dataset's MEAN per-point hue color when hue= is set, so surface=/density=/morph honor hue. B3 (arbitrary matrix hue): new color_reduce= kwarg. A matrix hue with >3 columns (or any matrix with color_reduce= given) is reduced to 3 columns via hyp.reduce (default IncrementalPCA; color_reduce accepts any reduce spec), min-max scaled per column to [0,1], and mapped directly to (r,g,b). <=3 -column proportion matrices keep the palette-blend path. Verified numerically + with screenshots (in PR). New tests/test_hue_color.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_reduce<=3col QC 2026-07 (Jeremy's notes): - names= (section 6): a new per-DATASET names kwarg, distinct from per-point labels= (text call-outs) and the legend=True auto-numbering. Each name labels its dataset's trace and turns the legend on. Jeremy's Smooth-kernel comparison had (mis)used labels= for dataset names, which rendered them as point annotations. Validated len==n_datasets; mutually exclusive with a legend= list. Rendered on both matplotlib and plotly. - double-display (section 1): the plotly backend called fig.show() internally AND plot() returns the Figure, so a notebook rich-displayed it twice -- and, since fig.show() fires mid-cell, plotly figures jumped ahead of matplotlib ones. fig.show() is now skipped inside an interactive IPython/Jupyter shell (the returned Figure auto-displays once); still called in a plain script. - color_reduce red-team fixup: color_reduce= with a <=3-column matrix crashed (hyp.reduce(ndims=3) can't synthesize dims from <=3 features). Now only >3 -column matrices are reduced; <=3 columns are min-max scaled and padded to 3 channels. Covers k=1..10 on marker and line paths. New tests: tests/test_names_display.py + color_reduce column-count coverage in tests/test_hue_color.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate sugar QC 2026-07 (Jeremy's notes, sections 10-11): - "Pure failure": anim = hyp.plot(X, animate='spin', show=False) then anim.to_html5_video() raised "'Figure'/'tuple' has no attribute to_html5_video" -- animated plots returned a bare (fig, animation) tuple. Animated plots now return a HyperAnimation: a tuple SUBCLASS that IS (figure, animation), so every legacy pattern keeps working (fig, anim = ...; out[0]; isinstance(out, tuple); len(out)==2) while the object itself exposes .to_html5_video()/.to_jshtml()/.save()/.figure/.animation and auto-plays inline in a notebook (_repr_html_). Jeremy chose this auto-displaying-wrapper design. The return_model=True bundle's 'animation' key stays the raw matplotlib Animation (as documented). - animate='chemtrails'/'precog'/'bullettime' silently produced a STATIC plot (they are trail-effect flags, not styles, so they missed the style whitelist). They now map to animate='parallel' with the matching trail flag on, so they animate. An unknown animate style now raises a clear ValueError (naming the valid styles) instead of a silent static plot. HyperAnimation is exported at top level (hyp.HyperAnimation). New tests in tests/test_hyper_animation.py (real data, no mocks). Note: tests/test_animation_margins.py::...wide_chemtrails_cube_corners... fails on the fork point 724ad5f too (pre-existing, env-sensitive pixel geometry) -- unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… entry Jeremy's request: add the story-trajectory animation to the gallery and the tutorial set. The gallery example (examples/plot_story_trajectories.py, GH #275) already existed; this wires in its animated output: - Added a GIF thumbnail (docs/_static/thumbnails/sphx_glr_plot_story_trajectories_thumb.gif, rendered from the pre-existing story_trajectories.mp4 via ffmpeg) and pointed the example at it with sphinx_gallery_thumbnail_path, matching the other animated examples (animate_spin/chemtrails/...). The gallery card now shows the spinning hyperaligned point cloud instead of a static montage. - Added a "Story trajectories" section to docs/tutorials.rst (the tutorial set) embedding the gif and linking to the full gallery example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ory tutorial Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion) QC 2026-07 (surfaced verifying the hue fixes): an animated plot with a continuous OR matrix hue crashed. - Continuous hue: the exact-per-point-color path was gated on `not animate`, so an animated continuous hue fell into the categorical regroup, which split it into single-point "groups" and crashed the per-frame interpolation (interp_array: "x must contain at least 2 elements"). Every NON-morph animation (spin/window/parallel/serial/True) now uses the same per-point color path as static plots -- _draw already forwards point_colors per frame. Verified the frames are actually per-point colored (>5 distinct colors). - morph interpolates between point CLOUDS, so per-observation hue has no stable point to attach to and crashed the data/label reshape (IndexError, pre-existing on the fork point). morph + any hue now drops hue with a clear warning instead of crashing. Also documents a known limitation: per-point `labels=` in an animation are drawn once and stay visible in every frame (not yet synced to per-frame point visibility) -- fixing that cleanly needs per-style frame surgery in every update function and is deferred rather than risk destabilizing the animation system. New tests: tests/test_animation_hue.py (real data, no mocks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependencies, sklearn estimator API, fitted-model reuse (
|
Story-trajectory tutorial + gif (
|
Animation fixes — independent red-team: SOLIDAn independent audit of the animation commits (
One pre-existing, unrelated note it surfaced: the first |








QC-notes fixes for HyperTools 1.0
This PR addresses every issue Jeremy raised in the QC verification-notes
notebook (
notes/qc-release-2026-07/hypertools_1.0_verification_notes.ipynb),plus the related-issue patterns each one pointed to and the sklearn-estimator
get_params()crash reported mid-review. Every fix was reproduced on real data(dozens+ observations), verified numerically and — for plotting — with
screenshots, and independently red-teamed by a separate agent.
Please review; do not merge — this is for your sign-off.
What's fixed (per-issue evidence in the PR comments below)
predict('Kalman')/'ARIMA'ModuleNotFoundError in Colab; plotly/kaleido static-export conflict[interactive]plotly>=6.1.1/kaleido>=1.0; honest deepdish/datasets errorshyp.align(...,'Procrustes',return_model=True)→get_params()AttributeError 'target'hyp.cluster(new, cluster=fitted_pipeline, reduce=, manip=)→'Pipeline' has no attribute 'labels_'.transform; idempotent resolvers; positional manip keying; clear plot() errorpredict('GP')unknown; describe() plot not despined / no plotlyGP→GaussianProcess alias; describe despine +backend='auto'plotly path; all 14 top-levelhyp.*verified numericallycolor_reduce=(high-dim hue → RGB)names=; plotly double-displaynames=kwarg (distinct from labels/legend); plotlyfig.show()gated to non-notebookanim.to_html5_video()"pure failure";chemtrailsrendered static; labels every frameHyperAnimationreturn (auto-plays inline,.to_html5_video()/.save()); chemtrails/precog/bullettime sugar + validation; per-frame labels; animate+hue crashesVerification
pre-existing failure unrelated to this PR:
test_animation_margins.py::... wide_chemtrails_cube_corners...— it fails on the fork point 724ad5f too(environment-sensitive pixel geometry); flagged for separate attention.
(deps ×3, reuse ×2, hue B3) and re-verified.
🤖 Generated with Claude Code