# Troubleshooting & Known Failure Modes

Each entry gives the symptom you will actually see, the cause, and the
workaround.

## Installation

### `conda env create` fails on `deblur` → `sortmerna`

```
package deblur-1.1.1 requires sortmerna 2.0, but none of the providers
can be installed
```

**Cause.** A defect in the upstream QIIME 2 2026.7 `linux-64` distribution file:
it pins `zlib=1.3.2`, while every `sortmerna` 2.0 build in bioconda requires
`zlib >=1.2.11,<1.3.0a0`. The two cannot be satisfied together.

**Workaround.** `deblur` is not used anywhere in this tutorial. Download the
environment file, delete the `deblur`, `q2-deblur` and `sortmerna` lines, and
create the environment from your edited copy.

### `qiime gglasso pca` fails with `TemplateNotFound`

**Cause.** A non-editable install built before the fix did not ship
`_pca/assets/index.html` — it was missing from `package_data`.

**Workaround.** Reinstall from a current checkout, or use `pip install -e .`.

### pip silently breaks a working environment

**Cause.** pip cannot see conda's pins and will install a wheel over
`numpy=2.4.2`.

**Workaround.** Always `pip install --no-deps` inside a QIIME 2 environment.
Check with `conda list numpy pandas scipy` afterwards.

## q2-gglasso

### `--p-rank` always fails

```
NotImplementedError: Explicit rank selection requires a GGLasso version that
exposes an explicit low-rank rank parameter...
```

**Cause.** No released GGLasso (up to and including 0.3.0) can fix the rank of
the low-rank component. It exposes only the continuous `mu1` penalty and reports
the achieved rank as an *output*. The parameter is registered but guarded so it
raises rather than being silently ignored.

**Workaround.** Size the low-rank block through `mu1`: a larger `mu1` gives a
smaller rank. Scout a small `mu1` grid and read the achieved rank out of the
solution — see [Choosing the Latent Rank](../04_highdim_atacama/03_slr_ranks.md).
Explicit rank selection becomes available when GGLasso PR #50
(`fix_latent_rank`) is merged and released.

### `qiime gglasso pca` crashes on a solution that has no low-rank part

**Cause.** `pca` reads `solution/lowrank_`, which only exists when the problem
was solved with `--p-latent True`.

**Workaround.** Run `solve-problem` with `--p-latent` first. A sparse-only (SGL)
solution cannot be used with `pca`.

### `pca` refuses: `n_components (3) exceeds the rank of the low-rank component (2)`

**Cause.** `--p-n-components` defaults to 3, but the achieved rank of the
low-rank component falls as `mu1` rises, and you cannot take more components
than there are. On the 300-ASV Atacama data the map is μ = 15 → rank 2,
μ = 10 → rank 5, μ = 7.5 → rank 10. The default therefore breaks on the
*headline* fit at μ = 15.

**Workaround.** Pass `--p-n-components 2` at μ = 15, or in a script derive it
from the solution rather than hardcoding:

```python
rank = np.linalg.matrix_rank(np.asarray(root["solution/lowrank_"]), tol=1e-8)
n_components = min(3, rank)
```

This is a guard doing its job, not a bug: earlier versions accepted the
mismatch and failed later inside `utils.PCA` with an unrelated matmul error.

### The taxonomy join returns all-`NaN` instead of raising

**Cause.** The transformed table was built with `--p-no-keep-original-id`, so its
features are `ASV-1` … `ASV-p` positional labels, while the taxonomy artifact is
keyed on 32-character hexadecimal feature IDs. `df.join(tax)` finds no overlap
and fills with `NaN`, and `.reindex()` does the same. Nothing raises, so the
failure reads as "this dataset has no taxonomy".

**Workaround.** Rebuild with `--p-keep-original-id` (the default). Do not try to
map `ASV-k` back through an abundance ranking — `ASV-k` is assigned by position,
and ties in total abundance make the mapping ambiguous. On the 300-ASV table,
209 of 300 features share a total-abundance value and are therefore at risk.
Measured against the plugin's own ordering, a rank-based mapping placed 146 of
300 features differently. Within a tie group a feature can still land correctly
by coincidence, which is why the measured count is below the at-risk count — but
you cannot tell which ones did. See
[Interpretation](../04_highdim_atacama/06_interpretation.md).

**How to notice.** Assert rather than eyeball:
`assert tax.reindex(features)["Taxon"].notna().all()`.

### `qiime gglasso pca` crashes with `AttributeError` on metadata

**Cause.** `--m-sample-metadata-file` is optional in the signature, but the
action dereferences it unconditionally.

**Workaround.** Always pass `--m-sample-metadata-file`. Treat it as required.

### `transform-features` demands a taxonomy I don't have

**Cause.** `--i-taxonomy` is a required input but the function body never uses
it. This is a registration wart, not a real dependency.

**Workaround.** Pass any valid `FeatureData[Taxonomy]` artifact; its contents do
not affect the result.

### `build-groups` output cannot be fed to `solve-problem`

**Cause.** `build-groups` emits a `TensorData` artifact, but `solve-problem`
takes `group_array` as a `List[Int]` parameter. They do not chain through the
QIIME 2 type system.

**Workaround.** Export the artifact and pass the group index explicitly as
`--p-group-array`. See
[Multiple Graphical Lasso](../02_lowdim_gglasso/06_multiple_graphical_lasso.md).

### The `.qzv` opens as a blank white page

**Cause.** A bokeh version mismatch: the visualizer emits a bokeh 3 document
while the page loads a bokeh 2.4.3 runtime. There is no Python traceback — the
failure is entirely client-side.

**Workaround.** Fixed in current q2-gglasso, which injects version-matched bokeh
resources instead of hardcoding a CDN URL. If you see it, you are on an older
build. Confirm by unzipping the `.qzv` and grepping `index.html` for `bokeh-2.`.

### Model selection ran even though I only wanted a single fit

**Cause.** The solver performs model selection whenever at least one grid holds
more than one value, so a single fit requires `lambda1` *and* `lambda2` — plus
`mu1` on a latent problem — to each resolve to exactly one value. Leaving
`--p-lambda2-min` / `--p-lambda2-max` unset does not count as one value: the
solver expands an unset pair of bounds to the 5-point default
`np.logspace(-1, -4, 5)` and warns `Default values for lambda2 have been used.`.
That alone switches model selection on, so a lone
`--p-lambda1-min 0.5 --p-lambda1-max 0.5` still gives you a path. (The
single-instance solver does not otherwise use `lambda2`; it only decides this
branch.)

**Workaround.** Pin `λ₂` explicitly — pass the same value to
`--p-lambda2-min` and `--p-lambda2-max` — and do the same for `--p-mu1-min` /
`--p-mu1-max` on a latent problem. Check the result by looking for a
`modelselect_stats` group in the solution: a single fit has none, and `summarize`
reduces its statistics tab accordingly.

## q2-classo

### `--p-cv--nlam` has two dashes — is that a typo?

**No.** The parameter really was named `cv__nlam` with a double underscore, which
QIIME 2 renders as `--p-cv--nlam`.

It is now spelled `cv_nlam` (`--p-cv-nlam`). The old spelling still works but
emits a `DeprecationWarning`. Passing both with *different* values raises — but
only if `cv_nlam` was changed from its registered default of `100`. QIIME 2 fills
that default in whether or not you typed it, so `100` is indistinguishable from
"not given" and is treated as unset: `--p-cv-nlam 100 --p-cv--nlam 50` does *not*
raise — it silently uses 50.

### `qiime classo classify --p-concomitant True` is rejected

**Cause.** The concomitant formulation is not available for classification.
`classify` has no `concomitant` parameter, and the underlying solver forces
`formulation.concomitant = False` for classification problems.

**Workaround.** Use the Huber hinge loss: `--p-huber True`, tuned via `--p-rho`.
See [Concomitant Formulation](../03_lowdim_classo/05_advanced/01_concomitant_formulation.md).

### `qiime classo --help` lists two actions called "regress"

**Cause.** `classify` was registered with `name="regress"`.

**Workaround.** Fixed in current q2-classo. If you see it, your install predates
the fix — the action still works, it is only mislabelled.

### Every plot pane in the `summarize` `.qzv` is blank

**Cause.** The plot-writing calls were commented out while the templates still
referenced 16 `<iframe>` files. No error was raised. The panes rendered empty.

**Workaround.** Fixed in current q2-classo. A regression test now asserts that
every iframe the templates reference has something that writes it.

### `NameError: name 'xGraph' is not defined`

**Cause.** A typo (`xGrpah`) on the non-log-scale branch, reachable via
`--p-cv-logscale False`.

**Workaround.** Fixed in current q2-classo.

## Both plugins

### A misspelled enum value fails at runtime, not on the command line

```
ValueError: Unknown transformation name, use clr and not 'clrr'
```

**Cause.** Neither plugin declares `Choices()` on its string parameters, so QIIME 2
accepts any string and the check happens inside the function. Affects
`--p-transformation` (`clr`/`mclr`), `--p-method` (`scaled`/`unscaled`),
`--p-reg` (`GGL`/`FGL`), `--p-path-scale` (`log`/`linear`) and the various
`--p-*-numerical-method` parameters.

**Workaround.** Check spelling against the
[parameter reference](02_gglasso_parameters.md). The `*_numerical_method`
parameters default to the literal string `"not specified"`, so a typo there is
silently accepted.

### The CLR transform produces obviously wrong values

**Cause (historical).** Zero imputation used a chained pandas assignment that
becomes a silent no-op under Copy-on-Write, so no pseudo count was ever added.
The failure surfaced far from its origin, as
`AssertionError: Add pseudo count before using clr`.

**Workaround.** Fixed in current q2-gglasso.
