Skip to content

Cleanup/lean package - #17

Merged
LRabuzin merged 25 commits into
mainfrom
cleanup/lean-package
Aug 13, 2026
Merged

LRabuzin merged 25 commits into
mainfrom
cleanup/lean-package

Conversation

@LRabuzin

Copy link
Copy Markdown
Collaborator

No description provided.

LRabuzin and others added 25 commits March 5, 2026 16:25
These were an uncommitted work-in-progress that broke 5 interpretability
tests: build_multi_attribute_scatters coerced every extra column with
to_numpy(dtype=float), which raises on `condition` (routinely a string
label). Unlike the composite-heatmap call site it was not wrapped, so the
whole report build failed.

- Add _resolve_color_values: numeric columns pass through, non-numeric ones
  are factorized with their labels kept as colorbar ticks, so categorical
  attributes stay plottable instead of raising.
- Expose per_cluster_cell_type_enrichment params in the plugins config; the
  plugin was registered but unreachable by configuration. It stays out of
  `enabled` to preserve the conservative default.
- Cover both with tests, including a string-valued condition column and the
  noise-cluster skip behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tree was importable only as `src.*` from the repository root, and
pyproject.toml had no [project] table or build backend, so `grass-mil` could
not be installed or depended on.

- Move all modules to src/grass_mil/ and rewrite `from src.*` imports plus the
  `_target_:` strings in the config tree.
- Move configs/ into the package. Hydra resolves config_path relative to the
  decorated function's file, so the tree now resolves identically from a
  checkout and from an installed wheel, with no importlib.resources shim.
- Add [build-system] (hatchling) and [project], with squidpy/scanpy, plotly and
  playwright demoted from hard requirements to extras.
- Namespace the console scripts (grass-mil-train, ...); the previous
  `train_command`/`eval_command` names would collide on any user's PATH.
- Replace the bare rootutils.setup_root calls with _paths.setup_project_root,
  which tolerates a missing .project-root marker, and let paths.root_dir fall
  back to the Hydra runtime cwd. An installed wheel has no marker above it.
- Point tests at the packaged config tree instead of CWD-relative paths.
- Add tests/test_packaging.py and a CI job that pip-installs outside conda and
  runs the CLI from an unrelated directory, so packaging cannot rot silently.

No behaviour change: the same 243 tests pass before and after, plus 9 new
packaging tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs that together removed cell type from every sampled subgraph. Both
had to be fixed before the encoder can consume cell-type codes.

1. _apply_transform_to_each_subgraph discarded every node-length tensor as
   "already represented by x". That is false for categorical_index (the
   cell-type codes) and pos (coordinates), neither of which is recoverable
   from x. They are now sliced to the subgraph like any other node attribute.

2. Fixing (1) exposed the reason it had gone unnoticed: PyTorch Geometric's
   Data.__cat_dim__ returns -1 for any key whose name contains "index", so
   `categorical_index` was concatenated along the feature dimension when
   graphs are batched. With equal-sized graphs that silently corrupts the
   codes; with unequal ones it raises. Renamed to `categorical_codes`, which
   PyG batches along the node dimension like `pos`.

Graphs written by an older precompute are migrated on load, so existing
processed caches keep working without a forced re-precompute.

Regression tests cover subgraph survival, correct batching, and the migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encoder could not express the published input scheme. It had no
categorical embedding, so cell identity -- the only node input for some
cohorts -- had no path into the model at all, and `gine` fed the whole
edge_attr vector into the convolution rather than the edge length alone.

- NodeInputEmbedding implements h0 = E[t] + Wx, summing a learned cell-type
  embedding with a projection of the continuous features. It handles
  input_dim=0, where precompute legitimately emits a zero-column feature
  matrix and the embedding is the entire input representation.
- EncoderConfig.edge_feature_index narrows edge_attr to one scalar column for
  GINE message passing, matching the current NSCLC/LUAD models. No edge-type
  embedding: methods.tex still describes the older edge-type convention and is
  stale on this point.
- Out-of-range categorical codes raise instead of being clamped; clamping
  would silently train on the wrong cell type.
- Vocabulary size and column index are inferred from the processed dataset's
  label map and categorical slices, with explicit config winning -- the same
  contract as encoder.input_dim.
- New gine_length encoder preset (LayerNorm, max pooling) is the production
  arm; existing configs are unaffected because categorical_embedding defaults
  to null and reproduces the previous input projection exactly.

Covered by unit tests (sum-not-concat, zero-width x, reserved unassigned row,
out-of-range rejection, and a bit-identical assertion that non-selected edge
columns cannot influence the output) and an end-to-end fast_dev_run on a
cell-type-only cohort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bag pooling squeezed the attention head to a single channel and broadcast it
across every class logit, so the published per-class form could not be
expressed. The attribution suite rests on the identity

    L_c = sum_i A[i,c] * l[i,c]

which needs one attention channel and one logit per class; a shared channel
cannot express it.

- Softmax down the instance axis per class column. Attention width must be 1
  (shared, previous behaviour) or one per class; any other width now raises via
  a shared contract instead of silently broadcasting into the wrong shape.
- Fix the shape ripples together, since they failed in opposite ways:
  inference/aggregation.py flattened attention silently, while
  interpretability_export.py hard-rejected anything wider than one column.
- Export attention_c{j} and the raw attention_logit_c{j} alongside the
  canonical `attention` column. The raw logits are comparable across bags,
  which the within-bag normalised scores are not.
- validate_attention_width runs at build time too, so a mismatched config
  fails before the first batch rather than part-way through it.
- The width contract lives in a new leaf module (grass_mil.contracts) because
  models and inference already import each other through the supervised module.

BREAKING: task=finetune_mil is now categorical/CE with graph_head.output_dim=2
and attention.n_classes=2. A single-logit BCE head cannot express the
attribution identity. finetune_mean is unchanged.

Also changed: AggregatedPredictionPayload.bag_attention rows are now
[n_instances, n_channels] rather than [n_instances].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two sampling controls the clean repo lacked. Both default to off, so existing
configs are unaffected.

Interior seeds (ported from nsclc_tuning_study/boundary_sampling.py):
ego-graphs rooted at the edge of a section are truncated by the section
boundary rather than by biology, so they are not comparable to interior ones.
The mask excludes only the n_hops graph-hop rim around the convex hull;
internal holes and low-cellularity gaps are deliberately not boundary. The mask
definition is kept identical to the legacy one so models stay comparable, but
the traversal is expressed as sparse frontier expansion instead of a Python
BFS. Interior restriction is applied to the candidate pool *before* class
weighting, so class frequencies are computed over eligible nodes only.

Not ported: the Voronoi-bbox boundary mode (it exists to bridge networkx graphs
to relabelled tensors, which is unnecessary here since `pos` is available), the
distant-edge fallback, merged_allowed_seed_indices (it serves a
merge-all-regions pretraining path this repo does not have), and the
NSCLC_BOUNDARY_MODE environment variable, which becomes plain config.

Ego radius: r_k = radius_per_hop * depth + radius_offset, applied as a
post-hoc subgraph transform through the existing hook. Distances are in
datamodule coordinate units, not pixels -- the published 75/55 are pixel values
at the NSCLC imaging scale, so the defaults are documented as needing
conversion per cohort rather than being silently correct.

Kept from the legacy implementation: the empty-interior fallback to all nodes,
the largest-interior-component filter, and the atomic np.save/os.replace cache
write (a half-written cache file must not be visible to another worker).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SSL views were uniform elementwise masking, not the production augmentation.

- drop_importance: nodes and edges are dropped with probability
  min((1 - importance) * mu, p_lambda), where importance is the mean-max
  normalised log-degree, so hubs are preserved and sparse periphery is masked.
  Dropped nodes have features zeroed and their cell type set to the reserved
  "unassigned" code rather than being left as a spurious real type.
- feature_noise resolves its columns BY NAME against the datamodule's feature
  names. The legacy default noised a hardcoded index, which pointed at cell
  size in one cohort and at an unrelated column in another, feeding the encoder
  pure noise.

Two bug classes carried across deliberately:
- Edge dropping is symmetric. The legacy finetune transform left
  force_undirected=False while pretraining used True, so the two regimes
  augmented differently and message passing became direction-dependent. Both
  the new augmentations and the existing augment_graph now force it, with a
  test asserting is_undirected() over several seeds.
- Every write clones first. The legacy code wrote through a view, so its
  unassigned-token fixup silently reverted; a test now asserts the source graph
  is untouched.

Naming corrected without changing behaviour: the legacy `keep_prob` was used as
`rand > p`, so it is the drop probability.

Deviations recorded in the docstrings: methods.tex describes a min-max degree
normalisation, but the final run code uses mean-max and the code is
authoritative. Not ported: RewireEdgesAll, AddEdgesByCellType, ShufflePositions,
Apoptosis, Mitosis, PhenotypeShift, DropFeaturesGRACE, and the O(E) Python
remove_directed_edges (replaced by a vectorised mutual-edge mask).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Survival was fully supported by the module and losses but had no task preset,
so it could only be reached by hand-overriding four keys, and a mis-shaped
config failed deep inside the Cox loss with an opaque shape error.

- New task=finetune_survival, documenting that target_columns order is
  load-bearing ([time, event]) and that attention stays single-channel because
  a scalar log-hazard has no per-class decomposition.
- validate_task_config now rejects survival configs with the wrong number of
  target columns or a non-Cox loss.
- _survival_concordance_index was an O(n^2) Python double loop with two
  .item() calls per pair, i.e. a device synchronisation per pair. Replaced with
  a chunked tensor implementation that bounds peak memory at chunk_size * n
  booleans. No new dependency: lifelines/sksurv would contradict the lean
  packaging. A 4000-subject cohort now scores in ~0.05s.
- Tests compare against a reference double loop over several seeds, check that
  chunk size cannot change the result, and cover the no-events, all-ties,
  perfect and reversed orderings.

The synthetic datamodule gained a survival mode. Its follow-up times are spread
and its events mixed deliberately: with every event at the largest time the
partial likelihood is constant and carries no gradient, which is exactly how
the first version of this test failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interpretability suite computed an attention lift but no margins, pooled
over all bags with no region-equal weighting and no uncertainty. The method's
attribution claim rests on the identity L_c = sum_i A[i,c] * l[i,c], which was
never actually evaluated.

- instance_margin_contributions gives M[i,c] = A[i,c] * (l[i,c] - beta_c),
  with the head bias removed so the margin reflects the instance-driven part of
  the decision rather than the head's prior.
- cluster_attribution_summary reports signed margin share, |M|-weighted margin,
  margin sign consistency, abundance-corrected attention lift and prevalence,
  each with a 200-resample percentile bootstrap interval over regions. Regions
  are resampled, not instances: region sizes vary by orders of magnitude and
  pooling over instances lets one large region dominate.
- identity_residual = max_r |sum_i M[i,c] - L_c| is reported so the additive
  assumption is checked rather than trusted, and is covered by a test that
  deliberately breaks it.

One correction to the manuscript's description: the signed margin share is NOT
bounded to [-1, 1]. It is a fraction of the bag margin, so a cluster can exceed
1 when another cluster pushes the opposite way. The genuine invariant is that a
full partition sums to 1, which is what the test asserts. Regions whose margin
is within margin_eps of zero are excluded rather than clipped, since the ratio
is meaningless there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three spatial/geometry analyses the report generator relies on had no
equivalent here.

- Moran's I over a binary connectivity matrix with a permutation null,
  BH-FDR adjustment, and the differential-vs-reference z statistic
  (dI / sqrt(var_k + var_ref)) used to contrast clusters against the noise
  cluster.
- Ripley's centred cross-L via cKDTree pair counts, keeping the self-pair
  correction (a point is not its own neighbour: the count loses n_a and the
  denominator is n_a(n_a - 1)) and the region-equal aggregation.
- Chance-corrected agreement (ARI/AMI/NMI) plus cluster Jaccard overlap, so
  "structure the encoder resolves beyond the composition baseline" is a
  measurement rather than an assertion.

Implemented natively over scipy.sparse rather than through squidpy: the package
stays installable without scanpy/numba, and tier2.neighborhood already sets
that precedent.

Two things stated rather than papered over:

- SCOPE: the legacy computes Moran's I over cells inside pooled ego-graphs.
  Here the spatial table is root-instance level, so this measures
  autocorrelation over the instance graph. Defensible and arguably cleaner, but
  not numerically the same statistic.
- A latent legacy bug is fixed in the port: the Ripley radius grid was derived
  from whichever region happened to be iterated first, so results depended on
  ordering. The grid is now explicit config, defaulting to the median extent,
  with a test asserting order-independence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Attention heads expose head_spaces(), giving a_pre_tanh (the argument of the
  tanh gate) as a first-class output. The legacy report reached into
  attention_net.attention_a[0] directly. Pre-tanh matters because saturation
  collapses exactly the extremes that distinguish strongly-attended instances.
  With the space exported as table columns, multi-space analysis needs no new
  pipeline code: re-run the report with a different embedding_prefixes.
- New plugins: margin_attribution (the exact additive identity with bootstrap
  intervals), morans_i (+ differential vs the noise cluster), ripley, and
  cluster_agreement.
- All four are configurable and stay out of the default `enabled` list; a new
  plugins=full preset turns everything on.

margin_attribution is added alongside the existing attention_attribution rather
than replacing it. Consolidating them would change the payload keys that
build_composite_cluster_heatmap consumes, so that cut is deferred rather than
bundled into an already-large change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The goal is region cross-entropy as the sole objective, but the module still
carried the machinery for two auxiliary terms that the reported models never
used, plus config surface implying they were part of the method.

Removed:
- task.node_aux (instance-pull) and task.entropy_reg, with build_mil_aux_targets,
  compute_aux_node_loss, compute_entropy_regularization and gather_instance_logits.
- task.loss_weights, which is vacuous once those are gone: _compute_losses now
  returns the region loss directly instead of composing a weighted sum.
- NodePredictionHead, build_node_head and configs/model/heads/node.yaml. It was
  only ever consumed by node_aux, and was never wired into SupervisedModule at
  all -- the "node" auxiliary actually used instance-level subgraph logits from
  the graph head.
- GraphPooling's `attention` and `set2set` modes. Instance selection is the MIL
  head's job, pooling across ego-graphs; duplicating it inside each ego-graph
  adds parameters without a matching claim. GlobalAttention is also deprecated
  upstream.

supervised_module.py drops from 1318 to ~1100 lines. The advanced-MIL test arm
now exercises region accumulation alone, which is retained: it is a throughput
mechanism rather than an objective, and removing it would change optimizer-step
semantics for streamed bags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- New docs/method_fidelity.md maps each part of the method to its
  implementation and to the test that holds it in place, so a reader can check
  what is actually enforced before trusting a number. It also records the four
  places where methods.tex and the run code disagree (edge length vs edge type,
  mean-max vs min-max degree normalisation, the signed-share bound, and the
  Moran's I scope), and lists the analyses methods.tex claims that no code
  implements.
- Rewrote docs/legacy_gap_audit.md. It previously asserted that a list of
  legacy risks had been resolved by rebuilding from concepts, which was
  misleading: it audited engineering hygiene while several load-bearing pieces
  of the published method were missing entirely. It is now a status record,
  including the silently-wrong-answer bugs found while closing those gaps.
- Contracts doc gains the encoder, attention and attribution contracts,
  including the identity sum_i A[i,c]*l[i,c] == L_c as an invariant that bag
  pooling changes must preserve.
- README states the scope decision (the method, not a reproduction harness)
  instead of listing benchmark packs as a "known gap", and documents the
  survival preset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The margin was computed as contribution_c1 - contribution_c0 for ANY class
count, so with three or more classes every class beyond the first two was
silently ignored.

- instance_ovr_margins gives m[i,c] = M[i,c] - mean over the other classes.
  With C=2 this is exactly M[i,1] - M[i,0], so binary results are unchanged;
  with C=1 (regression, or Cox on the log-hazard scale) the contribution is
  already the margin.
- Cluster summaries are now indexed by (cluster_label, class_index). Binary
  heads summarise the positive class only, since the class-0 margin is its
  exact negation; multi-class heads summarise every class. Selectable via
  focus_classes.
- Each class's summary uses that class's own attention channel, so the
  attention lift reported for class c reflects A[:, c] rather than a pooled
  weight. Covered by a test where one class attends a single instance and
  another is uniform.

The contributions were already the bias-free attention-weighted logits
M[i,c] = A[i,c] * (l[i,c] - beta_c); that is unchanged, and the additive
identity is now asserted for a 4-class head as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Interpretability output is read by biologists, but it spoke the algorithm's
language.

- "Cluster" becomes "niche" throughout the interpretability layer: the
  `niche_label` column, `NicheSummary`, `niche_attribution_summary`,
  `niche_profiles`/`niche_agreement`/`per_niche_cell_type_enrichment` plugins,
  figure titles and axis labels. The *algorithm* is still clustering, so
  `run_clustering`, `ClusteringResult` and the sklearn parameters (`n_clusters`,
  `min_cluster_size`) keep their names -- those are the method, niches are the
  result.
- Label -1 is named **Background** rather than "noise", including the
  `skip_background` parameter and the "excluding Background" figure variants.
  Density-based clustering puts transitional and sparsely populated tissue
  there; it is a meaningful category, not a failure.
- Niches are ordered by a dendrogram over their composition profiles, so
  compositionally similar niches are adjacent in every table and heatmap
  instead of appearing in arbitrary label order. Correlation distance is the
  default, so equal makeup at different densities reads as similar. Background
  is pinned last rather than joining the tree, since a catch-all would distort
  it. Ordering flows from the composition summary into enrichment, counts and
  the report figures, and is disableable.

Figures name niches "Niche 3", keeping the numeric id so a reader can join a
figure back to the table without a lookup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Identifiers that embedded "cluster" inside a longer name were missed by the
word-boundary pass: the cluster-transfer helpers, AttributionResult.per_cluster,
and the instance_with_clusters.csv artifact. All now use niche vocabulary.

The clustering algorithm's own names are deliberately left alone
(run_clustering, ClusteringResult, n_clusters, min_cluster_size, and the
internal PCA feeding the clustering step).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Attention was allowed to emit a single channel broadcast across every class
logit. That cannot express a niche which pushes toward one class and away from
the other, and it makes the per-class attribution identity vacuous. A binary
head now needs two attention channels, a scalar head (regression, Cox
log-hazard) one; anything else raises with the required width named.

Also merges grass_mil/interpretability/contracts.py into grass_mil/contracts.py.
Two modules with the same name, one holding a single validator and the other
the interpretability payload dataclasses, was a needless ambiguity; the single
module stays dependency-light so any layer can import it without a cycle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drop_edges accepted a torch.Generator and never used it: it delegated to
torch_geometric's dropout_edge, which takes no generator and draws from the
global RNG. The module advertised purity while BGRL additionally passed a
literal None, so pretraining views were not reproducible under a fixed seed and
the existing determinism test passed only because it exercised a different
function.

- drop_edges now takes the draw itself, so the generator actually governs it.
- BGRL seeds one generator from task.augmentation_seed and advances it across
  views, so the two views of a batch differ while the run stays reproducible
  and independent of unrelated global RNG consumption.
- Deleted ssl_runtime.augment_graph. It duplicated drop_edges/drop_features
  with different semantics -- elementwise feature masking rather than dropping
  whole columns -- so the same-looking config behaved differently depending on
  which path ran. The column-wise version is kept, matching the legacy
  DropFeatures it was ported from. The uniform preset now goes through
  build_augmentation like every other mode.

Tests assert that the generator drives the draw, that the global RNG cannot
influence it, and that BGRL's two views repeat across runs while differing from
each other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The config told users to read logit_bias from model_summary.json, but nothing
ever wrote that file, so bias-free margins were unreachable in practice: the
value could only be transcribed by hand.

read_graph_head_bias pulls the output bias straight from a training
checkpoint -- the highest-numbered net.<i>.bias under the head prefix, which is
the final Linear of the head MLP -- and margin_attribution takes
logit_bias_checkpoint. An explicit logit_bias still overrides it. An
encoder-only checkpoint raises rather than silently attributing with the bias
left in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Niche ordering was re-derived inside the composition summary and reached only
that one table, so the attribution and spatial-statistics outputs in the same
report still came out in arbitrary label order.

Niches are now renumbered once, immediately after clustering: Niche 1 is first
in dendrogram order, Niche 2 second, and so on. The id carries the ordering, so
every downstream table, figure and export gets composition order by simply
sorting on it, and no ordering state has to be threaded through the pipeline.
Background keeps its own label and takes no position in the tree. The mapping
is recorded in pipeline_summary.json.

Consequently niche_composition_summary loses its order_by_composition flag, and
sort_by_niche_order is deleted -- it existed only to re-apply an ordering that
the ids now carry, and its axis/level parameters were never exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Attention weights on their own say what the model looked at, not which way that
pushed the decision, so reporting them alongside margins invited reading
attention as evidence. Interpretation is now expressed solely through the
weighted logit margins M[i,c] = A[i,c] * (l[i,c] - beta_c).

Removed: the attention_attribution plugin, niche_attention_summary and
niche_survival_attention_summary, the attention-derived NicheSummary fields
(weighted_scores, mean_scores, attention_present, attention_lift_present), and
the attention_lift column from margin attribution. The composite report figure
drops its predictions and attention-lift panels, keeping composition enrichment
beside niche abundance.

Attention itself is unchanged -- it still drives bag pooling and still appears
in the instance table. What is gone is the reporting of attention as an
interpretation in its own right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- resolve_encoder_cfg now calls resolve_categorical_embedding_config instead of
  reimplementing the same inference inline. The shared helper had been reachable
  only from tests, so the tested implementation was the dead one.
- Renamed the region-accumulation locals: total_losses, step_total_loss,
  mean_total_loss and _region_total_loss_buffer all held the region loss alone
  once the auxiliary terms were removed, so the names implied a composition
  that no longer exists. The rename immediately exposed a test that had been
  setting a stale attribute name.
- Dropped the `slow` marker. The six marked tests run in ~13s, so `-k "not
  slow"` bought nothing and left `make test` and `make test-full` as two
  commands that ran different subsets for no reason.
- EncoderConfig.categorical_embedding is typed Dict[str, Any] like its
  neighbours instead of a bare dict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs still described removed features, and the hyperparameter reference
handed users copy-pasteable commands that now fail: task.node_aux.*,
task.entropy_reg.*, task.loss_weights.*, set2set/attention pooling,
configs/model/heads/node.yaml, and the attention_attribution plugin.

requirements.txt is deleted. It duplicated pyproject.toml's dependency list and
was what two docs told users to install, which bypassed the extras and would
drift silently. Install instructions now point at the package with its extras.

Also records the survival preset, the per-class attention requirement, and the
two things deliberately retained: region accumulation (the final NSCLC runs use
hyperbatch_size 16) and the clustering algorithm's own parameter names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The macOS job failed building torch-sparse from source. The compile error is in
PyTorch's own header -- c10/util/strong_type.h specializes std::is_arithmetic,
which Xcode 26's libc++ marks _LIBCPP_NO_SPECIALIZATIONS and rejects outright --
so on that runner *any* pip-built torch C++ extension fails, not just this one.

torch-sparse was the only PyG extension routed through pip; pytorch-scatter was
already coming from the pyg conda channel and installed cleanly as a prebuilt
osx-arm64 binary. The pyg channel publishes pytorch-sparse 0.6.18
py312_torch_2.4.0_cpu for osx-arm64 too, so it now comes from there as well.
Nothing is dropped: macOS keeps full coverage, including the shadow_custom
sampler tests that need torch-sparse.

Also clears the warnings the run surfaced: actions/checkout and setup-python
bumped for the Node 20 runner deprecation, codecov-action to v5, and
setup-miniconda's renamed auto-activate input. setup-miniconda@v3 still targets
Node 20 upstream; nothing to do on our side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LRabuzin
LRabuzin merged commit d02e05b into main Aug 13, 2026
5 checks passed
@LRabuzin
LRabuzin deleted the cleanup/lean-package branch August 13, 2026 10:57
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