The Studio

The Studio shows a running simulation as a film, from a browser or a phone, and lets you pause, checkpoint, stop or fork it; watching never changes a bit of the computation. The Studio and its hub are experimental in this release (Experimental features): sim.watch records the telemetry as documented, the page, the server, the hub and the client may change.

import numpy as np

import conatus as cn

box = cn.Box(x=cn.periodic(2.0), z=cn.walls(-0.5, 0.5))
m = cn.Model(box, name="Rayleigh-Benard 2D", Ra=2e4, Pr=1)
m.velocity("u"), m.pressure("p"), m.scalar("T")
m.equations("""
    dt(u) + grad(p) - Pr*lap(u) = Pr*Ra*T*ez - u·grad(u)
    div(u) = 0
    dt(T) - lap(T) = -u·grad(T)
""")
m.at("bottom", u=0, T=+0.5)
m.at("top", u=0, T=-0.5)
sim = m.simulate(N=(32, 16), scheme="RK443", dt=1e-3)
sim.initial(T=lambda x, z: -z + 1e-3 * np.cos(np.pi * x) * np.cos(np.pi * z))

def kinetic(sim):
    """Mean kinetic energy: one number per call, a time series in the Studio."""
    return 0.5 * float(np.mean(sim.grid("u") ** 2))

w = sim.watch("rb_demo",
              film={"T": cn.view("T", scale="signed")},          # the first view is the default
              stats={"kinetic": kinetic,
                     "T(z)": cn.profile("T", along="z")},        # a declared curve
              every_s=0.5,                                       # wall seconds between frames
              spool=spool_dir)                                   # where the Studio reads
print(w.explain().splitlines()[0])
sim.run(steps=50)
sim.unwatch()
Watch 'rb_demo': the callback reads the state every 10 step(s) (never changes it); frames at most every 0.5 s of wall time per name, declared numbers at most every 0.25 s; t, iteration, dt, wall time per step are always sent.
watched run identical to the unwatched one: True

The run is the same with and without the watch, bit for bit: the Studio only reads.

Only what you declare

sim.watch(...) says what the Studio may show, and nothing else is sent:

  • film: the views of the film, by name. A view is an unknown ("c"), a function of the simulation that returns an array (vorticity(sim)), or cn.view(source, scale=...) to choose the colour scale: smoke (one-signed), signed (zero in black) or dye. A 3D field is shown as an isosurface, a volume or slices (cn.view(q, mode="isosurface")).

  • stats: numbers and curves. A function returns one number per call (a time series); cn.series, cn.spectrum and cn.profile declare curves with their formula.

  • every: the wall-clock time between two frames of a view.

w = sim.watch(...) returns the watch; w.explain() (and sim.explain()) lists what will be sent and what is lost on the way: frames are stored on 8 bits and decimated, and the numbers of the computation are never touched.

What you see

  1. The gallery: one card per run, live runs first.

  2. The film: the flow in large, on a dark background. Pinch or scroll to zoom, drag to move, double-tap to return to the initial view; the view chips switch between the declared views.

  3. The timeline: play, 1× to 8×, Live (the end of the film is the present of the run), drag to rewind. The film is kept for the whole run, losslessly re-encoded, within a declared size budget; older frames are thinned out, never silently.

  4. Three sheets, closed by default: Stats (the declared curves, synchronised with the playhead), Controls (pause, resume, checkpoint; stop and fork ask for a second tap), Details (provenance, settings, explain(), logs and what was lost). A time step declared settable (sim.watch(..., settable={"dt": (lo, hi)})) can be changed from Controls, within its bounds.

  5. The board: every run with what needs attention (a non-finite value, a silent run, a relay down…), who is piloting it, and the recent commands; read only.

A second screen compares one quantity across two to six runs. On a phone the Studio can be added to the home screen. The link is always shown: live with its round-trip time, reconnecting with the next attempt (tap to retry now), or offline; the Studio reconnects by itself when the page comes back or the network returns, and a light stream (no 3D data, fewer points) can be chosen on the board for a slow connection.

Scripts and agents read the same state and send the same commands: Pilot a run from a script or an agent.

Opening the Studio

The computation writes its film and numbers to a spool directory; the Studio reads it. Choose who serves it:

  • On a cluster. The server runs inside a batch job on a compute node, never on a login node. A tunnel through the login node brings it to your laptop:

    ssh -J <login-node> -L 8765:localhost:8765 <compute-node>
    # then open http://localhost:8765/login with the one-session token of the job's user_token file
    # (0600; never printed in the job log, which other members of the group can read)
    
  • On your own machine. python -m conatus.studio.hub serve --local --spool <dir> --port 8765. The one-session login token goes to a 0600 file, hub_state/local_user_token (--state DIR, or --user-token-out FILE), written atomically and never through a symbolic link; only its path is printed. --print-token prints the token itself (it then lands in any log of the process).

The Studio needs tornado (pip install "conatus[studio]"); the library never imports it.

Without the native layer

An expert time stepper is watched with conatus.telemetry.Telemetry, which takes the same declarations (views=, stats=) and gives a callback for solver.run(n, callback=..., every=...) and a wrapper for Newton–Krylov searches.

Limits and timings

Every number the Studio, the hub, the relay and the telemetry use on their own (when a quiet run counts as silent, how long a confirmation or a session lives, how many failed logins are allowed, how large a request may be, how much film the hub keeps) is declared in one table, conatus.studio.constants, and printed by python -c "import conatus.studio.constants as c; print(c.explain())". A test fails if the code and the table ever differ, or if a new constant is not listed.

Constant

Value

Kind

What it decides

How to change it

studio.agentapi.SILENT_S

60.0 s

timing

a live run with no record for longer gets the alert ‘silent’

edit the constant (and this table)

studio.agentapi.LIVE_S

30.0 s

timing

a record within the last LIVE_S seconds makes a run ‘running’ (else ‘stale’)

edit the constant (and this table)

studio.agentapi.CONFIRM_TTL_S

120.0 s

security

a confirmation token (stop, fork) is valid this long, and once

edit the constant (and this table)

studio.agentapi.Confirmations(ttl_s)

120.0 s

security

same, for a Confirmations store built by hand

argument of the constructor (and this table for the default)

studio.agentapi.MAX_PARAMS_BYTES

4096 bytes

limit

fork/set parameters: a small JSON object at most

edit the constant (and this table)

studio.agentapi.last_values(n)

200 values

display

scalars returned by an agent’s ‘last values’ query

argument

studio.agentapi.run_view(log)

20 lines

display

log lines in an agent’s view of a run

argument

studio.agentapi.run_view(events)

20 events

display

events in an agent’s view of a run

argument

studio.agentapi.frame_png(upscale)

0 factor

display

nearest-neighbour upscaling of an agent’s frame PNG (0: to about 256 px across)

argument

studio.auth.SCRYPT_N

16384 iterations

security

scrypt cost parameter N of stored password hashes

edit the constant (and this table)

studio.auth.SCRYPT_R

8

security

scrypt block size r

edit the constant (and this table)

studio.auth.SCRYPT_P

1

security

scrypt parallelisation p

edit the constant (and this table)

studio.auth.hash_password(n)

16384 iterations

security

= SCRYPT_N (default of the argument)

argument

studio.auth.hash_password(r)

8

security

= SCRYPT_R

argument

studio.auth.hash_password(p)

1

security

= SCRYPT_P

argument

studio.auth.TOKEN_BYTES

32 bytes

security

random bytes of relay, user and agent tokens and session cookies (256 bits)

edit the constant (and this table)

studio.auth.MIN_PASSWORD

12 characters

security

shorter passwords are refused at creation

edit the constant (and this table)

studio.auth.Sessions(ttl_s)

43200 s

security

lifetime of a browser session cookie (12 h)

argument of the constructor (and this table for the default)

studio.auth.LoginThrottle(max_failures)

5 failures

security

failed password logins per client address within the window before refusal (a valid token is never refused; all clients of a compute node share 127.0.0.1)

argument of the constructor (and this table for the default)

studio.auth.LoginThrottle(window_s)

900.0 s

security

sliding window of the login throttle (15 min)

argument of the constructor (and this table for the default)

studio.client.TAIL_BYTES

8388608 bytes

limit

spool transport: bytes of a run file read from its end

edit the constant (and this table)

studio.client.read_run(tail_bytes)

8388608 bytes

limit

= TAIL_BYTES

argument

studio.client.SpoolClient(tail_bytes)

8388608 bytes

limit

= TAIL_BYTES

argument of the constructor (and this table for the default)

studio.client.read_run(max_scalars)

20000 records

limit

scalar records kept per run when reading a spool file

argument

studio.client.read_run(max_logs)

2000 lines

limit

log lines kept per run when reading a spool file

argument

studio.client.HubClient(timeout)

30.0 s

timing

HTTP timeout of a hub client request

argument of the constructor (and this table for the default)

studio.client.connect(timeout)

30.0 s

timing

HTTP timeout of conatus.studio.connect()

argument

studio.compare.MAX_RUNS

8 runs

display

runs compared at once (the page’s categorical palette has 8 slots, never cycled)

edit the constant (and this table)

studio.compare.compare_runs(max_points)

2000 points

display

points per curve of a comparison (uniform stride)

argument

studio.hub.MAX_PARAMS_BYTES

4096 bytes

limit

fork/set parameters accepted by the hub

edit the constant (and this table)

studio.hub.ACK_TIMEOUT_S

10.0 s

timing

how long the hub waits for the relay to acknowledge a command

edit the constant (and this table)

studio.hub.FILM_MAX_FRAMES

720 frames

limit

film frames kept per run and name in the hub’s memory (then thinned)

edit the constant (and this table)

studio.hub.MAX_BODY_BYTES

1048576 bytes

security

largest HTTP request body (commands and jobs are a few kB)

edit the constant (and this table)

studio.hub.MAX_WAITS

4 requests

security

concurrent long polls per caller (more: HTTP 429)

edit the constant (and this table)

studio.hub.AUDIT_FIELD_BYTES

512 bytes

limit

a command’s fields longer than this are not copied whole into the audit log

edit the constant (and this table)

studio.hub.Hub(max_scalars)

20000 records

limit

scalar records kept per run in the hub’s memory

argument of the constructor (and this table for the default)

studio.hub.Hub(max_logs)

2000 lines

limit

log lines kept per run in the hub’s memory

argument of the constructor (and this table for the default)

studio.hub.Hub(max_runs)

200 runs

limit

runs kept by the hub (beyond it the stalest run is evicted)

argument of the constructor (and this table for the default)

studio.hub.Hub(session_ttl_s)

43200 s

security

browser session lifetime on the hub (12 h)

argument of the constructor (and this table for the default)

studio.hub.Hub(film_mb)

512.0 MiB

limit

film memory budget of the hub

hub serve –film-mb (default 512)

studio.hub.Hub(film_max_frames)

720 frames

limit

= FILM_MAX_FRAMES

argument of the constructor (and this table for the default)

studio.launch.MAX_PARAMS_BYTES

4096 bytes

security

parameters of a job prepared from the Studio

edit the constant (and this table)

studio.launch.LOG_TAIL_BYTES

16384 bytes

display

bytes of a job log shown in the Studio

edit the constant (and this table)

studio.launch.Launcher(max_per_hour)

20 submissions

security

Slurm submissions from the Studio per sliding hour (more: HTTP 429)

argument of the constructor (and this table for the default)

studio.registry.MAX_LIST

500 rows

display

registry rows sent to the page at most

edit the constant (and this table)

studio.relay.SLURM_MIN_PERIOD_S

120.0 s

security

smallest period of the relay’s job-queue poll (at least 120 s between two scheduler calls)

edit the constant (and this table)

studio.relay.MAX_PARAMS_BYTES

4096 bytes

security

fork parameters accepted by the relay

edit the constant (and this table)

studio.relay.FILM_BYTES_PER_PUSH

2097152 bytes

limit

film history sent per poll at most (live data goes first)

edit the constant (and this table)

studio.relay.MAX_LINE_BYTES

4194304 bytes

security

a single telemetry line longer than this is skipped and logged

edit the constant (and this table)

studio.relay.PING_INTERVAL_S

20.0 s

timing

WebSocket ping period of the relay

edit the constant (and this table)

studio.relay.PING_TIMEOUT_S

20.0 s

timing

WebSocket ping timeout of the relay (then reconnect)

edit the constant (and this table)

studio.relay.Relay(poll_s)

1.0 s

timing

spool poll period

relay –poll

studio.relay.Relay(max_rate)

256000.0 bytes/s

limit

upload rate limit (token bucket)

argument of the constructor (and this table for the default)

studio.relay.Relay(max_batch)

131072 bytes

limit

largest message sent at once

argument of the constructor (and this table for the default)

studio.relay.Relay(backoff_s)

1.0 s

timing

first reconnection delay (doubled up to max_backoff_s)

argument of the constructor (and this table for the default)

studio.relay.Relay(max_backoff_s)

60.0 s

timing

longest reconnection delay

argument of the constructor (and this table for the default)

studio.relay.Relay(slurm_s)

120.0 s

timing

job-queue poll period of the relay (>= SLURM_MIN_PERIOD_S; 0 = off)

relay –slurm

studio.relay.Relay(connect_timeout_s)

20.0 s

timing

WebSocket connection timeout

argument of the constructor (and this table for the default)

studio.relay.read_header(max_bytes)

1048576 bytes

security

largest spool header read

argument

studio.webapp.MAX_FRAMES_PER_REQUEST

64 frames

limit

= conatus.telemetry.film.MAX_FRAMES_PER_REQUEST (re-exported)

edit the constant (and this table)

studio.webapp.LITE_MAX_SCALARS

2000 records

limit

scalars per run in a light snapshot (uniform stride, last kept)

edit the constant (and this table)

telemetry.film.MAX_FRAMES_PER_REQUEST

64 frames

limit

frames in one HTTP answer of the film endpoint

edit the constant (and this table)

telemetry.film.MAX_BYTES_PER_REQUEST

8388608 bytes

limit

about this many bytes per film answer (one frame at least)

edit the constant (and this table)

telemetry.film.READ_CHUNK

16777216 bytes

limit

film file indexed per poll (a 256 MiB film: 16 polls)

edit the constant (and this table)

telemetry.sink.FILM_GROWTH

4 factor

limit

film thinning: level j keeps every FILM_GROWTH**j-th frame

edit the constant (and this table)

telemetry.sink.Telemetry(control_interval)

1.0 s

timing

minimum wall time between two reads of the control file

argument of the constructor (and this table for the default)

telemetry.sink.Telemetry(max_px3d)

48 points/axis

limit

3D frames strided to at most this many points per axis

argument of the constructor (and this table for the default)

telemetry.sink.Telemetry(max_points)

16384 points

limit

points of a particle frame

argument of the constructor (and this table for the default)

telemetry.sink.Telemetry(film_max_bytes)

0 bytes

limit

film budget of a bare Telemetry (0 = no film; sim.watch passes film_mb)

argument of the constructor (and this table for the default)

telemetry.protocol.encode_curve(max_points)

4096 points

limit

points of a curve record

argument

telemetry.protocol.film_record(compress_level)

6 zlib level

limit

deflate level of film frames

argument

studio.client.HubClient.status(log)

20 lines

display

log lines of a run’s full view

argument

studio.client.HubClient.status(events)

20 events

display

events of a run’s full view

argument

studio.client.HubClient.scalars(max)

2000 rows

display

scalar rows returned at most (uniform stride)

argument

studio.client.HubClient.log(n)

100 records

display

log records and events returned

argument

studio.client.HubClient.wait(timeout)

25.0 s

timing

long-poll period of a wait for a change

argument

studio.client.SpoolClient.status(log)

20 lines

display

= HubClient.status(log)

argument

studio.client.SpoolClient.status(events)

20 events

display

= HubClient.status(events)

argument

studio.client.SpoolClient.scalars(max)

2000 rows

display

= HubClient.scalars(max)

argument

studio.client.SpoolClient.log(n)

100 records

display

= HubClient.log(n)

argument

studio.client.SpoolClient.wait(timeout)

25.0 s

timing

= HubClient.wait(timeout)

argument

studio.client.SpoolClient.wait(poll_s)

0.5 s

timing

spool file poll period of a wait

argument

studio.hub.Hub.pilots_of(n)

3 users

display

last distinct senders of a command shown for a run

argument

studio.hub.Hub.run_view(log)

20 lines

display

= agentapi.run_view(log), on the hub

argument

studio.hub.Hub.run_view(events)

20 events

display

= agentapi.run_view(events), on the hub

argument

telemetry.film.FilmIndex.frames(limit)

64 frames

limit

= MAX_FRAMES_PER_REQUEST (frames per call)

argument

telemetry.sink.Telemetry.close(timeout)

30.0 s

timing

wait for the writer thread to flush the last records at close

argument

studio.cli --timeout

30.0 s

timing

= HubClient(timeout), from the command line

python -m conatus studio –timeout

studio.cli --log

20 lines

display

= HubClient.status(log)

studio status –log

studio.cli --events

20 events

display

= HubClient.status(events)

studio status –events

studio.cli --timeout->wait_timeout

25.0 s

timing

= HubClient.wait(timeout)

studio watch –timeout

studio.cli -n

100 records

display

= HubClient.log(n)

studio log -n

studio.cli --max

2000 rows

display

= HubClient.scalars(max)

studio scalars –max

studio.hub --film-mb

512.0 MiB

limit

= Hub(film_mb)

hub serve –film-mb

studio.hub --slurm

0.0 s

timing

job-queue poll period of the local relay of hub serve --local (0 = off)

hub serve –slurm

studio.relay --poll

1.0 s

timing

= Relay(poll_s)

relay –poll

studio.relay --max-rate

256000.0 bytes/s

limit

= Relay(max_rate)

relay –max-rate

studio.relay --max-backoff

60.0 s

timing

= Relay(max_backoff_s)

relay –max-backoff