Troubleshooting
Who this page is for. Anyone whose Tether run did something other than what they
expected — including the runs that looked fine. Entries are keyed by what you saw, not
by which module owns the problem, so scan the headings or paste your error text into your
browser's find bar: every message quoted here is copied verbatim from the code that raises it
— Tether's own src/ unless the entry says otherwise — and each entry names the module
responsible. Any term that reads as jargon is defined in the
Glossary.
The silent failures come first, because nothing will tell you about them. They are marked with a Signature block: something you can check on your own store to confirm or rule the diagnosis out.
Data and analysis failures
Every efficiency is mirrored when donor-side is backwards
Symptom. The run succeeded. Molecule counts look right. But the population histogram sits where you expected its mirror image — a low-FRET construct reads high, a high-FRET construct reads low — and every trace's donor and acceptor look swapped.
Cause. --donor-side says which horizontal half of the dual-view frame is the donor
channel. It defaults to left, and nothing in Tether detects the true orientation. The
CLI takes it as free text (tether.cli, metavar="{left,right}" rather than choices=), so
a typo is rejected downstream by ExtractOptions.__post_init__:
tether extract: donor_side must be 'left' or 'right', got 'middle'
…but a valid-but-wrong value is accepted in silence. All donor_side does is choose which
crop tuple is returned by _half_split_geometry in tether.project.extract, and detection
is donor-anchored — so the swapped run still
colocalizes and still registers. Since apparent E is A / (D + A), swapping the two channels
gives exactly 1 − E.
The molecule count is not a control here. Donor-anchoring makes the molecule set exactly the detections in the half you labelled donor, so swapping the label swaps which half is counted. The two counts agree only when both halves detect the same number of spots — true of a symmetric synthetic movie, and not something to rely on with real data, where the acceptor-dark and low-FRET population ADR-0015 exists to preserve is precisely the set present in one half and not the other.
Two related traps:
- With
--tmap,--donor-sideis ignored: the split comes from the.tmap's own crop geometry. No warning is emitted when both flags are passed. - The
tether batchCLI cannot set it at all.run_batch()does take anextract_options=argument and forwards it, buttether.cli._run_batchnever builds one — so every batch run takes theExtractOptionsdefaults. Dumped from a real batch run's/settings/extraction.attrs["profile_json"]:"donor_side": "left","detection_mode": "wavelet","pair_tol": 2.0,"rms_gate": 0.5.
Signature. Run the two extractions side by side on one movie and compare. The relevant store fields, dumped from a real pair of runs on the same input — a symmetric 3-spot synthetic movie, which is why the two counts match:
Field --donor-side left--donor-side right/settings/extraction.attrs["profile_json"]→donor_side"left""right"/movies/table→donor_crop(1-based[y1, x1, y2, x2])[1, 1, 64, 48][1, 49, 64, 96]/movies/table→acceptor_crop[1, 49, 64, 96][1, 1, 64, 48]mean apparent E 0.19860.8014Both runs printed the same
Extracted 3 molecule(s) -> …line and both exited0.
Remedy. Confirm the orientation before you trust a dataset, two ways:
- Against the instrument.
donor_cropin/movies/tableis a 1-based[y1, x1, y2, x2]box; check that its half of the frame is the one the splitter sends through the donor emission filter. - Against a trace. Open a molecule that photobleaches. At the acceptor-bleach step the
acceptor channel drops to background and the donor channel rises — the dequenching step
Tether's own γ estimator measures as
ΔI_A / ΔI_D(tether.fret.gamma). If in your traces the channel labelled acceptor is the one that rises, the sides are swapped.
If it was wrong, re-extract. There is no in-place fix and nothing to flip after the fact: the crops, the registration map, the molecule keys and the tags are all written from the chosen split.
The batch finished "ok" but nothing was idealized
Symptom.
Batch run: 1 movie(s), 1 ok, 0 failed (policy=warn, idealize=on)
ok warp_010 extract=done correct=done idealize=deferred
Exit code 0. /idealization is empty.
Cause. tether batch probes the sidecar once at startup. When no interpreter resolves,
and defer_if_unavailable is on (the default), the idealize stage is recorded as
deferred rather than failed — and deferred is not a failure, so n_failed stays 0
and the CLI returns 0 (tether.project.batch). The reason is logged at INFO, below
Python's last-resort handler threshold, so it never reaches your console; the word
deferred in the stage list is the only tell.
Signature.
/settings/batch.attrs["idealize_status"] == "deferred",/idealizationempty, and the JSONL log carries the explanation:
text {"movie": "(batch)", "stage": "idealize", "status": "deferred", "detail": "sidecar unavailable \u2014 idealization deferred: no sidecar interpreter: pass sidecar_python= or set TETHER_SIDECAR_PYTHON to a Python in an env built from sidecar/conda-lock.yml with tMAVEN installed"}That is the byte-for-byte line as it lands on disk.
BatchLog.eventwritesjson.dumps(record)at the defaultensure_ascii=True, so every non-ASCII character in a detail string is escaped: the em dash the code emits is stored as\u2014. Grep for an ASCII-only substring —grep '"status": "deferred"' <out-dir>/batch-log.jsonl, orgrep 'idealization deferred' …— never for the rendered\u2014.
Remedy. Fix the sidecar (see
Idealization cannot find the sidecar interpreter)
and re-run the same batch — extract and correct report skipped from their checkpoints and
only idealization runs. To make the condition loud in the first place, pass --no-defer,
which fails each movie's idealize stage in isolation and exits 1:
warp_010 · idealize: failed — no sidecar interpreter: pass sidecar_python= or set TETHER_SIDECAR_PYTHON to a Python in an env built from sidecar/conda-lock.yml with tMAVEN installed
Batch run: 1 movie(s), 0 ok, 1 failed (policy=warn, idealize=on)
FAIL warp_010 extract=done correct=done idealize=failed(no sidecar interpreter: pass sidecar_python= or set TETHER_SIDECAR_PYTHON to a Python in an env built from sidecar/conda-lock.yml with tMAVEN installed)
The first line is BatchLog.event's console record — it joins movie and stage with a middle
dot (·) and the error with an em dash (—). The last two lines are
BatchSummary.format_report, which appends the error text only because the stage is failed.
The batch says correct=done but no corrections were applied
Symptom. The run reports correct=done and exit 0, but α and γ are NaN on every
molecule and your efficiencies are uncorrected.
Cause. This is the documented, expected outcome for an acquisition with no clean
acceptor-bleach step. compute_leakage_alpha withholds α below
DEFAULT_MIN_QUALIFYING_TRACES = 10 qualifying donor-only tails rather than fabricating one;
run_correct_stage then skips γ entirely (it cannot run without α) and stamps every molecule
apparent-E (corrections unavailable). Nothing is wrong with the code — but the batch
console never says so, because the explanation lives in the stage detail string, which the
end-of-run report does not print.
Signature. Three store reads, dumped from a real run:
text /settings/correction : {'apparent_e_only': False, 'n_molecules': 7, 'n_corrected': 0, 'n_manual': 0, 'n_apparent': 7, 'total_failure': True, ...} /settings/leakage : {'alpha': nan, 'withheld': True, 'n_qualifying': 0, 'min_qualifying_traces': 10, ...} /settings/gamma : ABSENT
total_failure=Truewithapparent_e_only=Falseis the unambiguous "correction failed" signature;total_failure=Truewithapparent_e_only=Trueis the deliberate user toggle. The absence of/settings/gammais itself diagnostic — γ never ran./molecules.correction_methodreadsapparent-E (corrections unavailable)withcorrection_confidence = 0.0. The stage detail string spells it out. The line below is copied byte-for-byte from a realbatch-log.jsonl, so theαthatrun_correct_stagebuilds appears in its escaped on-disk form (ensure_asciiagain — see the deferral entry above):
text {"movie": "warp16b", "stage": "correct", "status": "done", "detail": "pb 0D/0A; \u03b1 withheld; apparent-E fallback (7 mol)"}
Remedy. There is no flag that makes the estimators succeed on data that lacks the step they measure. Realistically there are two options: accept apparent E (which is what every population plot shows anyway — see apparent E), or acquire traces long enough to capture acceptor photobleaching in ≥ 10 molecules.
The correction overrides are a provenance tool, not a third option. compute_corrected_fret
accepts alpha_override / gamma_override (tether.project.correct), and in this state
they must be supplied together: the gate is
np.isfinite(eff_alpha) and np.isfinite(eff_gamma) and eff_gamma > 0.0, an override replaces
only the factor it names, and here the other factor is still the stored NaN — so passing
only one leaves every molecule stamped apparent-E (corrections unavailable) exactly as
before. (Where the other factor was estimated, one override is enough: it combines with the
stored value and the row is stamped manual.) Even when both are supplied, the visible
effect is provenance only: the applied factors are persisted to /molecules.alpha /
.gamma, /settings/correction records the overrides, and correction_method becomes
manual — but plots and CSV exports still show apparent E, because
tether.fret.efficiency.corrected_fret has no call sites outside the tether.fret
re-export (tether.analysis.histogram, .cloud, tether.gui.trace_dock,
tether.project.export and tether.ml.features all call apparent_fret).
Note the related non-silence: a molecule that fails γ's own gates silently receives the dataset median γ and is still stamped
correctedwith confidence1.0. Only the population count/settings/gamma.attrs["n_fallback"]reveals how many; there is no per-row marker.
tether batch --overwrite changed nothing
Symptom. You fixed a parameter, re-ran the batch with --overwrite, and got:
ok warp_010 extract=skipped correct=skipped idealize=deferred
The results are the old ones, reported as ok, exit 0.
Cause. For a normal completed extraction, _do_extract in
tether.project.batch finds the /settings/extraction checkpoint and returns skipped;
--overwrite is not a general "recompute completed work" switch. _do_correct likewise
skips a completed /settings/correction.
There is one deliberate extraction exception. Under --policy fail, the runner reads the
saved registration_rms_px and effective rms_gate from the extraction profile. A saved
over-gate result remains failed on every resume. Passing --overwrite --policy fail
re-extracts that rejected checkpoint and applies the gate to the new result. An in-gate
completed extraction still skips, even with --overwrite. Batch claims every
--overwrite --policy fail destination lock before testing whether its project file
exists or reading a checkpoint; a lock held by any writer, including another thread in
the same process, reports the movie as locked and is neither refreshed nor released by
batch. A process-local per-destination reservation also rejects same-identity access
from another local thread. Both claims remain held while batch creates, skips, or
re-extracts the destination.
Signature.
extract=skipped/correct=skippedin the report, and/settings/extraction.attrs["profile_json"]still holding the old parameters.
Remedy. For a policy-rejected project, keep the inspectable .tether and re-run with
--overwrite --policy fail; downstream stages proceed only if the new extraction passes
the gate. For a normal completed checkpoint whose parameters must change, preserve or move
the old project as needed, then use tether extract --overwrite for that movie — the
batch runner intentionally does not recompute an accepted completed extraction.
Deleting is not enough if the parameter you fixed is an extract tunable
(--donor-side, --detection-mode, --detection-threshold, --min-separation, --window,
--pair-tol, --rms-gate). tether batch exposes none of them and never builds an
ExtractOptions, so a clean re-run silently re-applies the same defaults
(see every efficiency is mirrored).
Run those movies through tether extract --overwrite one at a time, or drive
run_batch(..., extract_options=ExtractOptions(...)) from the library, which does forward
them.
Registration passed the gate with a suspiciously perfect residual
Symptom. /calibration reports an RMS residual at machine epsilon (1e-14 or smaller)
and low_confidence: False, on a movie you know is not perfectly registered.
Cause. The gate is vacuous whenever the control points exactly determine the fit — the
residual of an exactly-determined least-squares fit is zero by construction, so it measures
nothing about the mapping. _fit_both in tether.imaging.calibrate uses a degree-2
polynomial at _MIN_POLY_POINTS = 6 pairs or more and a 4-DOF similarity below that, which
gives the condition two arms:
- 2 pairs,
degree: 1— 4 unknowns, 4 equations. - 6 pairs,
degree: 2— the polynomial basis[1, x, y, xy, x², y²]has 6 coefficients per axis (poly_basis_deg2intether.imaging.register), so a 6×6 system.
At 3–5 pairs the similarity is over-determined and the residual is real — the gate works
normally there, and a low_confidence: True on a 4-point fit is a genuine warning, not an
artefact. (What it cannot tell you is whether the map needs degree 2: a 3–5-point residual
only measures how well a similarity fits.) Separately, pair_control_points silently
drops outlier pairs beyond --pair-tol (default 2 px), which is how a movie with plenty
of spots lands on 6 surviving pairs; only fewer than 2 is a hard error.
Signature. In
/calibration/<cal-id>.attrs: anrms_residualat machine epsilon (~1e-14) withn_control_pointsequal to the fit's degrees of freedom — 2 atdegree: 1, or 6 atdegree: 2. Both are verified below on synthetic movies whose true donor→acceptor mapping is a cubic warp, i.e. deliberately not representable by either fit, at the default--rms-gate:
text vacuous : {'rms_residual': 4.24e-14, 'n_control_points': 6, 'gate_px': 0.5, 'degree': 2, 'low_confidence': False, 'source': 'native'} genuine : {'rms_residual': 0.502, 'n_control_points': 15, 'gate_px': 0.5, 'degree': 2, 'low_confidence': True, 'source': 'native'}The first row is the trap: a badly mis-mapped movie reporting a perfect fit, and
degree: 2alone does not clear it.
Remedy. Treat the RMS number as meaningful only when n_control_points is comfortably
above the fit's DOF — in practice 7 or more, since that is where the degree-2 fit starts being
over-determined. Get more pairs: raise --pair-tol so genuine pairs are not discarded, lower
--min-separation, or try --detection-mode intensity with a lower --detection-threshold.
Importing a Deep-LASI map with --tmap does not help here: the imported path skips pairing
entirely, so the residual is left NaN and the gate is not merely vacuous but absent (see
registration was flagged low-confidence). Tether
also has no .tmap writer — tether.imaging.register.read_tmap is the only entry point —
so there is no way to extract a bead movie in Tether and export a calibration map from it.
Extraction found no molecules
Symptom.
tether extract: registration failed: only 0 control-point pair(s) matched (need >= 2). Detected 0 donor / 0 acceptor spots; check the channel split, detection sensitivity (--min-separation) or pairing tolerance (--pair-tol).
Exit code 1. Raised by tether.project.extract.extract_movie when fewer than 2 control
points survive pairing (the native path only — an imported --tmap skips pairing entirely).
Cause and remedy, in the order worth trying. Read the two counts in the message first: they tell you which half of the problem you have.
Detected 0 donor / 0 acceptor— the detector found nothing. Check that the movie really is a side-by-side dual-channel frame and that--donor-sidematches, then loosen detection:--detection-mode intensitywith a lower--detection-threshold(e.g.0.2; it is a fraction of the detection-image maximum, not an absolute count), or a smaller--min-separation.- Spots were detected but pairs did not match — the two halves are misaligned by more
than
--pair-tol(default 2 px). Raise--pair-tol, switch to--prealign similarity, or import the real calibration with--tmap.
Defaults worth knowing: --detection-threshold is ignored by the wavelet mode, and
defaults to 0.5 for intensity and 0.98 for bandpass. Omitting --min-separation
gives each mode its own faithful default (wavelet 8 px, intensity/bandpass 3 px;
ADR-0022).
Extraction found far fewer, or far more, molecules than expected
Symptom. Extracted N molecule(s) where N is wildly off — a handful on a crowded
field, or thousands on a sparse one. No error is raised; molecule count is not gated.
Cause. Detection sensitivity. There is no absolute-count check anywhere in
tether.project.extract, and remember that colocalization is
donor-anchored — every in-frame donor spot
becomes a molecule, whether or not an acceptor was independently detected, so the count
tracks donor detections alone. Molecules whose full 21 × 21 aperture would fall outside
the frame in either channel are dropped rather than zero-filled, which also trims edge spots.
Signature.
/settings/extraction.attrs["profile_json"]records every tunable verbatim (detection_mode,detection_threshold,min_separation,window,pair_tol,coloc_distance,rms_gate,donor_side). Compare it against a run you trust; the parameters, not the code, will differ.
Remedy. Adjust --detection-mode / --detection-threshold / --min-separation and
re-extract. There is no post-hoc re-detection: detection happens once, at extraction.
The movie will not load
All three of these arrive wrapped by extract_movie as
could not extract from <name>: …, exit 1 — that wrapper catches any non-ExtractionError,
so the inner text is passed through unchanged from whatever raised it. The first two come from
tether.io.movie; the third is not a Tether message — it is tifffile's own
TiffFileError, raised inside MovieReader.__init__ when tifffile.TiffFile opens the path,
so grepping src/ for it finds nothing.
| Message | What it means | Remedy |
|---|---|---|
… is not memory-mappable; the lazy reader needs an uncompressed, contiguous TIFF (the reference movie format, PRD Appendix A). |
A compressed or tiled TIFF. This is the most common real-world case. | Re-save uncompressed — in ImageJ, Save As → Tiff with no compression. |
…: expected a 3-D movie (frames, height, width), got shape (64, 64) (axes 'YX'). |
A single-page TIFF — often a max-projection saved instead of the stack. | Point Tether at the stack. |
not a TIFF file: header=b'this' |
The file is not a TIFF at all: renamed, corrupt, or a copy that never finished. | Re-copy from the acquisition machine. |
Two more from the same family, raised by tether.project.extract:
movie changed during extraction: <path>— the file's size or mtime moved mid-run (cloud sync, or the acquisition is still writing). Copy the movie locally, or wait for the acquisition to finish.movie too narrow (1 px) to split into two channels— the frame cannot be halved. Note that a movie of odd width silently drops its trailing column so both halves match.
tether extract rejects an option value
Every operator-actionable tunable is validated in ExtractOptions.__post_init__
(tether.project.extract) so bad input fails with a clean one-line message on stderr and
exit 1 — never a traceback. The ones you are most likely to meet:
| Message | Fix |
|---|---|
donor_side must be 'left' or 'right', got 'middle' |
use left or right |
detection_mode must be one of ('wavelet', 'intensity', 'bandpass'), got 'gaussian' |
pick one of the three |
detection_threshold must be in [0, 1) (a fraction of the detection-image max), got 1.5 |
it is a fraction, not a count |
min_separation must be > 0, got -1.0 |
omit it to get each mode's faithful default |
window must be a positive odd integer, got 20 |
the aperture side must be odd (default 21) |
ring (2*8.0) does not fit in a 11px window |
enlarge --window |
pair_tol must be > 0, got 0.0 |
also applies to coloc_distance and rms_gate |
output exists: <path> (use overwrite=True / --overwrite) |
pass --overwrite (the message names the library keyword first) |
--detection-mode/--detection-threshold cannot be combined with --tdat (the .tdat supplies the detection mode); pass one or the other |
drop one side |
Ordering gotcha. Option validation runs before any file-existence check, and
movie not foundfires beforetmap not found/tdat not found. Sotether extract nope.tif -o x --donor-side middlereports the donor-side error, not the missing movie.
Registration was flagged low-confidence
Symptom. On stderr, with the run still succeeding and exit code 0:
warning: registration RMS 0.368 px exceeds the 0.2 px gate (7 control points); molecules tagged 'low-confidence-registration'.
Extracted 5 molecule(s) -> …\warp2.tether
The library-level form of the same verdict (tether.imaging.calibrate) reads:
registration RMS residual 0.368 px exceeds the 0.200 px gate (channels 0->1, 7 control points, source native): calibration flagged low-confidence; molecules will be tagged 'low-confidence-registration'.
Cause. The fit's RMS residual exceeded --rms-gate (default
DEFAULT_RMS_GATE_PX = 0.5 px). Under the default warn policy this is accept-with-flag:
molecules are tagged, never dropped.
Signature.
/calibration/<cal-id>.attrscarrieslow_confidence: Truealongsiderms_residual,n_control_points,gate_px,degreeandsource; every row of/molecules.tagscarrieslow-confidence-registration.In
tether batchthe warning disappears entirely — the end-of-run report prints the stage status only, and the flag survives only in the JSONLdetail("…; low-confidence registration (flagged)"). Run with--policy failto make an over-gate movie fail loudly and exit1.
Remedy. Nothing downstream currently filters on the tag — no GUI banner, no analysis
exclusion — so treat it as a prompt to look at the registration, not as a handled condition.
Improve the fit (more control points, --prealign similarity, a looser --pair-tol so real
pairs are not discarded), or import a dedicated calibration with --tmap. Note that an
imported .tmap leaves the residual NaN and therefore can never trip the gate — a clean
gate on an imported map means nothing was measured, not that the map is good.
A correction step refuses: run something else first
Symptom. One of these ValueErrors from tether.project.gamma or
tether.project.leakage:
/molecules row 0 has no photobleach frames (bleach_frames=(-1, -1), the undetected sentinel); run compute_photobleach() before compute_leakage_alpha()
/molecules row 0 has no photobleach frames (bleach_frames=(-1, -1), the undetected sentinel); run compute_photobleach() before compute_gamma()
/molecules row 0 has no leakage factor (alpha is NaN); run compute_leakage_alpha() before compute_gamma()
Cause and remedy. These messages are the fix — they encode the canonical order
compute_photobleach → compute_leakage_alpha → compute_gamma. (-1, -1) is the
documented "photobleaching not yet computed" sentinel; alpha being NaN is "leakage not yet
computed". tether batch runs the chain for you; you only see these when driving the
library directly.
Two siblings from the same family:
project has no /traces/<layer>; run extraction first(aKeyError, identical text intether.project.photobleach,.leakageand.gamma) — the store has no traces yet.intensity_quantity must be one of ['corrected', 'raw'], got 'bogus'— only those two layers exist.tether.project.exportphrases the same condition differently:unknown intensity_quantity 'bogus'; expected one of ['corrected', 'raw'].
A condition query or per-condition overlay returns nothing
Symptom. query_molecules(project, key={"ligand": "tRNA"}) matches zero molecules, or a
per-condition histogram overlay called with that same key= filter comes back with no
curves — while the plain population histogram over the same store is fine.
The key filter is the whole story: an overlay called without key groups on the raw
condition_id and is unaffected by everything below, so if your unfiltered overlay is empty,
this is not your entry.
Cause. Two rules in tether.analysis.query, both deliberate:
- A
keyfilter only sees materialized/conditionsrows. Untiltether.project.conditions.sync_conditions()has run, a molecule's provisionalcondition_idcannot be resolved back to a key, so it is not key-matchable and the filter excludes it.per_condition_apparent_e_histogramsforwardskeytoquery_molecules, so it inherits this rule and nothing else. - A condition query never returns a molecule with an empty
condition_id— it is a condition query by definition. This one is a defensive invariant rather than a likely cause: every Tether writer stamps the id fromConditionKey.condition_id(), which always returns a non-emptycond-<12 hex>, so no natively-extracted or imported store should contain one. If you do meet an empty id, note thatsync_conditionscannot repair it — it skips empty ids outright (if not cid: … continue) and excludes them fromreferenced, so it reportsn_unresolved=0while fixing nothing.rekey_conditionis not the escape hatch either; it rejects an emptyfrom_condition_id.
Signature. Verified end to end on a freshly batch-extracted 7-molecule project (
Tbox_Cy3Cy5_tRNA_100nM_25C.tether), running each probe before and after the sync:
text before sync after sync plain population histogram N : 7 7 query (no filter) matches : 7 7 query key={'ligand':'tRNA'} : 0 7 per-condition overlay curves : 1 1 overlay + key={'ligand':...} : 0 1 sync_conditions -> created_ids=('cond-6115d12da682',), n_unresolved=0Only the two
key-filtered rows moved. The unfiltered query, the plain histogram and the plain per-condition overlay were never affected — if one of those is what came back empty,sync_conditionswill not change it.
Remedy. Call sync_conditions(path) once per store before using key filters, then
validate_conditions(path) to see anything still dangling. sync_conditions reports
n_unresolved for ids with neither an existing row nor a filename witness — but read that
number narrowly: n_unresolved=0 means every non-empty id either already had a row or got
one from a filename witness. It is not a claim that every molecule is now key-matchable.
A plot shows fewer molecules than I curated
Cause. Two on-by-default invariants on every store-level group-B/C entry point, both Tether additions with no tMAVEN analogue: stale molecules are excluded, and rejected molecules are excluded. A molecule goes stale when its input trace, its analysis window, or its effective applied α/γ changed after the model was fitted — so re-running the corrections can silently empty a TDP that worked yesterday.
Remedy. Pass include_stale=True / include_rejected=True to see them, or re-idealize
to make the model fresh again. Mind the blast radius: the α estimator is store-wide —
compute_leakage_alpha fits one dataset median over every analysable molecule and writes it
to every one of their /molecules.alpha cells, with no partition by condition — so
re-estimating α re-stales every molecule in the project, not one condition's cohort. A
γ-median shift re-stales only the molecules that took the fallback. See
staleness.
The project will not open
Raised by tether.project.core.Project.open / tether.io.schema. In the GUI these appear on
the status bar as Open project failed: <msg> — the shell never crashes, and the previously
loaded project stays in place.
| Message | Cause | Remedy |
|---|---|---|
no such .tether project: <path> |
path typo, or the file moved | — |
file schema_version 99 is newer than this app's 1; refusing to open (PRD section 5.4). |
written by a newer Tether | upgrade Tether. Do not attempt to edit the file |
<path> is not a .tether project (format marker=None) |
valid HDF5, but not ours — an SMD, a model, or a .mat opened by mistake |
open the right file |
<path> is not a readable .tether HDF5 project |
not HDF5 at all, or truncated beyond reading | restore from backup; check the copy completed |
<path> is not a complete .tether project; missing: /movies, /molecules, … |
a half-written store; every missing group is named | re-extract. tether extract itself writes atomically (temp file + replace), so this comes from an external interruption |
A batch resume additionally refuses a newer-schema output even under
--overwrite:--overwritemeans "redo my extraction", not "discard whatever a newer Tether wrote". A foreign or half-written HDF5, by contrast, falls through to the normal re-attempt path.
Setup and environment failures
Idealization cannot find the sidecar interpreter
Symptom.
SidecarError: no sidecar interpreter: pass sidecar_python= or set TETHER_SIDECAR_PYTHON to a Python in an env built from sidecar/conda-lock.yml with tMAVEN installed
or, when the path is set but wrong:
SidecarError: sidecar interpreter does not exist: C:\nope\python.exe
Both are raised by resolve_sidecar_python in tether.idealize.driver, both are
deterministic (transient=False), so the supervisor fails fast instead of burning its
restart budget.
Cause. The interpreter is resolved in this order: the explicit sidecar_python= argument
→ $TETHER_SIDECAR_PYTHON → the installer's sibling envs/sidecar derived from
sys.prefix. None of the three resolved.
On a proper install the third step should cover you even when the environment was never
activated — that fallback exists precisely because a menu shortcut, a PATH shim or a
.desktop entry never runs the conda activate.d hook that sets the variable
(ADR-0051). If you see this error from an
installed app, the envs/sidecar environment is missing or was moved.
Remedy. From a source checkout, build the sidecar with python scripts/setup_sidecar.py,
which creates the env from sidecar/conda-lock.yml, installs the pinned tMAVEN, probes
liveness, and prints the line that points Tether at the interpreter. See
Guided sidecar setup. Then set
TETHER_SIDECAR_PYTHON, or pass --sidecar-python to tether batch.
The sidecar starts but tMAVEN cannot import pkg_resources
Symptom. Idealization fails with a sidecar-reported error; the stderr tail carried in
Tether's wrapper message names pkg_resources. The Tether-side wrapper
(tether.idealize.driver) looks like:
sidecar idealization failed (exit 1): <the sidecar's own error>
--- stderr (tail) ---
…
Cause. tMAVEN calls import pkg_resources at runtime while setting up its version log
(tmaven/maven.py), without declaring the dependency. setuptools deprecated
pkg_resources by 80.9.0 (still shipped through 81.0.0, with a UserWarning on import)
and removed it in 82.0.0, so a sidecar environment on 82 or newer cannot construct
maven_class at all. sidecar/conda-lock.yml resolves setuptools 82.0.1, i.e. past the
removal.
Remedy. Get setuptools<81 into the sidecar environment. Which side you are on decides
whether that has already happened.
Sidecar built from a source checkout. scripts/setup_sidecar.py applies the pin for you.
The version and its SHA-256 live in packaging/setuptools-compatibility.txt, which is the
only place either is written — the script parses that file rather than carrying its own copy,
and installs it hash-checked. sidecar/conda-lock.yml deliberately resolves setuptools
82.0.1, and the script's pip step downgrades it after the env is created. Do not go looking
for the pin in the lock file or the workflow — .github/workflows/sidecar.yml just calls the
script.
Sidecar that came with the installer. The pin is applied for you. envs/sidecar is
materialised from the rendered sidecar/conda-lock.yml (setuptools 82.0.1), and the
constructor post_install script then lays a bundled setuptools<81 wheel over it — a
third offline wheel alongside tether-*.whl and tmaven-*.whl — before installing tMAVEN.
So on an installed app this failure should not happen; if it does, the sidecar env has been
changed after installation (see below), and it is worth reporting.
Environment you built by hand. Nothing applies the pin here, so downgrade it yourself with the sidecar's own interpreter (needs network):
# Linux / macOS
<install-prefix>/envs/sidecar/bin/python -m pip install "setuptools<81"
# Windows
<install-prefix>\envs\sidecar\python.exe -m pip install "setuptools<81"
Then re-run the liveness probe or the idealization. The same command is the recovery step if
you have upgraded setuptools inside envs/sidecar yourself. This affects the sidecar
environment only — Tether's base environment does not import pkg_resources.
Idealization times out, or is restarted repeatedly
Symptom. From tether.idealize.driver:
sidecar idealization timed out after 1800.0s
--- stderr (tail) ---
or, after the restart budget is spent (tether.idealize.supervisor):
sidecar idealization failed after 3 restart(s); last error: …
Cause. The default per-call timeout is 1800 s and the default restart budget is 3. A cold
tMAVEN vbconhmm fit is dominated by JIT compilation, so the first fit of a session is far
slower than the rest. A timeout or crash is classed transient and is retried; an error the
sidecar itself reports is deterministic and is never retried — re-running it will only
repeat the same failure, so look at the data or the model choice instead.
Remedy. Raise --sidecar-timeout, or --max-restarts for a flaky machine. 0 disables
restarts.
A Deep-LASI .tdat uses a particle-detection mode Tether does not implement
Symptom.
tether extract: could not use --tdat mode4.tdat: Deep-LASI ParticleDetectionMode 4 is not supported by Tether (only [1, 2, 3] = wavelet/intensity/bandpass; modes 4 'local-variance' and 5 'ZMW intensity' are not ported)
Raised by tether.io.tdat, wrapped by tether.project.extract. A related message tells you
the file is not a TIRFdata container at all, and prints its actual root keys so you can see
what you handed it:
tether extract: could not use --tdat notdat.tdat: 'notdat.tdat' is not a Deep-LASI TIRFdata .tdat (no 'temp' struct; root keys: ['junk'])
Remedy. Re-run without --tdat and choose a supported --detection-mode yourself.
Tether refuses rather than silently substituting a mode, so an import can never mis-detect
under the wrong method (ADR-0021).
A related non-error: the threshold decode is deliberately best-effort. If the
.tdat's MCOS threshold blob cannot be decoded,tether.io.tdatreturnsNoneand the detector keeps its own default — this never raises, so a slimmed.tdatsilently leaves your threshold at the CLI/library value.
The project is locked by another user or machine
Symptom. One of these from tether.project.lock:
LockedError: <path>.tether.lock is locked: held by alice@OTHERPC (pid 424242, acquired 2026-07-21T02:33:13.661070+00:00)
LockedError: <path>.tether.lock is locked: held by alice@OTHERPC (pid 424242, acquired 2020-01-01T00:00:00+00:00; stale)
LockedError: <path>.tether.lock is locked: the lock file is corrupt (unknown owner)
CorruptLockError: unparseable lock file: <path>.tether.lock
Cause and remedy — the three cases differ, and the message tells you which one you have.
- No
; stalesuffix — a live writer. Close the other Tether window, or ask the named host's user to. Do not steal. ; stale— the holder has been idle longer than the ≈30-minute staleness timeout. Liveness is judged by wall clock, not by probing the PID, because a remote PID cannot be probed and cloud storage is eventually consistent. A stale lock is reclaimable only by an explicit steal; it is never auto-acquired, and the ousted owner's unsaved work is not merged.the lock file is corrupt (unknown owner)— the.lockJSON is unreadable, so no owner can be identified and writes are refused until an explicit steal overwrites it. Deleting the stray.tether.lockfile is the manual fix.
While locked out you can still open the project read-only and curate into your own
split/subset .tether, merged back later on molecule_key.
The spot and overlap views are unavailable
Symptom. A project opens and curates fine, histograms and idealization work, but the movie panel and the per-molecule spot/overlap view are not wired, and the shell shows:
coordinates and patches absent; movie round-trip and spot/overlap views unavailable
Cause. This is an analysis-only import — a coordinate-less project built from an SMD
or a …-donc-accc-w.txt with no .tdat or .mat to supply pixel coordinates. It is not a
failure: tether.gui.shell deliberately leaves the overlap seam unwired because the view is
meaningless without coordinates, and surfaces the marker banner once
(ADR-0046). Every molecule also carries the
round-trip-unavailable tag.
Remedy. To get the round-trip views you must re-import with a coordinate source present —
the movie plus the .mat (the shipped reconstruction path needs both). The import wizard
refuses up front rather than producing a half-wired project, with reasons such as
'<key>' cannot reconstruct: no movie and no .mat trace source. See the
coordinate-availability matrix.
The installer is flagged as unsigned
Symptom. Windows SmartScreen or macOS Gatekeeper warns that the installer is from an unidentified developer.
Cause. Every Tether installer published so far is unsigned — release builds included.
Code-signing is wired into the release pipeline but gated on repository variables that are
not yet set: .github/workflows/release.yml runs the SignPath submission only
if: runner.os == 'Windows' && vars.SIGNPATH_ORGANIZATION_ID != '', and the macOS
codesign/notarytool leg only if: … vars.APPLE_SIGNING_ENABLED == 'true'. With the gates
off, each build emits a ::warning:: saying the installer ships UNSIGNED, and that is the
current state of the published assets. Re-downloading from the release page will reproduce the
same warning.
Remedy. Verify the download instead of trusting the OS prompt, then click through it:
- Check the file against the release's
SHA256SUMS-<platform>.txt(sha256sum -con Linux,shasum -a 256 -con macOS,Get-FileHashon Windows). - Check the build-provenance attestation the pipeline publishes for the same assets
(
gh attestation verify <file> -R bioedca/tether).
Authenticode and Apple-notarized installers arrive once SignPath enrollment and the Apple gate
are completed — the maintainer-side steps are in
Releasing (signed installers), and macOS additionally needs a hardened-runtime
pass over the conda payload before APPLE_SIGNING_ENABLED can be flipped.
The deep classifier does not see the GPU
Symptom. torch.cuda.is_available() is False, or the CUDA runtime fails to initialize,
in the optional deep/ environment.
Cause. The committed deep/conda-lock.yml is CPU-only on purpose, so that CI stays
reproducible; a CUDA build is selected at install time and is deliberately outside
pin-and-hold (ADR-0047). The usual
failure is a wheel whose CUDA channel (cu124 / cu126 / cu128) does not match the
installed NVIDIA driver.
Remedy. Install the wheel whose channel matches your driver, per the PyTorch Start Locally selector. The CUDA wheel bundles the CUDA runtime, so only an NVIDIA driver is needed — no system CUDA toolkit. Full instructions and the reference GPU floor are on the Deep trace classifier page.
Exit codes, and where the details go
| Command | 0 |
1 |
2 |
|---|---|---|---|
tether extract |
success — including a low-confidence-registration warning | the run failed; the message is on stderr as tether extract: <msg> |
argparse rejected the command line |
tether batch |
every movie finished without a failed stage — including deferred, not-requested and warning |
at least one movie had a failed stage | argparse rejected the command line, or the run refused to start (colliding output basenames, a bad supervision argument) |
tether (no subcommand) |
prints help | — | an unknown subcommand or top-level flag |
Two notes on that table:
2is argparse, not Tether. A missing or unknown option never reaches Tether's own validation:tether extract movie.tifwithout the required-o/--outputprints a usage block and exits2, as does any unrecognized flag or unknown subcommand. A bad value for a known option is different — that is Tether's ownExtractOptions.__post_init__check, one line on stderr and exit1.blockednever appears on an exit-0run. A stage is recordedblockedonly when its upstream stage was notdone/skipped, and the only way upstream lands there isfailed(orblocked, which itself implies afailedfurther up). Anyblockedtherefore travels with afailedin the same movie, which flipsMovieResult.okand exits1.
Two things the console deliberately does not print, and where to find them instead:
- Stage details.
tether batch's end-of-run report printsstage=statusand appends the error text only for afailedstage. Everything else — the over-gate flag, the α-withheld fallback, the deferral reason — lives in the JSONL structured log, whose path is echoed to stderr on every run (<out-dir>/batch-log.jsonlby default, or--log). - Basename collisions. Two input movies with the same stem are refused up front, exit
2, before any work:movies '…/flat.tif' and '…/sub2/flat.tif' both map to '…/bout/flat.tether'; rename one or use a separate --out-dir. This is the classicmovie_010.tifin two condition folders; the checkpoint would otherwise treat the second movie as already done using the first's data.