Summary
run.log is the reproducibility artifact — it is what someone writing a Methods section reads. For any run using an inline -m override it currently reports both the parameter that was applied and the default that was not, with nothing distinguishing them.
Reproduce
protspace prepare -q 'family:"beta-lactamase"' \
-m 'pca2,umap2:n_neighbors=200;min_dist=0.4' \
-a all --stats --cluster-selection both -o out/
out/run.log:
## Projection
methods: pca2, umap2:min_dist=0.4;n_neighbors=200 <- applied
similarity: False
metric: euclidean
random_state: 42
n_neighbors: 25 <- NOT applied
min_dist: 0.1 <- NOT applied
The bundle disagrees with its own log. projections_metadata.info_json records n_neighbors: 200, min_dist: 0.4 — the standalone lines are the globals that lost.
A reader reconstructing the run from n_neighbors: 25 gets a different projection.
Root cause
apps/protspace/src/protspace/cli/prepare.py (~702-706):
lines += ["", "## Projection"]
lines.append(f"methods: {', '.join(str(m) for m in pipeline_config.methods)}")
lines.append(f"similarity: {similarity}")
for key, val in rp.items(): # rp = the GLOBAL ReducerParams
lines.append(f"{key}: {val}")
Inline overrides are parsed into MethodSpec.overrides and merged at pipeline.py as effective_params = {**global_params, **spec.overrides_dict}, then applied via _run_with_overridden_config — which restores the previous config in a finally. So the override provably cannot survive into the object _write_run_log serializes (asdict(pipeline_config.reducer_params)). Confirmed: cfg.reducer_params.n_neighbors == 25 after an override run.
Suggested fix
The resolved values already exist. <Reducer>.get_params() (e.g. UMAPReducer.get_params) feeds reduction["info"], which is serialized to info_json — the same object the bundle records. Printing those, per projection, is both correct and strictly more informative, because each reducer reports only the parameters it actually consumed (a UMAP run would stop printing perplexity/learning_rate, which the current log lists misleadingly).
## Projection
methods: pca2, umap2:min_dist=0.4;n_neighbors=200
similarity: False
### Applied (per projection)
ProtT5 — PCA 2: n_components=2, svd_solver=arpack, random_state=42
ProtT5 — UMAP 2: n_components=2, n_neighbors=200, min_dist=0.4, metric=euclidean, random_state=42
### Global defaults (overridden per projection where shown above)
n_neighbors: 25
min_dist: 0.1
...
Roughly: expose self.reductions on ReductionPipeline, keep the instance in prepare.py, pass it to _write_run_log. Two caveats found while checking: under --stats, route_faithfulness_to_metadata injects quality into red["info"] (filter it), and red["ids"] is also set under --stats (read only name/info). Cached projections are safe — the cache key includes effective_params, so a hit implies identical parameters.
Failing that, labelling the block defaults (may be overridden per method) would at least stop it asserting something false.
Two related ambiguities found while tracing this
1. n_neighbors — the effective value is 25. DimensionReductionConfig defaults to 15 and ReducerParams to 25, which has caused confusion. The CLI passes n_neighbors as an explicit keyword all the way to DimensionReductionConfig(n_components=dims, **filtered_config), so the dataclass default is never consulted. 25 is what reaches the reducer from any CLI run; the 15 is only live when DimensionReductionConfig is constructed directly without the kwarg. Documenting this would settle it.
2. eps has three defaults, and the Python API silently differs from the CLI. ReducerParams.eps = 1e-6, DimensionReductionConfig.eps = 1e-3, CLI eps = 1e-3. A CLI run gets 1e-3; a Python-API caller using PipelineConfig(...) with the default_factory=ReducerParams gets 1e-6 — a 1000× difference in MDS convergence tolerance, with no warning. Aligning ReducerParams.eps to 1e-3 breaks no test (none asserts either default).
Context
Found while preparing datasets for a publication, where the projection parameters go into a Methods section verbatim. Related: PRs #430 and #431 fix two other cases of the same underlying shape — an operation that did not happen reporting as though it did.
Summary
run.logis the reproducibility artifact — it is what someone writing a Methods section reads. For any run using an inline-moverride it currently reports both the parameter that was applied and the default that was not, with nothing distinguishing them.Reproduce
out/run.log:The bundle disagrees with its own log.
projections_metadata.info_jsonrecordsn_neighbors: 200,min_dist: 0.4— the standalone lines are the globals that lost.A reader reconstructing the run from
n_neighbors: 25gets a different projection.Root cause
apps/protspace/src/protspace/cli/prepare.py(~702-706):Inline overrides are parsed into
MethodSpec.overridesand merged atpipeline.pyaseffective_params = {**global_params, **spec.overrides_dict}, then applied via_run_with_overridden_config— which restores the previous config in afinally. So the override provably cannot survive into the object_write_run_logserializes (asdict(pipeline_config.reducer_params)). Confirmed:cfg.reducer_params.n_neighbors == 25after an override run.Suggested fix
The resolved values already exist.
<Reducer>.get_params()(e.g.UMAPReducer.get_params) feedsreduction["info"], which is serialized toinfo_json— the same object the bundle records. Printing those, per projection, is both correct and strictly more informative, because each reducer reports only the parameters it actually consumed (a UMAP run would stop printingperplexity/learning_rate, which the current log lists misleadingly).Roughly: expose
self.reductionsonReductionPipeline, keep the instance inprepare.py, pass it to_write_run_log. Two caveats found while checking: under--stats,route_faithfulness_to_metadatainjectsqualityintored["info"](filter it), andred["ids"]is also set under--stats(read onlyname/info). Cached projections are safe — the cache key includeseffective_params, so a hit implies identical parameters.Failing that, labelling the block
defaults (may be overridden per method)would at least stop it asserting something false.Two related ambiguities found while tracing this
1.
n_neighbors— the effective value is 25.DimensionReductionConfigdefaults to 15 andReducerParamsto 25, which has caused confusion. The CLI passesn_neighborsas an explicit keyword all the way toDimensionReductionConfig(n_components=dims, **filtered_config), so the dataclass default is never consulted. 25 is what reaches the reducer from any CLI run; the 15 is only live whenDimensionReductionConfigis constructed directly without the kwarg. Documenting this would settle it.2.
epshas three defaults, and the Python API silently differs from the CLI.ReducerParams.eps = 1e-6,DimensionReductionConfig.eps = 1e-3, CLIeps = 1e-3. A CLI run gets 1e-3; a Python-API caller usingPipelineConfig(...)with thedefault_factory=ReducerParamsgets 1e-6 — a 1000× difference in MDS convergence tolerance, with no warning. AligningReducerParams.epsto1e-3breaks no test (none asserts either default).Context
Found while preparing datasets for a publication, where the projection parameters go into a Methods section verbatim. Related: PRs #430 and #431 fix two other cases of the same underlying shape — an operation that did not happen reporting as though it did.