Skip to content

Reproduce the outdated vcell.org tutorials as replayable scripts - #2067

Open
jcschaff wants to merge 19 commits into
masterfrom
feat/tutorial-scripts
Open

Reproduce the outdated vcell.org tutorials as replayable scripts#2067
jcschaff wants to merge 19 commits into
masterfrom
feat/tutorial-scripts

Conversation

@jcschaff

@jcschaff jcschaff commented Sep 5, 2026

Copy link
Copy Markdown
Member

The tutorial PDFs at vcell.org/webstart/VCell_Tutorials/ are screenshot decks shot against VCell 6.1–7.2. Six were refreshed in July 2025 (the 7.7/ subtree); ten still describe a client that has moved on.

This extracts a storyline from each of those ten — what the document teaches, in prose, with its exact parameter values — and turns four of them into scripts that rebuild the model against a current client, run it, and read the results back.

What runs

tools/debug-bridge/launch-client.sh
tools/debug-bridge/scenarios/tutorials/simple-frap.sh      # or moving-boundary.sh,
                                                           # frap-with-binding.sh, phgfp.sh
Tutorial State
SimpleFRAP_7.2.pdf reproduced, 0 errors — also a 59-step replayable recording
MovingBoundaries.pdf reproduced, 0 errors, through the Kinematics tab
FRAPBinding_7.2.pdf reproduced in full, 0 errors — both applications, run locally
PHGFP_7.2.pdf reproduced in full, 0 errors — both applications, run locally
MultiAppTransport image segmentation — genuinely out of reach
PathwayCommons depends on a third-party service, current state unverified
2 × 6.1 rule-based superseded by the 7.7 rewrite — don't reinvest
Quickstart, SpatialRuleBasedGuide reference guides, nothing to script

No script touches File > Save or the green Run button: saving needs an account and Run dispatches a real job to shared compute. They use Native Quick Run instead, which executes with the bundled local solvers and saves nothing — so a scripted tutorial produces real results without an account and without putting anything on the server.

Reading results back is what makes the two multi-stage tutorials reachable. Each takes the steady state of its compartmental run as the initial conditions of its spatial one, the way the PDF's reader does — off the results table:

  • FRAP with binding reads the end of the time course. Worth checking rather than trusting: rB equals rfB because RAN and RAN-FITC start at 5.0 each and compete symmetrically for the same sites; rf + rfB is exactly 5.0 and BS + rB + rfB exactly 20.0. Both conservation laws hold.
  • PH-GFP must not read the end: an event fires at t = 5 s, so the last row is a stimulated state. It reads t = 5, the instant before the stimulus. A variable-time-step integrator picks its own output times, so there is no row number to hard-code — result_row_at_time binary-searches the monotonic time column, about nine reads, and cannot land on the wrong instant.

PH-GFP is also where the compartment rules earn their keep. PIP2_PM + PH_GFP_Cyt -> PIP2_PHGFP_PM joins a membrane species to a volume species, so the reaction belongs on PM — the interface where they meet — not in either compartment. Nothing in the UI enforces that, and typing the equation into a model that does not already have those species would have created all three in the reaction's own structure, silently and with no error.

The canvas problem

Every tutorial builds its physiology by drawing on the Reaction Diagram — "hover on the dotted black lines so they turn green". Those are pixel gestures on a custom-painted canvas, and the recorder deliberately never stores coordinates.

The way round it is that the diagram is not the only route. The Quick Start guide says so outright: species "can be specified and edited in both Structure and Reaction Diagram views". So the scripts drive the table views, where "type 20 in the Diffusion Constant column" is a sentence a script can say literally.

Most of this diff is bridge fixes, not scripts

simple-frap.sh did not work against the bridge as it stood. Each of these reported success while changing nothing:

  • setText never committed. Much of the older GUI reads a field from a focusLost handler rather than on Enter (TimeBoundsPanel, the mesh and output panels). setText moves no focus, so mesh size, ending time and maximum time step were displayed and then discarded — the dialog reopened showing the old numbers. Fixed by calling the focus listeners directly; a synthetic FOCUS_LOST via dispatchEvent is dropped by AWT for a component that never held focus.
  • doClick() never closed a menu. A real click dismisses the pop-up on its way to firing the action. The stale "New Application" submenu then shadowed the next text= lookup, and the failure surfaced several steps later, nowhere near its cause.
  • click used doClick(), which invokes only the action listeners — the Kinematics "New" button shows its pop-up from MouseAdapter.mousePressed, so nothing opened. Buttons now get a real press/release.
  • The recorder never stored which table column was clicked, and replay could not send one. Every tutorial sets values in named columns, so every recording replayed its edit into the wrong cell.
  • Row text was the raw model objectFeature@4d973a55(name=EC), an identity hash that differs every launch — where findRow, setCell, the recorder and the /tree dump all meant "what the row displays". Replay silently fell back to positional indices.
  • findRow only ever matched column 0. A spatial process's parameter table leads with a prose description and carries velocityX in the next column. Now findrow … --in Parameter.
  • Tabs replayed by index, though the application tab strip differs between spatial (4 tabs) and non-spatial (5).
  • text= selectors resolved in /find but nowhere else — pop-up items are exactly the components that need them, since VCell builds them without names. Added text=, type=, and active-window preference.
  • trow is selectTableRow — on a JTree it returns {"selected": false} and does nothing. Two steps in the first version of the FRAP script were silent no-ops that only worked because the Geometry tab happened to already be selected.
  • bridge.sh findrow word-split its query, so findrow "Analytic Equations (2D)" searched for Analytic and quietly selected the 1D row.
  • A panel's columns arrive before its rows, so col succeeded while row returned -1. Lookups now retry rather than trusting a fixed sleep.
  • Committing a field is still not enough for Edit Simulation. That dialog clones the simulation, edits the clone, and replaces the original only on OK — so a focusLost writes into the clone and closing the dialog is what writes the clone back. Leaving it open discards the lot, silently.
  • /tree truncates a table to 25 rows, which is right for reading a UI and useless for reading a result: a steady state is the last row of a series hundreds long. Hence readCell — a row index (negative counts back) and a column by header.
  • The results data table is not "showing". The results window opens on the plot with the spreadsheet as a hidden card that still holds the data, and two PlotDataTables exist — the document window has its own, empty one. Resolved by path within the results window.
  • Copying an application copies its simulations, which keep the source's solver; and adding a geometry leaves the generated math stale, after which VCell refuses to open Edit Simulation at all. RefreshMathButton settles both.
  • setCell could not tick a checkbox. The value arrives as text but a checkbox column's model casts it straight to Boolean, so a String threw on the EDT and the caller saw only a cell that had not changed. PH-GFP needs it: Stim has to be Clamped before its initial condition may depend on t.

No scripted verb drives the physical mouse any more. rrow, rclick, drow, dtrow and click's non-button branch all issued java.awt.Robot clicks at absolute screen coordinates — moving the user's real cursor, breaking if the window moved, and silently failing when the app could not be activated. They now dispatch AWT events. glide and robotClick keep the Robot; that is their purpose.

_common.sh's must helper stops a run when the bridge reports a step did not take. Most of the list above was found by it.

Naming debt, fixed at source

ScrollPaneTable and SortTable were each used by eight or more panels, so a lookup could land on the wrong table. SpatialProcessPropertyPanel called itself "SpatialObjectPropertyPanel" — a copy-paste slip giving two panels the same name. Named: the five model editor tables, SubVolumesTable, StructureMappingTable, SimulationsTable, SpatialObjectsTable, SpatialProcessesTable and their New/Delete buttons, SpatialProcessParametersTable, SpatialObjectQuantitiesTable, subdomainShapeComboBox, ten shape fields in AddShapeJPanel, the kinetics and plot-data tables, and — for the two later tutorials — EventsTable, EventActionsTable, EventSingleTimeTextField, OutputFunctionsTable, FunctionDomainComboBox, PreviousButton and FinishButton. Nothing looked any of these up by name.

A finding for whoever owns the tutorials

Simple FRAP never tells you to map the membrane. Mapping only EC and Cyt leaves PM reading Unmapped while the model still reports 0 errors — but VCell then picks SundialsPDE rather than Fully-Implicit. Followed literally, the tutorial can produce a different simulation than the one it is teaching. The script maps it and asserts the mapping resolved.

PH-GFP's spatial half is built with every diffusion constant left at its default, and the PDF never mentions them. Not something the script should fix by inventing numbers, but worth an author's attention: the whole point of that application is that the fluorescent signal spreads.

Separately: SpatialRuleBasedGuide is the only rule-based topic the 7.7 refresh did not cover — the gap in the current documentation set.

Running it without the window in your face (macOS)

VCELL_UI_BACKGROUND=true tools/debug-bridge/launch-client.sh adds -Dapple.awt.UIElement=true: no Dock icon, never becomes the active application, so it cannot pull you to its Space when a modal dialog opens. Verified by sampling the frontmost process through a full run. It does not hide the window, and /iconify reports a state the window server has not honoured in that mode — worth knowing before trusting the flag.

Verification

  • All four scripts exit 0 with 0 model errors, each from a clean client. The FRAP simulation ends up with mesh 51×51, End Time 3.0, max step 0.01, output 0.05, and both Moving Boundary processes carry velocityX = 4, velocityY = 5*sin(10*t).
  • PH-GFP's final state was read back and checked field by field: five structures mapped (PMCyt_EC_membrane, NMCyt_Nuc_membrane), the three analytic subdomains, Stim clamped with ((t > 5.0) && (t < 6.0)), the spatial simulation on Fully-Implicit at 20.0 s every 0.2 s, and Fluorescence = (IP3_PHGFP_Cyt + PH_GFP_Cyt) defined in Cyt.
  • The recording replays from a clean client and rebuilds the same model.
  • Existing scenarios unaffected: smoke.sh, detach-window.sh (18/18), detach-window-recorded.sh (15/15).
  • mvn test -pl vcell-client -am -Dgroups=Fast: 606 run, 0 failures, 1 errorVCellDataTest, failing on ModuleNotFoundError: No module named 'vtkmodules', the missing-Poetry-env failure documented in CLAUDE.md, in a module this branch does not touch.

Note for reviewers

The bridge changes are behind -Dvcell.debugBridge=true and ship inert. The setName additions and the two renamed panels are the only changes to production UI code; setName is an identifier only, and files under vcell-client are CRLF with the additions preserving that.

🤖 Generated with Claude Code

https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf

jcschaff and others added 19 commits September 5, 2026 08:48
…no-op

Driving a real tutorial through the bridge turned up seven ways a step could
report success while changing nothing. Each is fixed at the point where the
tool was already trying to do the right thing.

Values in table cells
- The recorder computed a table row but never stored WHICH COLUMN was clicked,
  and replay could not send one. These editor tables carry one model value per
  column - initial condition, diffusion constant, size - so every recording
  replayed its edit into the wrong cell. Captured now as `column` +
  `columnName`, resolved by header on replay via a new /findColumn.
- Added /setCell, which commits through the table's own setValueAt - what the
  cell editor calls on Enter. The editor is created on edit and destroyed on
  commit and has no name, so it cannot be addressed any other way.
- setText did not commit. Much of the older GUI reads a field from a focusLost
  handler rather than on Enter (TimeBoundsPanel, the mesh and output panels);
  setText moves no focus, so mesh size, ending time and maximum time step were
  displayed and then discarded. It now delivers the focusLost the user's
  click-away would have produced, calling the listeners directly because AWT
  drops a synthetic FOCUS_LOST aimed at a component that never held focus.

Saying what a thing IS rather than where it sat
- `text=` and `type=` join `name=` as selector forms. Pop-up menu items are the
  case that matters: VCell builds them on the fly with no setName, so
  "In Compartment Cyt" was addressable only as 1/0/1/0/0/2. /find already
  matched on text while the acting endpoints did not, which made `text=` look
  supported and then quietly resolve to nothing.
- An unqualified selector now prefers the ACTIVE window, so `text=OK` means the
  button in the dialog in front of you, not one behind it.
- Row text was the raw model object. findRow, setCell and the recorder stored
  Feature@4d973a55(name=EC) - an identity hash that differs every launch - where
  they meant "what the row displays". Replay then fell back to positional
  indices. All three now go through the renderer, as the tree path already did,
  and the /tree dump does too so what you read matches what you can match.
- selectTab accepts a title. The application tab strip is built from the
  application: spatial shows four tabs, non-spatial five, so an index names a
  different tab in each.
- selectCombo picks a drop-down item by label; the dump lists the items, so you
  can see what is selectable instead of guessing an index.
- The recorder emits `text=` for buttons and menu items that have no name,
  and records "text" as its own durability tier. Restricted to AbstractButton:
  a JLabel's text is often a live value.

bridge.sh findrow built its flag with an unquoted command substitution, so any
query containing a space was word-split before curl saw it - findrow "Analytic
Equations (2D)" searched for "Analytic" and silently matched the 1D row.

Verified against the running client: smoke.sh, detach-window.sh (18/18) and
detach-window-recorded.sh (15/15) all still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
A UI script can only address a component durably if it has a name; otherwise it
falls back to a node path, which breaks the next time the panel is rearranged.
These are the components a script driving the model editor has to touch.

- BioModelEditorModelPanel: the five right-side tables were unnamed, so the
  structures table was reachable only as 0/0/1/0/1/0/0/2/2/0/2/0/0/0.
- GeometrySubVolumePanel, StructureMappingPanel and SimulationListPanel each
  called their table "ScrollPaneTable"/"ScrollPaneTable1" - names eight other
  panels also use, left over from the VisualAge generator. A replay looking for
  the simulation list could resolve the geometry subdomain table instead. Now
  SubVolumesTable, StructureMappingTable and SimulationsTable. Nothing looks
  these up by name, so the rename is safe.
- AddShapeJPanel: the shape panels were named but their ten text fields were
  not, leaving the circle radius addressable only as "the third text field
  currently reading 1.0". Named after construction, since several are assigned
  partway through the initializer.

No behaviour change; setName is only an identifier. Files under vcell-client
are CRLF and the additions preserve that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…ript for FRAP

The tutorial PDFs at vcell.org/webstart/VCell_Tutorials/ are screenshot decks
shot against VCell 6.1-7.2. Six were refreshed in July 2025 (the 7.7/ subtree);
the ten here still describe a client that has moved on.

Each gets a storyline - what the document teaches, in prose, extracted from the
PDF - so the content survives independently of the deck. Simple FRAP also gets a
script that rebuilds it against a current client, end to end, in ~100 seconds
with zero errors: physiology, application, analytic 2D geometry, structure
mapping, specifications and simulation settings. It stops before Save and Run,
which need an account and dispatch a real job to shared compute.

The tutorials all build their physiology by DRAWING on the Reaction Diagram,
and the recorder deliberately never stores coordinates. The way round it is
that the diagram is not the only route - the Quick Start guide says species
"can be specified and edited in both Structure and Reaction Diagram views" - so
the script drives the table views, where "type 20 in the Diffusion Constant
column" is a sentence it can say literally. What has no table equivalent is
called out per tutorial: image segmentation in MultiAppTransport is genuinely
out of reach.

Two findings worth acting on separately:

- Simple FRAP never tells you to map the membrane. Mapping only EC and Cyt
  leaves PM reading "Unmapped" while the model still reports 0 errors - but
  VCell then picks SundialsPDE rather than Fully-Implicit. Followed literally,
  the tutorial can produce a different simulation than the one it teaches. The
  script maps it and asserts the mapping resolved rather than letting a silent
  no-op flow downstream.
- SpatialRuleBasedGuide is the only rule-based topic the 7.7 refresh did not
  cover, and Tutorial06_PathwayCommons depends on a third-party service whose
  current state was not verified.

Both artifacts are kept because they are not equivalent: the shell scenario
resolves rows and columns at run time and asserts, so it is what to verify
with; the JSON recording is a faithful linear capture, which is what to use for
screenshots (replay.py --shots) or a filmed walkthrough (--driver robot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…tealing focus

VCELL_UI_BACKGROUND=true adds -Dapple.awt.UIElement=true, which registers the
client as a macOS accessory app: no Dock icon, no menu bar, and it never becomes
the active application. That matters because activating a window pulls the whole
desktop to whichever Space it is on, and an ordinary app activates itself both on
launch and every time a modal dialog opens - so a scripted run repeatedly yanked
the user off whatever they were doing.

Verified by sampling the frontmost process every 5s through a full tutorial run:
it stayed on the user's own applications throughout, dialogs included, and every
step still worked - the bridge drives the model rather than real input, so it
needs neither focus nor a Dock icon.

Documented alongside it: this does NOT hide the window, and it makes /iconify
report a state the window server has not honoured. Frame.setExtendedState accepts
ICONIFIED and the bridge faithfully repeats it, but with no Dock icon there is
nowhere to minimise to and the window stays visible - so "iconified": true from
the bridge is not evidence the window is gone.

Off by default; removing the menu bar makes it wrong for driving the client by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
rrow, rclick, drow, dtrow, rtrow and the non-button branch of click all issued a
java.awt.Robot click at absolute screen coordinates. That is the wrong default
for a tool whose whole point is to drive the model:

- it moves the user's real cursor and clicks for them, so a scripted run fights
  whoever is at the keyboard;
- it lands wherever the window happens to be, so moving the client out of the
  way silently breaks it;
- it needs the application to be ACTIVE. Run the client as a macOS accessory app
  and a real right-click never raises the context menu, while the endpoint still
  reports {"rightClicked": true} - which is how this was found: a tutorial step
  reported success and no menu had opened.

They now dispatch the events to the component in its own coordinates. Swing
decides to show a context menu from MouseEvent.isPopupTrigger(), so a
press/release pair carrying that flag reaches the same listeners the real gesture
would; the flag is set on both because which one carries it is platform-specific.
glide and robotClick keep the Robot, which is their entire purpose.

Also: doClick() on a menu item never closed the menu. A real click dismisses the
pop-up on its way to firing the action; doClick only fires the action, so the
stale "New Application" submenu stayed open and shadowed the next text= lookup -
and the failure surfaced several steps later, nowhere near its cause. Clicking a
JMenuItem now clears the menu selection path first (first, not after, so it does
not race a dialog the action opens).

Names the subdomain shape combo, the last addressable-only-by-path control in the
FRAP tutorial's path.

Verified: smoke.sh, detach-window.sh (18/18) and detach-window-recorded.sh
(15/15) all pass, and the FRAP tutorial now runs end to end with the client as a
non-activating accessory app parked off to one side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…the FRAP script

_common.sh factors out the helpers the tutorial scenarios share, with one rule
behind all of them: never let a step that did not happen pass for one that did.

- must() runs a bridge verb and stops if the reply says it did not take. Every
  acting endpoint answers {"clicked": true} / {"set": false} / ..., and piping
  that to /dev/null is how a mistake becomes invisible.
- row() and col() resolve by displayed text and column header, and stop on a
  miss rather than returning -1 into the next call.
- navselect() exists because `trow` is selectTableRow: calling it on a JTree
  returns {"selected": false} and changes nothing. The tree verb is `row`.

That last one was a real bug in this script. Two steps selected nothing, and the
tutorial only worked because the Geometry tab happened to already be selected
after the application was created. must() caught it on the first run.

The recording is regenerated: 59 steps, 49 resolved by name, 8 by label, and 2 by
path - the tree's transient cell editor and a bare JSortTable inside a
JOptionPane, both of which genuinely have nothing else to be addressed by.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…amed column

Two more silent failures, both found building the Moving Boundary tutorial.

click() drove a button with doClick(), which invokes only the ACTION listeners.
Not every button keeps its behaviour there: the Kinematics "New" button builds
and shows its pop-up from a MouseAdapter.mousePressed, so doClick fired the
(empty) action, reported success, and opened nothing. Buttons now get a real
press/release, which drives the button's own UI listener - firing the action
exactly once - and also reaches the mouse listeners, so both wirings work. Menu
items keep doClick, because a menu item's behaviour always lives in its action.

findRow only ever matched column 0. That is the row's identity in most of these
tables but not all: a spatial process's parameter table leads with a prose
description ("surface velocity (x coord)") and carries the name the tutorial
actually says - velocityX - in the next column. Searching column 0 found nothing
and returned -1, which the caller then passed to setCell as a row index. It now
takes an optional column to search, named rather than indexed:

    bridge.sh findrow name=SortTable velocityX --in Parameter

Names the four controls on the Kinematics tab. It has two tables and two pairs
of "New"/"Delete Selected" buttons that are identical by label and position, so
without names there was no way to say which one a step meant: SpatialObjectsTable,
SpatialProcessesTable, and their New/Delete buttons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…kups wait

Second of the outdated tutorials to run end to end. It is Simple FRAP in a cell
that MOVES: same physiology, but the membrane carries a velocity and the domain
deforms as the simulation runs. Both spatial processes end up with
velocityX = 4 and velocityY = 5*sin(10*t), the volume process VCell creates for
the wrong object is deleted, and the model reports 0 errors.

Three things the Kinematics tab teaches the hard way, all now in the script and
its storyline:

- Volume kinematics lands on the wrong object first. VCell creates a process for
  the next volume in the spatial-object table each time, so the first attempt
  targets EC and a second is needed to reach Cyt. The tutorial's "delete the one
  you did not want" is not a nicety.
- The Interior Velocity checkbox is on the OBJECT, not the process. A volume
  process reports an error until its spatial object is allowed to have one.
- A panel's columns arrive before its rows. Selecting a process yields the four
  parameter headers immediately and the velocity rows a moment later, so `col`
  succeeded while `row` still saw nothing and returned -1. row() and col() now
  RETRY for 10s rather than trusting a fixed sleep - which is the general fix for
  a whole class of flake these scripts would otherwise have.

SpatialProcessPropertyPanel called itself "SpatialObjectPropertyPanel" - a
copy-paste slip that gave two different panels the same name - and both panels
named their table "SortTable", so a lookup landed on whichever was found first.
Now SpatialProcessParametersTable and SpatialObjectQuantitiesTable.

Verified: moving-boundary.sh exits 0 with both processes carrying the tutorial's
velocities, and simple-frap.sh still exits 0 with End Time 3.0 and 0 errors, so
the retry change is not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
"Needs reaction creation" was too vague to act on. The specific obstacle, read
off BioModelEditorReactionTableModel.setValueAt:

An equation typed into the "(add new here, e.g. a+b->c)" row is only accepted
when the model has EXACTLY ONE STRUCTURE - the same restriction the species table
has, and the reason simple-frap.sh creates its species through the New Species
button's "choose compartment" menu instead. FRAPBinding has three structures and
PHGFP five, so that route is closed for both.

Editing the Equation column of an EXISTING reaction carries no such restriction,
and does the useful part: it parses the equation and creates any species it names
that do not exist yet. So the sequence is New Reaction -> choose compartment ->
set the equation, mirroring the species route that already works.

Read from the table model, not yet verified against the running client, and the
storylines say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
Correcting the previous commit, which said editing an existing reaction's
Equation column "creates any species it names" and left it there. True, but
misleading in a way that matters: it creates them in the REACTION's compartment,
and there is no way to say otherwise from the equation text.

ModelProcessEquation.parseReaction resolves each name with
model.getSpeciesContext(var) across the whole model and reuses it wherever it
already lives; only an unrecognised name becomes
new SpeciesContext(species, rxnStructure). So auto-creation is correct only when
every participant belongs in the compartment the reaction sits in.

FRAPBinding happens to satisfy that - all six species and all four reactions are
in Nuc - but PHGFP does not: PIP2_PM and PIP2_PHGFP_PM are on the membrane while
PH_GFP_Cyt and IP3_Cyt are in the cytosol, so
"PIP2_PM + PH_GFP_Cyt -> PIP2_PHGFP_PM" spans two compartments and typing it
against a model missing those species would put all three in one, silently and
with no error. MultiAppTransport has the same shape around its NM flux. The
recipe is therefore: create every species through New Species -> choose
compartment first, exactly as simple-frap.sh already does, and let the equation
only wire up species that exist.

Also corrects where the placeholder lives: in the Reactions table
"(add new here, e.g. a+b->c)" is in the EQUATION column, not the Name column as
it is for structures and species - getValueAt returns a ModelProcessEquation for
COLUMN_EQUATION on that row and null for every other column.

Thanks to Jim for catching the compartment point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…rface

Completes the reaction-creation recipe. Choosing the species' compartments is
only half of it; the reaction itself has one too, and New Reaction asks which.

The rule, from Jim: a localized reaction spanning more than one compartment must
occur where those compartments meet - the N-1 dimensional interface between them,
which is the membrane. A reaction whose participants all share one compartment
simply goes there. Nothing in the UI enforces this, so a script has to choose
deliberately rather than take whatever validates.

Recorded per reaction where the models actually differ:

  PHGFP   PIP2_PH  PIP2_PM + PH_GFP_Cyt -> PIP2_PHGFP_PM   PM  (membrane + volume)
          IP3PH    IP3_Cyt + PH_GFP_Cyt -> IP3_PHGFP_Cyt   Cyt
          r2       -> IP3_Cyt, catalysed by Stim           Cyt

  MultiApp  the RanC flux across the nuclear membrane      NM
            the cytoplasmic reaction                       Cyt

FRAPBinding needs no such distinction - every species and every reaction is in
Nuc - which is precisely why it is a poor model to generalise the recipe from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…on network

Third tutorial to run end to end, and the first whose physiology is a REACTION
NETWORK rather than a single diffusing species. Builds three structures, six
species, four reactions and the compartmental application, finishing with 0
errors: sizes Cyt 523.33 / Nuc 26.1665 / NM 130.8325, initial conditions
r 5.0 / rf 5.0 / BS 20.0, and a simulation to 30 s.

Three rules make a reaction network expressible in the table views, all of them
load-bearing and none of them visible from the PDF, which draws on the canvas:

- Species first. An equation cannot place a species, so every species is created
  through New Species -> choose compartment before any equation is typed.
- Each reaction in the compartment where its participants meet. Everything here
  is in Nuc, so every reaction is too.
- A catalyst is never written in the equation. The grammar is only
  "reactants -> products"; catalysts neither parse nor render, and setting the
  Equation column calls setReactionParticipants with reactants and products
  alone. A catalyst is implied by the KINETIC LAW - a rate expression naming a
  species that is neither reactant nor product makes it one. Confirmed against
  the running client: after setting the bleaching rate to
  (Vmax*rf*Laser*((t>1.0)&&(t<1.5))), Laser appears in the reaction's parameter
  table as a Variable and the model reports no errors. Thanks to Jim for the
  explanation; I had been looking for a way to declare it explicitly, and there
  isn't one.

The New Reaction dialog turned out to be the right seam: it asks for all three of
a reaction's defining properties at once - where it occurs, its name, its
equation - so there is no half-built intermediate state. Its three controls are
now named.

ReactionPropertiesPanel called itself "KineticsTypeTemplatePanel" - the third
copy-paste panel-name collision in this branch, after SpatialProcessPropertyPanel
- so the name resolved to a class that was not the panel on screen and the first
run edited nothing. Its kinetics table and type combo shared the generic
"JComboBox1"/unnamed forms with two other panels; both now have their own names,
as does KineticsTypeTemplatePanel's equivalent pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
…uick Run

The Edit Simulation dialog is unusual: it CLONES the simulation, edits the clone,
and replaces the original in the document only on OK. So making setText commit -
the earlier focusLost fix - only got the value into the clone. Closing the dialog
is what writes the clone back, and leaving it open discards every edit silently,
with the simulation table still showing the old numbers.

The scripts closed it with a bare `click text=OK`, which resolves against every
showing window and can land on a different OK. It usually worked and sometimes
did not: one run quick-ran a 1.0 s simulation while the open dialog read 30.0.

dialog_button now resolves the button INSIDE the named dialog and then waits for
that dialog to actually disappear, so a failure to close is an error rather than
a silently discarded edit.

Also names the Quick Run button. It runs a simulation on this machine without
saving the document to the database, which is what lets a scripted tutorial
produce real results: the local install carries every solver these tutorials need
(SundialsSolverStandalone for ODE, FiniteVolume for PDE, MovingBoundary, plus the
stochastic ones), and the results window exposes its species list and data table
to the bridge. That is the missing piece for the tutorials whose later steps read
values off an earlier run.

Thanks to Jim for the clone-and-replace explanation; I had the symptom but not
the mechanism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
Jim's call: prefer Quick Run, so nothing has to be saved to the database. That
turns out to unlock more than the compartmental runs - the local install carries
every solver these tutorials need (SundialsSolverStandalone, FiniteVolume,
MovingBoundary, VCellStoch, NFsim, smoldyn, langevin), and canQuickRun refuses
only parallel solvers, server-only features and missing executables.

simple-frap.sh is now a COMPLETE reproduction: it builds the model and runs it,
finishing with a "Results for Simulation FRAP" window from the bundled
FiniteVolume solver, with no account and nothing written to the server.

New bridge verb /readCell, because the tree dump truncates a table to 25 rows.
That is right for reading a UI and useless for reading a RESULT: a steady state
is the last row of a series hundreds long, and that is precisely the row a
tutorial copies into its next application. readCell takes a row index (negative
counts back, so -1 is the last row) and a column by header, and returns the
rendered text. The row count was already reported un-truncated, so the caller can
always find the end.

Two helpers in _common.sh: quick_run waits for the results window rather than
sleeping, and result_cell reads one value out of it.

Also fixes a flake the assertions caught: `click text=Rename` failed on one run
because a context menu is transient and built from a dispatched mouse event, so a
fixed sleep between opening it and clicking an item is a race. tree_pick and
menu_pick retry the open-and-click as a UNIT - reopening a menu after a miss is
harmless, since a second right-click just replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
… run

Completes the loop Jim asked for: run locally, no database save, and now read the
results back so a tutorial's later steps can use them.

FRAP with binding takes the steady state of its compartmental run as the initial
conditions of its spatial one - the step I had called a wall, on the grounds that
those numbers only exist after a run. They do, but the run can be local. The
script now quick-runs the ODE application and reads the end of the time course:

    BS 12.807787   rB 3.5961066   rf 1.4038934   rfB 3.5961066

Checked rather than assumed: rB equals rfB because RAN and RAN-FITC start at 5.0
each and compete symmetrically for the same sites; rf + rfB is exactly 5.0 and
BS + rB + rfB exactly 20.0. Both conservation laws hold, so this is the solver's
arithmetic and not a plausible-looking artifact.

Two new verbs were needed:

- /selectList picks list items by their displayed label, comma-separated for a
  multi-select. Which species are selected in the results window decides which
  COLUMNS the data table has, so reading four concentrations means selecting four
  names first - and the tutorials say "Ctrl-click the other species", never
  "select rows 0, 3, 4 and 5".
- /readCell reads one cell without the tree dump's 25-row cap, with a negative row
  counting back from the end. The cap is right for reading a UI and useless for
  reading a result.

Naming the results table was not enough on its own. TWO PlotDataTables exist - the
document window keeps its own, empty - and NEITHER reports isShowing, because the
results window opens on the plot with the spreadsheet as a hidden card that still
holds the data. So the usual "prefer what is showing" tie-break had nothing to
work with and a bare name landed on the wrong one. result_cell resolves the table
by path within the results window instead.

Also: macOS ships bash 3.2, which has no associative arrays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
The whole tutorial now reproduces: physiology, the compartmental application, a
local run, the steady state read off its results, and the spatial application
built from those numbers. Two applications, 0 errors, and a spatial simulation at
50 s / every 0.5 s on Fully-Implicit - the PDF's parameters, on a solver that can
run them.

Setting each species from its own steady state, rather than the PDF's copy-four-
cells-and-Paste-All, says the same thing without depending on the row order of
either table. r and rf both come out at 1.4038934 and rB and rfB at 3.5961066,
which is the symmetry the model implies.

Copying an application is not quite a copy, and none of this is in the PDF:

- It brings the SIMULATIONS with it, still carrying the compartmental ODE solver.
  "Combined Stiff Solver (IDA/CVODE) does not support ... Spatial". Delete them.
- Adding a geometry leaves the generated math stale, and VCell then refuses to
  open the Edit Simulation dialog at all: "Application geometry does not match
  Simulation geometry - Update Math before editing". RefreshMathButton on the
  Generated Math tab clears it.
- That same refresh settles the solver. A simulation created against stale math
  inherits the copied application's ODE solver; created after a refresh it gets
  Fully-Implicit. One click, two symptoms that look unrelated.

Each of these surfaced as a failure several steps from its cause - the Mesh tab
"not found", because an error dialog had blocked the Edit dialog from opening at
all. dialog_tab scopes tab selection to the named dialog, since JTabbedPane1 is
also the name of the pane in a results window, which these tutorials now open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
The value arrives over HTTP as text, but a checkbox column is not a text
column: its model casts what it is handed straight to Boolean, so passing a
String threw on the EDT and the caller saw nothing but a cell that had not
changed - the exact silent failure these scripts exist to catch.

Convert for columns whose declared class is Boolean. The PH-GFP tutorial
needs it: Stim has to be Clamped before its initial condition may depend on t.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
Both panels were addressable only by position: the events table and the
output functions table were each called "ScrollPaneTable", a name eight other
panels also use, and the wizard's domain combo and Previous/Finish buttons had
no name at all. The single-time field needed one too - several fields in that
dialog read "1.0" by default, so their text does not identify them.

No behaviour change; component names are used for lookup, not display.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
The longest of the decks (101 pp) and the one where the compartment rules
earn their keep: species live on a membrane as well as in volumes, and
PIP2_PM + PH_GFP_Cyt -> PIP2_PHGFP_PM spans both, so it belongs on the
interface between them rather than in either.

Both applications, end to end, 0 errors:

- physiology - five nested structures, six species placed by compartment,
  three reactions each where its participants meet
- a compartmental application with the sizes, the resting concentrations and
  two timed events, run locally with IDA
- a 3D spatial application - a sphere of cytosol with an off-centre nucleus,
  built from analytic equations - carrying the resting state across, with Stim
  clamped to a time window and a Fluorescence output function

Two things the tooling did not have:

- result_row_at_time. The value the spatial half needs is NOT the end of the
  compartmental run: an event fires at t = 5 s, so the last row is a
  stimulated state. The tutorial reads t = 5 instead, and a variable-time-step
  integrator picks its own output times, so there is no row number to
  hard-code. Binary-searching the monotonic time column finds it in about nine
  reads and cannot land on the wrong instant.
- result_table_path, factored out of result_cell so both share one lookup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HAnpFxkzf9LmxBayDSANf
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.

1 participant