Skip to content

12. Jupyter notebooks

sysml-jupyter-kernel runs SysML v2 in Jupyter notebooks. A notebook cell is read exactly as the REPL reads what you type: declarations accumulate into one session model, a bare expression is evaluated, and every % command of the prompt — instantiating, running, debugging, sweeping, checking, rendering — works as it does there, with the model and the runtime kept from cell to cell. Views and documents come back as rich output the notebook renders.

The kernel is the REPL's session behind the Jupyter protocol, not a client of the sysml-grpc service: nothing else needs to run, and the cells hold SysML, not Python. To drive a model from Python, use the opensysml client instead; the two can share a notebook server.

Installing

The kernel is a package on PyPI. Its wheel for your platform (Linux x64 and arm64, macOS Intel and Apple Silicon, Windows x64) carries the release's sysml-jupyter-kernel binary and registers it as a kernelspec named sysml under the Python environment it is installed into, so one command is the whole install:

pip install jupyter-opensysml-kernel
jupyter kernelspec list        # lists sysml

The kernelspec, with the OpenSysML mark as the kernel's icon, lives beside the package (<prefix>/share/jupyter/kernels/sysml) and starts the bundled binary through python -m jupyter_opensysml_kernel, so it goes wherever the package goes: pip uninstall removes both. Run pip install with the Python that runs the notebook server, or in the environment it runs in, and the server sees the kernel.

On a platform without a wheel, pip installs from the sdist, which carries no binary; then install downloads the release's binary and checks it against the SHA-256 digest the package was built with, so a mirror or a tampered download is refused before anything is written. The same command registers the kernelspec somewhere other than the package's prefix — for your user, or under another prefix — copying the bundled binary into it when there is one:

python -m jupyter_opensysml_kernel install                    # this environment, or the user
python -m jupyter_opensysml_kernel install --sys-prefix       # this environment
python -m jupyter_opensysml_kernel install --user             # ~/.local/share/jupyter
python -m jupyter_opensysml_kernel install --prefix /opt/jupyter
python -m jupyter_opensysml_kernel uninstall                  # removes what install wrote

A sysml-jupyter-kernel you built yourself (make build-jupyter-kernel) or installed with install.sh --tools sysml-jupyter-kernel is registered without a download:

python -m jupyter_opensysml_kernel install --binary bin/sysml-jupyter-kernel
sysml-jupyter-kernel -install              # or let the binary write its own kernelspec

With conda, the same package installs from conda-forge once its recipe is accepted there (the recipe is kept under packaging/conda); until then pip install into the conda environment registers the kernel under that environment's prefix the same way.

Then start a notebook server and pick SysML v2 (OpenSysML) from the kernel list:

pip install jupyterlab
jupyter lab

The package also carries jupyterlab-opensysml, a prebuilt JupyterLab extension, as shared data (<prefix>/share/jupyter/labextensions/jupyterlab-opensysml), where JupyterLab 4 and Notebook 7 load extensions from without a build step: jupyter labextension list shows it enabled right after pip install, with no Node.js and no jupyter labextension install. It highlights SysML v2 and KerML — keywords, comments and doc bodies, strings, numbers, 'unrestricted names', Qualified::Names, operators — in the cells of a sysml notebook, in .sysml and .kerml files opened in the editor, and in Markdown code fences tagged sysml or kerml; a cell's leading %command is marked as the kernel command it is.

Cells

A cell may hold declarations, % commands and expressions, mixed. The kernel splits it as the prompt does — a % command is one line; an expression is answered on its own once its brackets close; declarations between them go in as one submission, as a file would — and runs each part in order, stopping at the first that fails.

package Vehicles {
  private import ScalarValues::*;
  part def Wheel { attribute diameter : Real; }
  part def Car {
    attribute mass : Real default = 1500.0;
    part wheels : Wheel[4] { attribute :>> diameter = 0.65; }
  }
  part sedan : Car { attribute :>> mass = 1800.0; }
}

The next cell sees Vehicles; a bare expression evaluates against the model, and the result is the cell's output:

Vehicles::sedan.mass + 100.0
%instantiate Vehicles::sedan
%features Vehicles::sedan

Redeclaring a package replaces the earlier declaration, as %load at the prompt does, and instances built from it are rebuilt on their next use. %clear starts the session over; %quit is noted rather than acted on — the notebook's own shutdown ends the kernel.

Everything the prompt prints is the cell's standard output. A declaration that does not parse, a command that fails, or an expression that cannot be evaluated is an error output with the prompt's message, and the cells queued behind it (as when running the whole notebook) are skipped, as notebooks expect.

As at the prompt, a failed cell does not roll the session back: what the session accepted before the error stays, and a declaration refused for a semantic error is still in the model with that error reported. %clear (or Restart Kernel) starts over.

Tab completes the way the prompt's completer does: % commands, their arguments, and qualified names in the model. Shift+Tab on a name shows what %print would print for it. A notebook runs a cell on Shift+Enter whatever it holds; it is jupyter console that asks the kernel whether the input is complete, and there Enter on an unfinished declaration (an unclosed brace) reads another line rather than running it.

Drawing an element

%viz draws any named element on demand, with no view declared, in the grammar of the OMG pilot kernel's %viz, so a notebook written for the pilot runs unchanged:

%viz [--view=<VIEW>] [--style=<STYLE>...] [<form>] <NAME> [<NAME>...]
%viz Vehicles::Car
%viz --view Tree --style LR --style ortholine Vehicles::Car Vehicles::Wheel
%viz --view STATE Vehicles::Lamp

VIEW is DEFAULT, TREE, INTERCONNECTION, STATE, ACTION, SEQUENCE, MIXED or CASE, in any letter case. DEFAULT — the view when --view is absent — chooses the rendering from what the names resolve to: a state def or usage draws a state diagram, an action def or usage an action diagram, a case def or usage a case diagram, a part or other structural usage holding a connection, binding or flow an interconnection diagram, and a definition, a package or a usage with nothing to connect a tree; names calling for different diagrams draw a mixed one. Several names draw in one diagram. Names resolve as %render's do: qualified, or simple and in scope.

Each --style is a direction (TB, LR, RL, BT), a drawing style (pilot, cameo), a palette (okabe-ito, viridis, …) or a port display (minimal, full). The pilot's other styles (ORTHOLINE, POLYLINE, COMPTREE, SHOWINHERITED, …) are accepted and noted in the diagram as not drawn, so a pilot notebook runs and nothing is dropped silently; PUMLCODE asks for the PlantUML source, as it does in the pilot. An unknown view or style is refused with the list.

With no form named, a cell shows the diagram — as Mermaid, and as an SVG drawn from DOT when Graphviz is installed. A form (text, mermaid, dot, plantuml, d2) shows that form, as %render does. %viz is the pilot's spelling of a pseudo-view: %viz --view STATE P::Lamp draws what %render #state:P::Lamp mermaid writes, and %viz P::Car P::Lamp what a view exposing both would render. At the sysml prompt the same command prints the text rendering.

Reusing another notebook

Jupyter has no import between notebooks. %load has: a path ending in .ipynb loads that notebook's code cells into the session, in notebook order, as if their declarations had been run here.

%load wheels.ipynb
loaded wheels.ipynb: 3 of 4 code cells, 2 declarations
  skipped 2 % command lines and 3 expression lines: a loaded notebook declares; its commands are not run and its expressions not evaluated
  skipped cell 4: tagged skip-load
✓ package Wheels
✓ package Cars

What is loaded is the model: every code cell's declarations, split as the kernel splits a cell. Markdown and raw cells are not code. A cell's % command lines and its bare expressions are skipped — a notebook you load must not run its author's %sweep, %save or %load, nor spend your session evaluating its expressions — and the report counts what it passed over. A cell tagged skip-load (Jupyter's cell tags, in the cell's metadata) is skipped whole; tag the scratch cells of a notebook others load. The lines of a cell keep their numbers: an error in a loaded cell is reported as wheels.ipynb cell 3:2:5 — the notebook, the cell's position among the code cells, then the line and column within the cell — and %print of a loaded name still finds its text.

To load part of a notebook, name the cells:

%load wheels.ipynb --cells 1,3-5
%load wheels.ipynb --cells tag:model

--cells takes positions among the code cells, counted from 1, as single numbers and ranges, or tag:<tag> for the cells carrying a tag; one --cells applies to every notebook the same %load names, and may be written anywhere among the paths (--cells=tag:model too). A position past the notebook's last code cell is refused with the notebook's code-cell count; a tag no cell carries is refused too.

Loading a notebook again redeclares it, as loading a file again does: what an earlier load of the whole notebook declared and the notebook no longer holds is gone. Loading picked cells replaces just those cells and leaves the others as they were.

Only a SysML notebook loads: one whose kernel language (metadata.kernelspec.language, else metadata.language_info.name) is sysml, or that records none. A Python notebook is refused with cannot load analysis.ipynb: a python notebook; a file that is not nbformat 4 — an older nbformat 3 notebook, or a file that is no notebook at all — is refused with the reason.

Notebooks load wherever model files do: %load notebooks/ and %load '*.ipynb' pick them up beside .sysml files, sysml wheels.ipynb loads one on the command line, and a notebook's imports are followed to the files beside it as a loaded file's are.

Rich output

Output that has a richer form than text is sent in that form beside the text, and the front end shows the richest it can:

Command Shown as
%viz <name> [<name>...] The diagram: a Mermaid diagram, and an SVG drawing when Graphviz is installed
%viz <form> <name> What %render <view> <form> shows
%render <view> mermaid A Mermaid diagram (JupyterLab 4.1 and later draw it)
%render <view> dot An SVG drawing when Graphviz is installed; otherwise the DOT source
%render <view> markdown Rendered Markdown
%render <view> csv, tsv A table, where the front end renders tables
%render-document <doc> The document as rendered Markdown
%render-document <doc> html The document as HTML, with the table and figure styling of the HTML backend
%features <instance> json JSON, shown in the front end's tree view

%render <view> plantuml and d2 print their source, for a tool that draws them. See views and rendering for the forms, and the REPL commands reference for every command.

Interrupting and restarting

Interrupt (the stop button, or I I) stops the cell: a run the cell drives — %continue, %step, a sweep, a solver query — ends at its next step with KeyboardInterrupt, and the next cell runs on with the model unchanged. A declaration being analysed or a file being read is not interruptible and ends on its own.

Restart starts an empty session, as a fresh sysml would; Shutdown ends the kernel. Both are the front end's, through the protocol; the kernel also ends on SIGTERM.

The bounds the REPL takes from the environment (OPENSYSML_MAX_STEPS, OPENSYSML_MAX_ACTION_STEPS, OPENSYSML_JOBS, OPENSYSML_TOOLS and the rest) apply to the kernel, read when it starts: set them in the environment of the notebook server. See environment variables.

Working with a repository

The kernel round-trips models with a SysML v2 API server through the same four commands the OMG pilot's kernel has: %repo shows or sets the server, %projects lists its projects, %load reads a project's model into the session and %publish writes elements back as a new project or a commit. Any server speaking the standard API serves: the pilot's SysML-v2-API-Services wants no token and the default URL is its http://localhost:9000 when %repo names it; Flexo MMS wants a bearer token and an organization. Set, in the environment of the notebook server:

Variable For the pilot API server For Flexo MMS
FLEXO_SYSMLV2_URL http://localhost:9000 the SysML v2 API endpoint, by default http://localhost:8083
FLEXO_INTEROP_TOKEN unset the bearer token; never put it in a cell
FLEXO_SYSMLV2_ORG unset the organization, by default sysmlv2
FLEXO_ALLOW_PLAIN_HTTP unset on this machine 1 to allow a plaintext http:// server on another, whether FLEXO_SYSMLV2_URL or %repo names it; the token still never follows a redirect or a next-page link to another server

Then, as the pilot's notebooks do:

%repo http://localhost:9000
%projects
%load --name=Vehicles --branch=main
part def Truck :> Vehicles::Car;
%publish --project=Vehicles Truck
%publish -d --project="Trucks only" --branch=main Truck

%load takes the project by name, by --id or by --name, the branch by name or id or the default when absent, and submits the model as a document, so the cells after it refer to the loaded names; the session remembers which project, branch and commit, and a %publish of what it loaded is a commit on that branch, not a second project. %publish <name> names the project after the element's own name unless --project says otherwise; a project of that name receives a commit of what changed against the branch head, reported as the commit id with the counts created, updated and deleted, and no project of that name is created. The commit stays within its root: what the branch holds under other roots is left in place, and elements are deleted only from a branch the session loaded or published, since only then has it seen them; what was left alone is reported. -d sends every derived property the exporter computes, as the pilot's -d does. An argument problem is a UsageError; a server that cannot be reached, a missing project or branch, or a refused commit a CommandError with the status and the server's message. A project name two projects share is refused naming both ids: use --id. The REPL commands reference has each command's grammar.

Troubleshooting

  • The kernel is not listed. jupyter kernelspec list shows what the server sees; the spec must be under a path on that list. The wheel registers the kernel under the prefix of the python that installed it, so install with the Python that runs the notebook server, or run python -m jupyter_opensysml_kernel install --user (or --prefix) to register it where the server looks.
  • pip install fetched the sdist. There is no wheel for the platform, so the package carries no binary: python -m jupyter_opensysml_kernel install downloads the release's and registers it, or --binary registers one built from source.
  • The install refuses the download. The package pins the digests of its own release and verifies what it fetched against them; a mismatch is reported and nothing is written. Behind a mirror, download the release asset by hand, verify it against the release's SHA256SUMS.txt, and register it with --binary.
  • A cell hangs. Interrupt it. A run runs until it ends or its step budget is spent; set OPENSYSML_MAX_ACTION_STEPS lower, or use %step to drive it a step at a time.
  • Diagrams show as source. Mermaid is drawn by JupyterLab 4.1 and later, and by Notebook 7.1 and later; DOT is drawn only where Graphviz is installed on the kernel's machine.
  • Cells are not highlighted. jupyter labextension list must show jupyterlab-opensysml enabled; it is shared data of the package, so it is found under the prefix of the Python that runs JupyterLab or Notebook — install the package with that Python. A notebook server started before the install needs a restart, and a browser tab a reload. JupyterLab 3 and the classic Notebook do not load JupyterLab 4 extensions.

The protocol the kernel speaks, what kernel.json holds, and every option are in the Jupyter kernel reference.