Graphical interface
An optional local GUI for exploring what the network knobs do.
rbfenet plan has some sixty knobs, and choosing among them means running it, opening
the report, changing a flag, and holding the previous answer in your head. This subpackage
serves a small local page that closes that loop: move a knob, see the network and its
metrics, pin the run, move another knob, compare.
The design rule the whole thing hangs on is that the GUI’s state is an argv list. It
defines no knob and builds no options object; rbfenetmap.gui.schema derives the form
from the CLI’s own argument groups, and the filled form is serialized back to flags that
rbfenetmap.cli._args.build_network_options() turns into options exactly as the command
line does. So the GUI cannot drift from the CLI, and it can always show the user the precise
rbfenet plan ... line that produced what they are looking at.
Standard library only – there is no extra to install and no optional dependency to probe.
Unlike rbfenetmap.viz, whose output is deliberately script-free because it is an
artifact you email or attach to a ticket, this is an application and does use JavaScript.
The GUI’s form schema, derived from the CLI’s own argument parser.
This module defines no knob. It reads the same argument groups rbfenet plan
assembles, and turns each argparse action into a form field. The GUI then serializes
the filled form straight back to an argv list, which the CLI’s own
build_network_options() and friends turn into options.
That round trip is the whole design. A GUI that kept its own list of knobs would be a
second option surface, and a second option surface drifts: a flag added to the CLI would
quietly not exist in the GUI, and a default moved in one place would silently disagree with
the other. Here a new flag appears in the form with no change to this file, and
tests/test_gui_schema.py fails until any flag deliberately left out is classified.
It also means the GUI can always show the user the exact command that produced what they
are looking at, which is the point of the tool: explore in the browser, paste the
rbfenet plan ... line into a job script.
Reaching into parser._action_groups is unavoidable – argparse exposes no public
equivalent – and has precedent in rbfenetmap.cli._args.explicit_dests(), which does
the same thing for the same reason.
- rbfenetmap.gui.schema.EXPORT_KNOBS: frozenset[str] = frozenset({'design_lambda_max', 'design_lambda_min', 'design_total_ns'})
Knobs that change nothing about the planned network. The A-optimal sample allocation is read only by the Amber exporter, so moving these cannot move an edge – worth saying out loud in a tool whose whole display is the network.
- rbfenetmap.gui.schema.KNOB_EXCLUSIONS: Mapping[str, str] = {'ligands': 'Input, not a knob. The session loads ligands and owns the path.', 'mapper_opt': 'Dead flag: parsed and never read at any create_mapper call site. See issue #55.', 'name_property': 'Input, not a knob. Applied at load time by the session.', 'progress': 'The GUI reports its own progress; the stderr renderer has nowhere to go.', 'weights_file': 'The page edits scoring weights inline and emits them as --weights, so the copied command shows the values that produced the network. A path to a JSON file the GUI cannot display would be a knob whose effect is invisible in the command line.', 'write_aligned': "Output plumbing. The GUI writes nothing to the user's tree on a run."}
Destinations that appear in the five shared argument groups but are deliberately not rendered as knobs, each for a stated reason. Classified rather than merely skipped: the drift test in
tests/test_gui_schema.pyrequires every parser action to be either a field or a member of this set, so a flag added to the CLI cannot slip past unnoticed.
- rbfenetmap.gui.schema.OUTPUT_DESTS: frozenset[str] = frozenset({'cost_units', 'export', 'export_dir', 'exporter_opt', 'out', 'show_rejected', 'validate_exporter'})
Destinations that exist on the
plansubcommand but not in the five shared groups. Output plumbing the GUI supplies for itself. Listed so the drift test can account for every action on the real parser, not only the ones this module builds.
- rbfenetmap.gui.schema.PIPELINE_KNOBS: frozenset[str] = frozenset({'adaptive_batch_size', 'adaptive_initial_neighbors', 'align', 'align_min_atoms', 'align_reference', 'cbfe_atom_weight', 'cbfe_base_cost', 'charge_change_policy', 'compat', 'consistency', 'core_rmsd_threshold', 'distance_threshold', 'explicit_edge', 'intermediate_beta', 'intermediate_generator', 'intermediate_max_cycle', 'intermediate_max_dist', 'intermediate_max_subgraph_dist', 'intermediate_min_link_score', 'intermediate_pose_attempts', 'intermediate_pose_rmsd_factor', 'intermediate_seed', 'intermediates', 'intermediates_per_gap', 'jobs', 'mapper', 'match_selection', 'max_intermediate_gaps', 'max_intermediates', 'max_softcore_atoms', 'max_softcore_fraction', 'mcs_timeout', 'min_core_atoms', 'min_mcs_fraction', 'pair_evaluation', 'pair_strategy', 'planner', 'prefilter', 'prefilter_k', 'prefilter_min_tanimoto', 'ring_policy', 'scorer', 'weights'})
Knobs that apply whatever planner is chosen, because they are consumed before or after selection rather than by the planner: input preparation, mapping, the soft-core feasibility policy, candidate generation (
core/pairs.py), counterpoised pricing (core/cbfe.py), the post-selection consistency pass (core/consistency.py), intermediate generation, and the operational knobs.
- rbfenetmap.gui.schema.PLANNER_KNOBS: Mapping[str, frozenset[str]] = {'complete': frozenset({'allow_disconnected', 'banned_edge', 'n_edges'}), 'explicit': frozenset({'allow_disconnected', 'banned_edge', 'explicit_edge', 'n_edges'}), 'mst': frozenset({'allow_disconnected', 'banned_edge', 'cbfe', 'cluster_bridges', 'cluster_by', 'cycle_coverage_mode', 'edge_direction', 'edges_per_ligand', 'forced_edge', 'hub', 'max_cycle_size', 'max_diameter', 'min_cycle_coverage', 'n_edges', 'selection_objective'}), 'optimal': frozenset({'allow_disconnected', 'banned_edge', 'cbfe', 'design', 'design_candidate_factor', 'design_refine', 'edge_direction', 'edges_per_ligand', 'forced_edge', 'n_edges'}), 'redundant-mst': frozenset({'allow_disconnected', 'banned_edge', 'cbfe', 'cluster_bridges', 'cluster_by', 'cycle_coverage_mode', 'edge_direction', 'edges_per_ligand', 'forced_edge', 'hub', 'max_cycle_size', 'max_diameter', 'min_cycle_coverage', 'n_edges', 'n_redundancy', 'selection_objective'}), 'star': frozenset({'allow_disconnected', 'banned_edge', 'hub', 'hub_selection', 'n_edges'})}
Which selection knobs each built-in planner actually reads, as CLI destinations.
Transcribed from what each planner module touches on its
optionsargument. Advisory: it drives a “this planner ignores that” badge, and nothing depends on it being complete. That matters because the alternative is worse –star,explicitandcompletesilently no-op some fourteen network flags today, and only--designand--cbfeare refused out loud.
- rbfenetmap.gui.schema.WIDGETS: tuple[str, ...] = ('text', 'int', 'float', 'bool', 'choice', 'plugin', 'repeatable', 'path', 'path_list', 'tristate')
Every widget kind a field may carry. Closed so the page can switch on it exhaustively, and so a new argparse action type fails the schema test rather than rendering as a text box that quietly mangles its value.
- rbfenetmap.gui.schema.inactive_dests(planner)[source]
Return the knobs planner will silently ignore.
- Parameters:
planner (str) – A planner plugin name.
- Returns:
Sorted destinations. Empty for a planner not in
PLANNER_KNOBS– an unknown, probably third-party, planner gets no claims made about it rather than having every knob declared inactive.- Return type:
Notes
Advisory. The pipeline refuses only two of these out loud,
--designand--cbfe, through the planner’s owncheck_design_supportandcheck_cbfe_support. The rest are accepted and then not read, which is exactly the failure a form can prevent and a command line cannot.
- rbfenetmap.gui.schema.knob_parser()[source]
Return a throwaway parser carrying exactly the knobs
rbfenet planaccepts.Assembled from the same five public group builders
rbfenetmap.cli.main.build_parser()uses, so the flags, defaults, choices and help text are the CLI’s, not a copy of them.- Returns:
Not fit for parsing a real command line: it has no subcommands and none of the output flags. It exists to be walked.
- Return type:
- rbfenetmap.gui.schema.plan_schema()[source]
Return everything the GUI needs to render the knob form, as JSON-ready data.
- Returns:
groupsOne entry per argparse argument group, in the order
rbfenet plandeclares them, each with atitleand a list of fields.pluginsEvery built-in plugin by kind, with availability and missing requirements.
scorer_weightsDefault weight tables for the three configurable scorers.
planner_knobs,pipeline_knobs,export_knobsThe advisory relevance tables, so the page can grey out a knob the chosen planner will ignore.
compat_pinsDestinations each
--compatlevel pins, which it refuses to be combined with.exclusionsDestinations deliberately not offered, and why.
- Return type:
- rbfenetmap.gui.schema.to_argv(values)[source]
Serialize filled form values into
rbfenet planflags.Only what differs from the default is emitted, so the copied command line is the short one a person would have written rather than sixty flags most of which say nothing.
- Parameters:
values (Mapping) – Destination to value. A destination absent from the mapping takes its default and emits nothing.
- Returns:
The knob flags alone. The caller prepends
planand the input and output flags; those are the session’s business, not the form’s.- Return type:
- Raises:
ValueError – If values names a destination that is not a knob. Loud rather than ignored: a silently dropped value would produce a command line that does not reproduce the network shown beside it, which is the one promise this module exists to keep.
Memoized mapping, so a knob can be moved without re-running the MCS searches.
The measurement this module is built on: over the shipped Tyk2 set – sixteen ligands,
a hundred and twenty pairs, eight jobs – a full rbfenet plan takes about 2.1 s, and it
takes about 2.1 s for every one of the twenty-one variants in the published matrix,
whatever the planner or the selection knobs. The same run with --cbfe all, which skips
mapping entirely, takes 0.5 s. Mapping is the cost, and mapping is the one stage that does
not care which planner runs afterwards.
So the GUI wraps the mapper rather than reaching into the pipeline.
build_network() already accepts a mapper instance, which
makes CachingMapper a plugin like any other and needs no change to core. Moving a
selection knob then re-runs the repair and the scorer – pure Python, and cheap – while the
FindMCS calls come back from a dict.
The cache is keyed on molblocks rather than on ligand names, for the reason
rbfenetmap.io.networkio gives for embedding molblocks instead of file paths: an
AtomMapping is indices into a particular atom ordering, and
is meaningless against a molecule that has been re-read into a different one. A name is not
an identity; the atom block is.
- class rbfenetmap.gui.cache.CachingMapper(wrapped, cache=None, *, should_cancel=None)[source]
Bases:
AbstractMapperA mapper that remembers what it has already mapped, and can be cancelled.
- Parameters:
wrapped (AbstractMapper) – The real mapper. Every miss is delegated to it verbatim.
cache (MappingCache, optional) – Shared store. A fresh in-memory one is made if omitted.
should_cancel (callable, optional) – Polled before each pair. Returning
TrueraisesRunCancelled.
Notes
nameis set on the instance to the wrapped mapper’s, shadowing the class attribute. That is what keeps a cached run indistinguishable from an uncached one: the name is recorded on everyAtomMappingas itsmethodand is serialized into the network JSON, so aCachingMapperthat reported its own name would make every planned network say it was mapped by something that is not a mapping algorithm at all.- map_pair(source, target, options)[source]
Return the correspondence, from the cache when it is there.
- Raises:
RunCancelled – If should_cancel returns true.
rbfenetmap.core.exceptions.MappingError – As the wrapped mapper would, whether the failure is fresh or remembered.
- Parameters:
source (Ligand)
target (Ligand)
options (MappingOptions)
- Return type:
- class rbfenetmap.gui.cache.MappingCache(path=None)[source]
Bases:
objectAtom mappings kept by pair, mapper and mapping options.
- Parameters:
path (Path, optional) – JSON file to load on construction and write on
save().Nonekeeps the cache in memory for the life of the process.
- hits, misses
Lookup counters, so the GUI can say why a run was fast.
- Type:
Notes
Thread-safe.
evaluate_pairs()maps pairs across a thread pool, so several lookups and stores are genuinely concurrent.A failed mapping is cached too. A pair no MCS search can relate is precisely the pair that costs the full
--mcs-timeoutto fail, every time, and precisely the one whose answer will not change when a planner knob moves.- save()[source]
Write the cache to
path, atomically. A no-op with no path set.Written through a temporary file in the same directory and then renamed, so an interrupted save leaves the previous cache intact instead of a half-written file that the next load would discard.
- Return type:
None
- get(key)[source]
Return the cached result for key, or
Noneon a miss.- Parameters:
key (str)
- Return type:
AtomMapping | MappingError | None
- put(key, result)[source]
Store result under key.
- Parameters:
key (str)
result (AtomMapping | MappingError)
- Return type:
None
- exception rbfenetmap.gui.cache.RunCancelled[source]
Bases:
ExceptionRaised inside a mapper to abandon a planning run the user has cancelled.
Deliberately not a
MappingError.build_candidate()catches that one and turns it into amapper_failedrejection, so a cancellation spelled that way would not stop the run at all – it would quietly produce a network in which every pair not yet reached looks infeasible, which is far worse than not stopping.Notes
Cancellation takes effect within one
--mcs-timeout. The pool that maps pairs in parallel waits for its in-flight searches on the way out, and anFindMCScall already inside RDKit cannot be interrupted from Python. What this stops is every pair that has not started yet, which on a large set is nearly all of them.
One browser session: the loaded ligands, the runs, and the pinned comparisons.
A run is started from a filled form, evaluated on a worker thread, and polled. Threads rather than a synchronous handler because mapping is quadratic in the ligand count – past a thousand pairs a plan takes long enough that a blocking request would look like a hung browser, and there would be nowhere to put a cancel button.
Everything a run needs beyond build_network() is borrowed
from the CLI rather than rebuilt. Ligand loading and alignment come from
rbfenetmap.cli.commands._load() and the scorer from
rbfenetmap.cli.commands._make_scorer(), both of which take the same
argparse.Namespace this module already has. That matters: alignment deliberately
sits outside the pipeline, so a GUI that skipped it would leave --align doing nothing
while still printing it in the command line it offers to copy.
- class rbfenetmap.gui.session.PlanRun(run_id, values, argv)[source]
Bases:
objectOne planning attempt, and whatever it has produced so far.
- state
- Type:
{“running”, “done”, “error”, “cancelled”}
- done, total
Candidate pairs mapped, and how many there are to map. Under
pair_evaluation="adaptive"total is a ceiling the run may stop short of.- Type:
- metrics
Exactly what
rbfenet diagnose --format jsonreports, so a pinned comparison reads against the published variant matrix without translation.- Type:
dict or None
- as_dict(*, include_svg=True)[source]
Serialize for the browser. The network itself is never sent whole.
- report_html()[source]
Render the full self-contained report, once, on demand.
Deliberately not produced with the run. On the sixteen-ligand Tyk2 set the report is over two megabytes of inlined SVG, and generating one per knob change is exactly what would make the tool feel slow. The network diagram and the metrics are what the live panel needs; this is a button.
- Return type:
- class rbfenetmap.gui.session.PlanSession(ligands=None, *, name_property='_Name', cache_dir=None)[source]
Bases:
objectThe ligands, the mapping cache, the runs and the pins behind one served page.
- Parameters:
ligands (Sequence[Path], optional) – Initial ligand files. May also be set later from the browser.
name_property (str) – Molecule property ligand names are read from.
cache_dir (Path, optional) – Where the mapping cache is persisted between sessions.
Nonekeeps it in memory.
Notes
One run at a time. A second start cancels the first rather than queueing it: the user has moved a knob, which means they are no longer interested in the answer to the previous question, and two concurrent runs would compete for the same worker threads and double the peak memory of the mapping stage.
- set_ligands(paths, *, name_property=None)[source]
Point the session at a new ligand set and load it immediately.
- Parameters:
- Returns:
names,n_ligandsand thepathsas resolved – resolved rather than as given, so a pattern shows the browser which files it actually matched.- Return type:
- start(values)[source]
Validate values, then plan in the background.
- Raises:
For anything that can be judged without the ligands: an unparseable flag, a
--compatlevel contradicting a knob it pins, a star strategy with no hub, a knob out of range, an edge both forced and banned. Raised here rather than on the worker thread so the browser is told at once instead of having to poll to discover that the run it just started was never going to work.The checks that need the ligand count – an edge budget too small to span them, most of all – can only run once they are loaded, so those still surface on the run as
state == "error".- Parameters:
- Return type:
- cancel(run_id)[source]
Cancel a run by id, if it is still going.
- Parameters:
run_id (str)
- Return type:
None
- pin(run_id, label=None)[source]
Keep a finished run’s numbers for comparison.
Only the metrics, the command and the form are kept; the network is left on the run it came from. A pin is something to read a table row from, and holding every pinned network would grow the session without bound over an afternoon’s exploring.
- rbfenetmap.gui.session.expand_ligand_paths(paths)[source]
Resolve ligand inputs the way a shell would before
--ligandssees them.- Parameters:
paths (Sequence[str or Path]) – Files, directories, or glob patterns.
- Returns:
Sorted matches for each pattern, and every non-pattern passed through untouched.
- Return type:
list of Path
- Raises:
FileNotFoundError – If a pattern matches nothing. A pattern that silently contributes no ligands is worse than one that fails: the run would go ahead over whatever else was listed and quietly plan a network across the wrong set.
Notes
rbfenet plan --ligands data/*.mol2works because the shell expands the pattern into sixteen arguments before argparse ever runs. There is no shell behind a text box, so without this a pasted pattern reachesload_ligands()verbatim and fails as a missing file.Directories are left alone rather than expanded here.
load_ligandsalready scans them, non-recursively and by suffix, and duplicating that rule would be a second answer to which files count as molecules.
A small local HTTP server for the knob explorer.
Standard library only. There are eight endpoints and one page, which a framework would
make marginally pleasanter to write at the cost of a runtime dependency, an extra to
install, an autodoc_mock_imports entry, and a module that
tests/test_smoke.py’s unconditional import walk would trip over in the default CI job.
Bound to the loopback interface unless told otherwise, and it says so loudly when told otherwise: it reads whatever ligand path the form names, with the privileges of whoever started it, and it is not written to face a network.
- rbfenetmap.gui.server.build_server(session, *, host='127.0.0.1', port=8765)[source]
Create the server without starting it.
- Parameters:
session (PlanSession)
host (str)
port (int) –
0asks the operating system for a free one, which is what the tests use.
- Return type:
- rbfenetmap.gui.server.serve(ligands=None, *, host='127.0.0.1', port=8765, name_property='_Name', cache_dir=None, open_browser=True)[source]
Serve the knob explorer until interrupted.
- Parameters:
ligands (Sequence[Path], optional) – Loaded at startup. Omit to choose a file from the page instead.
host (str) – Loopback by default. Any other value is a deliberate choice to expose a server that reads local files on request, and is warned about.
port (int)
name_property (str)
cache_dir (Path, optional) – Persists the mapping cache between sessions, which is what makes the second launch fast rather than only the second run.
open_browser (bool)
- Return type:
None