structboost 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {structboost-0.2.0 → structboost-0.3.0}/CHANGELOG.md +70 -0
  2. {structboost-0.2.0 → structboost-0.3.0}/CITATION.cff +2 -2
  3. {structboost-0.2.0 → structboost-0.3.0}/PKG-INFO +3 -3
  4. {structboost-0.2.0 → structboost-0.3.0}/README.md +1 -1
  5. {structboost-0.2.0 → structboost-0.3.0}/pyproject.toml +1 -1
  6. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_decoder.py +0 -6
  7. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_model.py +145 -460
  8. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_persistence.py +12 -6
  9. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_types.py +7 -39
  10. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_utils.py +10 -12
  11. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae.py +43 -105
  12. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae_diagnostics.py +1 -9
  13. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae_layer.py +1 -1
  14. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae_persistence.py +0 -13
  15. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae_transfer.py +4 -12
  16. {structboost-0.2.0 → structboost-0.3.0}/tests/test_covcache_precompute.py +1 -1
  17. {structboost-0.2.0 → structboost-0.3.0}/tests/test_rng_isolation.py +1 -1
  18. {structboost-0.2.0 → structboost-0.3.0}/tests/test_stability.py +66 -149
  19. {structboost-0.2.0 → structboost-0.3.0}/.gitignore +0 -0
  20. {structboost-0.2.0 → structboost-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  21. {structboost-0.2.0 → structboost-0.3.0}/CONTRIBUTING.md +0 -0
  22. {structboost-0.2.0 → structboost-0.3.0}/LICENSE +0 -0
  23. {structboost-0.2.0 → structboost-0.3.0}/conftest.py +0 -0
  24. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/__init__.py +0 -0
  25. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_annotation.py +0 -0
  26. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_boosting.py +0 -0
  27. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_encoder.py +0 -0
  28. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_explorer.py +0 -0
  29. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_io.py +0 -0
  30. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_plotting.py +0 -0
  31. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_simulation.py +0 -0
  32. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/_stability.py +0 -0
  33. {structboost-0.2.0 → structboost-0.3.0}/src/structboost/py.typed +0 -0
  34. {structboost-0.2.0 → structboost-0.3.0}/tests/test_allboost.py +0 -0
  35. {structboost-0.2.0 → structboost-0.3.0}/tests/test_annotation.py +0 -0
  36. {structboost-0.2.0 → structboost-0.3.0}/tests/test_bae_init.py +0 -0
  37. {structboost-0.2.0 → structboost-0.3.0}/tests/test_batch_integration.py +0 -0
  38. {structboost-0.2.0 → structboost-0.3.0}/tests/test_explorer.py +0 -0
  39. {structboost-0.2.0 → structboost-0.3.0}/tests/test_linear_ceiling.py +0 -0
  40. {structboost-0.2.0 → structboost-0.3.0}/tests/test_obs_encoding.py +0 -0
  41. {structboost-0.2.0 → structboost-0.3.0}/tests/test_public_api.py +0 -0
  42. {structboost-0.2.0 → structboost-0.3.0}/tests/test_resolve_mandatory.py +0 -0
  43. {structboost-0.2.0 → structboost-0.3.0}/tests/test_simulation.py +0 -0
@@ -3,6 +3,76 @@
3
3
  Releases follow [semantic versioning](https://semver.org). While the project is
4
4
  pre-1.0, a minor bump may break API.
5
5
 
6
+ ### [0.3.0] - 2026-08-11
7
+
8
+ **Breaking.** Five settings are gone and `BAE.stability_selection` has one mode
9
+ instead of two. Every removed option was off by default, so a fit that took the
10
+ defaults is unaffected: the encoder matrix is bitwise identical across a plain
11
+ fit, a batch-integrated fit, and both disentanglement methods. What changes is the
12
+ surface you have to reason about.
13
+
14
+ `boosting_nu` stays at `0.1`. Raising it to `0.3` was measured and deferred: at
15
+ the default `stepno=50` it matches `0.1`'s marker recovery three to four times
16
+ faster and wins outright on low-signal data, but then degrades if training
17
+ continues, and the training-MSE stopping rule cannot see it happening. The guide
18
+ records the numbers; the two changes belong together and will land together.
19
+
20
+ **Checkpoints written by 0.2.0 no longer load.** `restore_payload` splats the
21
+ stored config into `BAEConfig`, so a dropped field is an unexpected keyword
22
+ argument rather than a missing one. The format is bumped to 5 and the loader
23
+ refuses 4 by name. No migration is written: the method is under active
24
+ development and a checkpoint is cheap to regenerate, whereas a compatibility
25
+ shim for options that no longer exist is not.
26
+
27
+ Removed from `BAEConfig`:
28
+
29
+ - `decoder_dropout_rate` and `decoder_use_batch_norm`. Batch norm was already
30
+ documented as harmful (marker-recovery F1 0.59-0.73) because the boosting
31
+ target is computed with the decoder in eval mode and the update applied in
32
+ train mode. Dropout has exactly the same inconsistency and it was never written
33
+ down: the target comes from the full network, the update from a thinned one.
34
+ With both gone the decoder is a deterministic per-cell function, which is the
35
+ property `_compute_boosting_targets` has always relied on.
36
+ - `disentanglement_standardize`. Both `disentanglement` methods stay.
37
+ - `standardize_targets`. It was a genuine trade-off — higher selection precision,
38
+ roughly half the recall — but it is superseded by
39
+ `stability_selection(threshold=...)` -> `stable_encoder()` -> `apply_encoder()`,
40
+ which is the same trade with a dial that reports what it is doing. Two knobs for
41
+ one trade-off is worse than one.
42
+
43
+ Removed from `fit`:
44
+
45
+ - `balance_obs`, and with it the `sample_weights` parameter that threaded through
46
+ nine methods and both stability paths. Its own documentation conceded the
47
+ limit: the weights never reached the `allboost` fit, so gene selection stayed
48
+ unbalanced no matter what they were set to. The measured effect was modest (per
49
+ group reconstruction-MSE spread 0.231 -> 0.150) for a mechanism that promised
50
+ more than it delivered.
51
+
52
+ `BAE.stability_selection` is now iteration mode only; `mode`, `subsample_frac`,
53
+ `n_subsamples` and `n_iterations` are gone, and `fit(stability_selection=...)`
54
+ takes a bool. Iteration mode was already the default and measures the lower
55
+ false-discovery rate (0.26 against 0.31). The subsample path stays available as
56
+ the standalone `structboost.stability_selection`, which is where it belongs: it
57
+ resamples cells in the Meinshausen-Buhlmann scheme and works on an `allboost`
58
+ problem, so supervised users with no training loop to iterate over still have it.
59
+ The Meinshausen-Buhlmann bound it reports was measured to be violated by roughly
60
+ an order of magnitude when the targets come from a model fitted on the same
61
+ cells, and that warning moved with it.
62
+
63
+ `uns["bae"]["variance_explained"]` is now written by every fit. It used to appear
64
+ only when a covariate argument was passed, which left the quality workflow the
65
+ guide documents — compare it against `linear_ceiling` — raising `KeyError` on a
66
+ plain `fit(adata)`. The metric has nothing to do with covariates; only the
67
+ per-group breakdown does, and that stays behind the covariate guard.
68
+
69
+ Two fixes found along the way: the `allboost` example in the guide passed
70
+ `mode="standard"`, an argument `allboost` has never accepted, so it raised
71
+ `TypeError` as written. And `disentangle_boosting_targets` projects through the
72
+ origin, with no intercept — exact residualization only on centered targets. That
73
+ was masked by `disentanglement_standardize`, which centered them; with the flag
74
+ gone the assumption is documented instead.
75
+
6
76
  ### [0.2.0] - 2026-08-05
7
77
 
8
78
  The boosting loop got faster without changing what it computes. Measured
@@ -2,8 +2,8 @@ cff-version: 1.2.0
2
2
  message: "If you use structboost in your research, please cite it."
3
3
  title: "structboost"
4
4
  type: software
5
- version: "0.2.0"
6
- date-released: "2026-08-05"
5
+ version: "0.3.0"
6
+ date-released: "2026-08-11"
7
7
  license: MIT
8
8
  authors:
9
9
  - family-names: Brunn
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: structboost
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Boosting Autoencoders (BAE) and componentwise L2 boosting utilities for scverse-style workflows.
5
5
  Project-URL: Homepage, https://github.com/NiklasBrunn/structboost
6
6
  Project-URL: Documentation, https://niklasbrunn.github.io/structboost
@@ -153,7 +153,7 @@ is not identifiable: many different sparse gene sets reconstruct the data about
153
153
  equally well, and a fit returns one of them.
154
154
 
155
155
  ```python
156
- res = model.stability_selection(adata, mode="iteration")
156
+ res = model.stability_selection(adata)
157
157
  genes = [adata.var_names[res.stable_support[:, j]] for j in range(res.frequency.shape[1])]
158
158
  ```
159
159
 
@@ -78,7 +78,7 @@ is not identifiable: many different sparse gene sets reconstruct the data about
78
78
  equally well, and a fit returns one of them.
79
79
 
80
80
  ```python
81
- res = model.stability_selection(adata, mode="iteration")
81
+ res = model.stability_selection(adata)
82
82
  genes = [adata.var_names[res.stable_support[:, j]] for j in range(res.frequency.shape[1])]
83
83
  ```
84
84
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structboost"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Boosting Autoencoders (BAE) and componentwise L2 boosting utilities for scverse-style workflows."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -55,11 +55,7 @@ class BAEDecoder(nn.Module):
55
55
 
56
56
  for i in range(len(dims) - 1):
57
57
  layers.append(nn.Linear(dims[i], dims[i + 1]))
58
- if config.decoder_use_batch_norm:
59
- layers.append(nn.BatchNorm1d(dims[i + 1]))
60
58
  layers.append(self.activation)
61
- if config.decoder_dropout_rate > 0:
62
- layers.append(nn.Dropout(config.decoder_dropout_rate))
63
59
 
64
60
  self.hidden = nn.Sequential(*layers)
65
61
  # Final projection to output space
@@ -83,8 +79,6 @@ class BAEDecoder(nn.Module):
83
79
  for module in self.modules():
84
80
  if isinstance(module, nn.Linear):
85
81
  module.reset_parameters()
86
- elif isinstance(module, nn.BatchNorm1d):
87
- module.reset_parameters()
88
82
 
89
83
  def forward(self, z: torch.Tensor, covariates: torch.Tensor | None = None) -> torch.Tensor:
90
84
  """Decode latent representation to reconstruction.