Extending inSiliScope¶
The design rule: every piece of the world is a pure function of (seed, address, channel), and consumers see only the
C ABI. Keep that and a new feature works in the Micro-Manager adapter, the viewer and webSMLM at once.
Layers¶
| Layer | Directory | Rule |
|---|---|---|
| World model (samples: the cell field, the nanorulers; dyes, schedules) | core/ |
C++17, no MMDevice or GPU/OS dependencies, extern "C" ABI only |
| Rendering (PSF, noise, widefield) | adapter/inSiliScope/Simulation/ |
no MM includes; also built by the CLI and the viewer's WASM target |
| Device adapter | adapter/inSiliScope/ |
MSBuild, MM properties |
| Viewer | web/ |
generated module + UI |
| webSMLM block | tools/make_cellfield_block.mjs |
generated |
Recipe: a new structure or cell type¶
- Pick a fresh hash channel number; never reuse one (a reused channel correlates two decisions).
- Generate it per cell in the cell's local frame from
Pcg4d(seed ^ SALT, cx, cy, channel). Decide everything cheap (exists? labelled?) from hashes before building geometry. - Generate in blocks with a stable address so a window query touches only the blocks it needs; cache them in an LRU.
- Expose the sites through the existing dye/event queries, so both modalities pick it up.
- Add
world_checkscases: order independence, tiling, 8 threads = 1 thread. - Document the model on its sample's pages under
docs/samples/(equations, parameters with units and defaults, sources, limits). - Show it in the overview: add its geometry to
ScopeGeometryJson(Simulation/ScopeMovie.cpp,insiliscope_cli --geometry-json) and one draw function toSTRUCTURE_LAYERSintools/build_overview.py. - A labelled structure is a target: a core structure id (
ISC_STRUCT_*, raisingISC_STRUCT_COUNT), a registry entry (below) and its linkage axis inLinkageOffset. The nuclear pores (core/src/npcs.*, 2026-10-12) are the worked example: per-target label options, MM rows, viewer label group and the combined light preset follow from the registry by themselves.
Recipe: a new sample¶
A sample (specimen in the code) is a geometry source; the photophysics, light path, PSF and camera are shared. The
nanorulers (core/src/origami.*) are the worked example.
- Core: a param
specimenvalue; the world builds the sample's geometry from it (Worldincore/src/world.cppbranches on it: an origami world holds anOrigamiField) and yields its sites as dye blocks with their own address (BlockKey), structure id and draws, so the event, continuous, density and site queries work unchanged. Its params inParams/kFields(core/src/params.*), skipped byPackingFingerprint; queries of its own for the hosts (ABI:isc_origami_*);world_checkscases (determinism under any query order, tiling, statistics); the golden vectors re-frozen with its lines added. - Registry: an entry in
data/specimens.jsonwith its targets (option prefix, core structure id; a new structure id raisesISC_STRUCT_COUNT) and theirtypicalLabelsindata/dyes/library.json;node tools/gen_dye_library.mjs. - Engine: its params as options (
ScopeSpecimenParamsinSimulation/ScopeMovie.cpp); the per-target label options come from the registry by themselves. A cli ctest. - Viewer and block: an option group shown for it (
show:predicates inweb/index.html), its drawing (a worker job and a layer inweb/scene/core.js),buildWindow({specimen})intools/make_cellfield_block.mjs. - Micro-Manager: a device for it (
spec/MM_DEVICES.md"A new specimen");SampleHolderlists it by itself;tools/test_specimens.py. - Docs: a page under
docs/samples/, a row in Samples, figures (tools/physics_figures/<page>.py) and a gallery entry.
Recipe: a new DNA-origami design¶
A design is data: add it to data/origami/designs.json (schema in its about), run node tools/gen_origami_designs.mjs (validates it and writes the core's and the
viewer's tables; --check in CI) and re-freeze the golden vectors if a golden case uses it. Its index is the
origamiDesign param; the viewer, Micro-Manager (Nanorulers.Design) and the CLI list it by itself. A body, deformation
or landing model the code does not implement yet (cuboid, 3D bending, stiffness patches, ...) is validated as "not
implemented": add it to core/src/origami.cpp and the generator's validation together.
Positions are in lattice steps of the site spacing (origamiSpacingNm, one value for all designs), so a design scales
with it. Two kinds exist:
- A rod (
"body": {"shape": "cylinder", "diameterNm", "endNm"}):spotsat"at"= 0, 1, 2, ... steps along the axis, each aringof strand positions around it; the rod reachesendNmbeyond the outer spots. - A 2D origami (
"body": {"shape": "plate", "thicknessNm", "marginNm"}, deformationrigid, landing rollflat): alatticeof flat-topped hexagons ("type": "hex",cols,rows, odd columns half a row down) and thecellscarrying a strand, each namedCCRR(column, row from the top-left, both two digits), the labels a hex-map editor prints. Drawing a new pattern in such an editor and pasting the labels of its filled cells is a new design; the plate covers the whole map. Rows run down the image and down the screen, so the pattern reads unmirrored.
Recipe: a new photophysics preset¶
dSTORM/PALM/DNA-PAINT presets are combinations of the labelling fractions, activation rate and the blink parameters
(Kinetics on the World). Add a named preset in the consumers (cli option, viewer, MM property) and a gallery entry.
Recipe: a new MM property¶
Add one row to the property registry (adapter/inSiliScope/Registry/PropertyTable.cpp) on the device and tier that
spec/MM_DEVICES.md picks, with a one-line help, and add the same option to ScopeMovieOptions() so
the CLI and viewer reach it. Regenerate the property section of docs/micro-manager.md (tools/gen_property_reference.py) and, if a config
group sets it, the shipped configurations (tools/gen_mm_configs.py).
Recipe: a new modality¶
Follow WideField or BrightField: a render path in Simulation/, a light source (a shutter device in Micro-Manager, an engine
option light-<name>; spec/MM_DEVICES.md), a CLI/viewer option, an entry in gallery/manifest.json, a CI check, and a section in the physics docs. A modality that
needs new geometry gets it from a core query (BrightField: isc_optical_volume_in_window, pure geometry; the optics
live in the renderer). A new structure should also say what it does to brightfield (its refractive index) and be added
to the optical volume, so brightfield never shows structures the world does not simulate.
Recipe: a gallery entry¶
Add an entry to gallery/manifest.json (options are insiliscope_cli options without the leading --) and give it a
place on the page: a panel in a row of the manifest's sections (each section is a short story followed by rows of
side-by-side panels; the panel's label says what changed, the row's caption what to look at; "shared": true puts a
row on one display range, for comparisons of brightness). Prefer a comparison with the default, or a new row in an
existing section, over a movie on its own. build_gallery.py refuses an entry no row shows. The release
workflow renders it. The 2x2 overview at the top of the gallery and on the Home page comes from the manifest's
overview block (tools/build_overview.py; --suggest proposes a spot); build_gallery.py --only overview renders
just that.
Performance work¶
Speed-ups keep every output bit-identical (cli TIFF pixel data, adapter_pixel_hash, the golden vectors,
world_checks 8 threads = 1 thread): hoist values computed identically, cache pure functions of (seed, address,
parameters), vectorise independent lanes, thread work that is consumed in serial order, re-lay data; never reassociate,
fuse (ctest world_checks NoContraction guards the build flags, link-time optimisation included), or change a
sampler or a default. Measure first: ISC_TIMING=1 makes a cli or viewer movie print its phase times
(Simulation/Timing.h), ISC_TEST=1 python tools/bench_live.py --profile --json out.json times the Micro-Manager live
path per phase (the release's numbers are on the Benchmarks page), isc_core_bench (native) and node tools/bench_core.mjs (WASM) time the core per phase,
sr_render_check --bench the splat, tools/bench.py whole movies. A consumer with several worlds (the viewer's
workers) packs each block once and hands the rows around with ABI 7 isc_world_pack_block / isc_world_set_block
(spec/PORT.md 6.5); across runs the same rows live in a small per-user file (ABI 8 isc_world_set_cache_dir, spec/PORT.md
6.6) or, in the viewer, in local storage. Both are keyed on ISC_WORLD_VERSION: bump it with every change that moves a cell, i.e. whenever spec/golden is re-frozen.
Bit-exactness and world_version¶
The content ported from the JS prototype (retired 2026-10-28) is frozen by golden vectors that were made by the JS; content added since has the C++ itself as its reference:
- add golden vectors generated from the C++ and commit them; a change to them is a deliberate, reviewed act;
- changing existing output (a default, a fix that moves cells) is a breaking change for seeds: record it in
CHANGELOG.md; - the plan is a
world_versionin the ABI, stamped in every output, so a seed can be reproduced on the version that made it.
A deliberate change of existing output re-freezes the goldens from the C++: node tests/parity/golden.mjs --freeze
build/msvc/tests/parity/Release/isc_parity.exe, then bash tools/core_check.sh; update spec/ in that commit.
Checklist for a pull request¶
bash tools/core_check.shruns the native and WASM builds and tests and the module check;- ctest passes natively and under WASM (see
CLAUDE.md); node tools/embed_web_module.mjs --checkis clean (regenerate after a core change);- adapter output unchanged by a refactor:
tools/adapter_pixel_hash.pybefore and after; - docs and the gallery updated;
- Micro-Manager:
tools/test_insiliscope.py,test_cellfield_stage.py,test_history.py,test_specimens.py,test_mm_configs.py(seeCLAUDE.md).