Skip to content

avoid hard cast to MCMC and support Runnable wrappers - #132

Merged
walterxie merged 5 commits into
masterfrom
mcmc
Aug 12, 2026
Merged

avoid hard cast to MCMC and support Runnable wrappers #132
walterxie merged 5 commits into
masterfrom
mcmc

Conversation

@walterxie

Copy link
Copy Markdown
Member

see #131

Summary

BEAUti's BeautiDoc.mcmc input is typed as beast.base.inference.Runnable, but most of
beast-fx assumed it was always safe to hard-cast it to MCMC. That assumption breaks for
wrapper Runnables — e.g. modelselection.inference.PathSampler (from the model-selection
package, see BEAST2-Dev/model-selection#15) — which hold their own MCMC via a named input
"mcmc" instead of extending MCMC directly. Selecting such a template in BEAUti threw
ClassCastException from a dozen+ call sites (Partitions, Clock Model, Site Model, State
Node Initialiser, Operators/connectors, Methods-section text generation).

Changes

  • Add BeautiDoc.getMCMC() / BeautiDoc.getMCMC(Runnable): return the value as-is if it's
    already an MCMC, otherwise unwrap its inner "mcmc" input if present, otherwise null.
  • Replace (MCMC) doc.mcmc.get() / (MCMC) mcmc.get() hard casts with doc.getMCMC()
    throughout BeautiDoc, BeautiConnector, BeautiPanel, BeautiTabPane,
    AlignmentListInputEditor, ClockModelListInputEditor,
    StateNodeInitialiserListInputEditor, SiteModelInputEditor (both packages), XML2Text,
    XML2HTMLPane, XML2HTMLPaneFX, and the BeautiBase test helper; add null-checks where a
    wrapper may have no MCMC yet.
  • BeautiDoc.connectByInput(): if a <connect targetID='mcmc' .../> target lacks the
    requested input but has an inner "mcmc" input that does, retry against that inner object.
  • BeautiPanelConfig: same fallback when resolving a panel's configured input path, so panels
    built against a plain MCMC (Partitions, Priors, Operators, ...) still populate correctly.
  • XML2Text / XML2HTMLPane / XML2HTMLPaneFX: stop pre-casting a freshly parsed file's
    top-level object to MCMC before storing it — keep the real Runnable and let getMCMC()
    unwrap it on demand.

@walterxie

Copy link
Copy Markdown
Member Author
Screenshot 2026-08-11 at 15 34 25

@walterxie

Copy link
Copy Markdown
Member Author

Then click all panels, all work. Save to test.xml, and run beast. The results are produced:

Loading /tmp/step/step99/likelihood.log, burnin 50%, skipping 500 log lines

|---------|---------|---------|---------|---------|---------|---------|---------|


Calculating statistics

|---------|---------|---------|---------|---------|---------|---------|---------|


marginalLs[99 ] = -9557.113325560467

Step theta likelihood contribution ESS
0 1 -5207.686 0 288.1445
1 0.9667 -5207.6183 -173.281 238.1964
2 0.9342 -5208.0912 -169.219 343.7339
3 0.9025 -5208.2264 -165.2011 297.3241
......
99 0 -9557.1133 -0.0021 1.333
sum(ESS) = 21264.2715

marginal L estimate = -5240.616524529081

Total wall time: 120 seconds
Done

…rapper sites #131

BeautiDoc: the inner-"mcmc" redirect added in f6a369c was applied to connect()
only. scrubAll() calls connect() and disconnect() as the two branches of one
decision, so with a wrapper Runnable objects were connected to the inner MCMC
but never removed from it again -- disconnect() resolved the wrapper, and
getInput(inputName) threw IllegalArgumentException into the catch-and-print,
silently doing nothing. Extract the redirect as redirectToInnerMCMC() and use
it from both. Also give disconnect(BeautiConnector, PartitionContext) the same
targetID='mcmc' -> runnable ID translation connect() already has; this is a
no-op for the shipped templates, whose runnable is literally id="mcmc".

OperatorListInputEditor and MRCAPriorInputEditor read MCMC inputs off
doc.mcmc.get() without a cast ("operatorschedule", "operator"), so they were
missed by the cast sweep, but they fail the same way: getInput()/setInputValue()
throw IllegalArgumentException for an unknown input name. Route both through
doc.getMCMC(). This is what still broke the Operators panel and the tip-date
sampling toggle for a PathSampler-style runnable.
@jordandouglas

Copy link
Copy Markdown
Member

Seems reasonable to me

@alexeid

alexeid commented Aug 11, 2026

Copy link
Copy Markdown
Member

Reviewed this carefully. The approach is right and the sweep is complete for its stated scope — git grep '(MCMC)' over beast-fx/src/main now returns only comments, guarding with getInputs().containsKey("mcmc") rather than catching IllegalArgumentException is the correct call, and keeping the real Runnable in XML2Text/XML2HTMLPane* instead of pre-casting on parse is right.

Two functional gaps in the PR's own scope, both fixed in #133 (stacked on mcmc, so merging it updates this PR):

  1. disconnect() didn't get the redirect. scrubAll() calls connect() and disconnect() as the two branches of one decision (BeautiDoc:1720/:1734). With a wrapper Runnable, objects get connected to the inner MCMC but never removed — disconnect() resolves the wrapper, getInput() throws, and the catch-and-print swallows it. Silent, so the generated XML ends up wrong rather than crashy.
  2. Two sites still assume MCMC's inputs. OperatorListInputEditor:66 (getInput("operatorschedule")) and MRCAPriorInputEditor:329/354 (setInputValue("operator", ...)). No (MCMC) cast, so the sweep missed them, but they throw IllegalArgumentException the same way. The Operators panel is in BEAUti throws ClassCastException in multiple panels when the active Runnable wraps MCMC (e.g. PathSampler) #131's repro list, so it was still broken.

Remaining, for you to decide:

  1. Null handling is split roughly half-and-half. Guarded: BeautiConnector:200, ClockModelListInputEditor:56, both SiteModelInputEditors, StateNodeInitialiserListInputEditor:131. Will NPE: BeautiDoc:1595/:1641 (getMCMC().posteriorInput.get() in scrubAll/setUpActivePlugins — every template load), :2859, :2976, the AlignmentListInputEditor/BeautiPanel deepCopyPlugin sites (it dereferences its mcmc arg unconditionally at BeautiDoc:2215, so those just trade a CCE for an NPE), XML2Text:102. Either the unguarded ones are bugs or the guards are dead code. My preference: have getMCMC() throw IllegalStateException naming the runnable class and drop the guards — a named diagnostic beats both CCE and NPE, and BEAUti throws ClassCastException in multiple panels when the active Runnable wraps MCMC (e.g. PathSampler) #131 is fundamentally about diagnosability.
  2. Empty-list fallbacks turn user actions into silent no-ops. ClockModelListInputEditor.getOperators() returns a fresh ArrayList<> when there's no MCMC, then :76-83 calls .add()/.remove() on that throwaway list. Same in SiteModelInputEditor:128 and spec/SiteModelInputEditor:123. "Fix mean rate of clock models" toggles and does nothing. Better to guard the mutation and disable the checkbox.
  3. Import churn is ~85% of the diff and is actively fragile: import java.awt.*; now sits alongside javafx.scene.control.* in BeautiTabPane and javax.swing.* in XML2HTMLPane. Those collide on Label, Button, TextField, TextArea, Menu, MenuBar, MenuItem, Dialog, ScrollPane, List. The smell already surfaced — BeautiTabPane:56-57 reads import java.util.*; followed by import java.util.List; to disambiguate against java.awt.List. It compiles today; the next edit touching one of those names fails for a reason unrelated to that edit. Worth reverting in the files where only a line or two actually changed.
  4. No tests. getMCMC(Runnable) is a pure static with four obvious cases (null / MCMC / wrapper with an MCMC-valued "mcmc" / wrapper without). The connect() and resolveInput() fallbacks are testable with a small stub Runnable — no dependency on model-selection needed. Since nothing in this repo exercises the wrapper path, that test is what stops it silently regressing.
  5. Minor: the same 2-line "unwraps a wrapper Runnable like PathSampler" comment is pasted ~15 times and will rot — the javadoc on getMCMC() already says it. Keep it only where it's genuinely non-obvious (the connect() and resolveInput() fallbacks). And XML2HTMLPane.initialise(MCMC, boolean) ignores its mcmc parameter and re-derives from beautiDoc, so the newly-passed argument is dead — pre-existing, but worth dropping the parameter while you're in there.

Happy to approve once 1/2 are in (via #133 or your own fix) and 3 is resolved either way. 4–7 are cleanup I wouldn't block on.

Make disconnect() symmetric with connect(), and cover two remaining wrapper sites
@walterxie
walterxie merged commit 8edd5cb into master Aug 12, 2026
1 check passed
@walterxie
walterxie deleted the mcmc branch August 12, 2026 03:17
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.

3 participants