diff --git a/.gitignore b/.gitignore
index 0d27eb1c..448db6ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,7 +83,7 @@ target/
# Jupyter Notebook
.ipynb_checkpoints
PPO_UC2/
-docs/_static/notebooks/html/*.html
+docs/source/notebooks/*.ipynb
# IPython
profile_default/
diff --git a/README.md b/README.md
index 2265538a..3fd73b53 100644
--- a/README.md
+++ b/README.md
@@ -137,7 +137,22 @@ python -m jupyter notebook
```
Then, click the URL provided by the jupyter command to open the jupyter application in your browser. You can also open notebooks in your IDE if supported.
-## 📚 Building documentation
+## 📚 Documentation
+
+### Pre requisites
+
+Building the documentation requires the installation of Pandoc
+
+##### Unix
+```bash
+sudo apt-get install pandoc
+```
+
+##### Other operating systems
+Follow the steps in https://pandoc.org/installing.html
+
+### Building the documentation
+
The PrimAITE documentation can be built with the following commands:
##### Unix
diff --git a/docs/Makefile b/docs/Makefile
index bd4ef1db..2346738f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -6,8 +6,6 @@ SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
-JUPYTEROUTPUTPATH="_static\notebooks\html"
-
# Remove command is different depending on OS
ifdef OS
RM = IF exist $(AUTOSUMMARY) ( RMDIR $(AUTOSUMMARY) /s /q )
@@ -31,8 +29,4 @@ clean:
%: Makefile | clean
pip-licenses --format=rst --with-urls --output-file=source/primaite-dependencies.rst
- jupyter nbconvert --execute --to html --output-dir _static/notebooks/html ../src/primaite/**/*.ipynb
-
- cp -r ../src/primaite/notebooks/_package_data _static/notebooks/html/_package_data _static/notebooks/html/_package_data
-
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/conf.py b/docs/conf.py
index 33e192aa..008c23a1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -9,7 +9,9 @@ import datetime
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
+import shutil
import sys
+from pathlib import Path
from typing import Any
import furo # noqa
@@ -43,11 +45,12 @@ html_title = f"{project} v{release} docs"
# ones.
extensions = [
"sphinx.ext.autodoc", # Core Sphinx library for auto html doc generation from docstrings
- "sphinx.ext.autosummary", # Create summary tables for modules/classes/methods etc
+ # "sphinx.ext.autosummary", # Create summary tables for modules/classes/methods etc
"sphinx.ext.intersphinx", # Link to other project's documentation (see mapping below)
"sphinx.ext.viewcode", # Add a link to the Python source code for classes, functions etc.
"sphinx.ext.todo",
"sphinx_copybutton", # Adds a copy button to code blocks
+ "nbsphinx",
]
templates_path = ["_templates"]
@@ -64,33 +67,7 @@ html_theme = "furo"
html_static_path = ["_static"]
html_theme_options = {"globaltoc_collapse": True, "globaltoc_maxdepth": 2}
html_copy_source = False
-
-
-def get_notebook_links() -> str:
- """
- Returns a string which will be added to the RST.
-
- Allows for dynamic addition of notebooks to the documentation.
- """
- notebooks = os.listdir("_static/notebooks/html")
-
- links = []
- links.append("
")
- for notebook in notebooks:
- if notebook == "notebook_links.html":
- continue
- notebook_link = (
- f'- '
- f"{notebook.replace('.html', '')}"
- f"
\n"
- )
- links.append(notebook_link)
- links.append("")
-
- with open("_static/notebooks/html/notebook_links.html", "w") as html_file:
- html_file.write("".join(links))
-
- return ":file: ../_static/notebooks/html/notebook_links.html"
+nbsphinx_allow_errors = True
def replace_token(app: Any, docname: Any, source: Any):
@@ -103,12 +80,26 @@ def replace_token(app: Any, docname: Any, source: Any):
tokens = {
"{VERSION}": release,
- "{NOTEBOOK_LINKS}": get_notebook_links(),
} # Token VERSION is replaced by the value of the PrimAITE version in the version file
"""Dict containing the tokens that need to be replaced in documentation."""
+temp_ignored_notebooks = ["Training-an-RLLib-Agent.ipynb", "Training-an-RLLIB-MARL-System.ipynb"]
+
+
+def copy_notebooks_to_docs():
+ """Copies the notebooks to a directory within docs directory so that they can be included."""
+ for notebook in Path("../src/primaite").rglob("*.ipynb"):
+ if notebook.name not in temp_ignored_notebooks:
+ dest = Path("source") / "notebooks"
+ Path(dest).mkdir(parents=True, exist_ok=True)
+ shutil.copy2(src=notebook, dst=dest)
+
+ # copy any images
+ # TODO
+
def setup(app: Any):
"""Custom setup for sphinx."""
+ copy_notebooks_to_docs()
app.add_config_value("tokens", {}, True)
app.connect("source-read", replace_token)
diff --git a/docs/index.rst b/docs/index.rst
index a03c857f..a0f302e9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -115,7 +115,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE!
:hidden:
source/example_notebooks
- source/executed_notebooks
+ source/notebooks/executed_notebooks
.. toctree::
:caption: Developer information:
diff --git a/docs/make.bat b/docs/make.bat
index 6989b67c..7c1cf0cc 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -9,9 +9,6 @@ REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
-if "%JUPYTER%" == "" (
- set JUPYTER=jupyter
-)
set SOURCEDIR=.
set BUILDDIR=_build
@@ -31,35 +28,14 @@ if errorlevel 9009 (
exit /b 1
)
-%JUPYTER% >NUL 2>NUL
-if errorlevel 9009 (
- echo.
- echo.'jupyter' command was not found. Make sure you have Jupyter
- echo.installed, then set the JUPYTER environment variable to point
- echo.to the full path of the 'jupyter' executable.
- exit /b 1
-)
-
if "%1" == "" goto help
REM delete autosummary if it exists
-IF EXIST %AUTOSUMMARYDIR% (
- echo deleting %AUTOSUMMARYDIR%
- RMDIR %AUTOSUMMARYDIR% /s /q
-)
-
-REM delete notebook if it exists
-IF EXIST %JUPYTEROUTPUTPATH% (
- echo deleting %JUPYTEROUTPUTPATH%
- RMDIR %JUPYTEROUTPUTPATH% /s /q
-)
-
-REM run and print html of notebooks
-JUPYTER nbconvert --execute --to html --output-dir %JUPYTEROUTPUTPATH% "%cd%\..\src\primaite\**\*.ipynb"
-
-REM copy notebook image dependencies
-robocopy ..\src\primaite\notebooks\_package_data _static\notebooks\html\_package_data
+@REM IF EXIST %AUTOSUMMARYDIR% (
+@REM echo deleting %AUTOSUMMARYDIR%
+@REM RMDIR %AUTOSUMMARYDIR% /s /q
+@REM )
REM print the YT licenses
set LICENSEBUILD=pip-licenses --format=rst --with-urls
@@ -74,15 +50,10 @@ goto end
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:clean
-IF EXIST %AUTOSUMMARYDIR% (
- echo deleting %AUTOSUMMARYDIR%
- RMDIR %AUTOSUMMARYDIR% /s /q
-)
-
-IF EXIST %JUPYTEROUTPUTPATH% (
- echo deleting %JUPYTEROUTPUTPATH%
- RMDIR %JUPYTEROUTPUTPATH% /s /q
-)
+@REM IF EXIST %AUTOSUMMARYDIR% (
+@REM echo deleting %AUTOSUMMARYDIR%
+@REM RMDIR %AUTOSUMMARYDIR% /s /q
+@REM )
:end
popd
diff --git a/docs/source/executed_notebooks.rst b/docs/source/notebooks/executed_notebooks.rst
similarity index 82%
rename from docs/source/executed_notebooks.rst
rename to docs/source/notebooks/executed_notebooks.rst
index 849529f2..f785a598 100644
--- a/docs/source/executed_notebooks.rst
+++ b/docs/source/notebooks/executed_notebooks.rst
@@ -9,5 +9,8 @@ Executed Jupyter Notebooks
Below is a list of available pre-executed notebooks.
-.. raw:: html
- {NOTEBOOK_LINKS}
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ *
diff --git a/pyproject.toml b/pyproject.toml
index 2619da90..723947ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,7 +41,8 @@ dependencies = [
"typer[all]==0.9.0",
"pydantic==2.7.0",
"ray[rllib] >= 2.9, < 3",
- "ipywidgets"
+ "ipywidgets",
+ "nbsphinx==0.9.4"
]
[tool.setuptools.dynamic]
diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb
index de473a7b..1b016bb8 100644
--- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb
+++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb
@@ -126,7 +126,13 @@
"There are two important parts of the YAML config for varying red agent behaviour.\n",
"\n",
"### Red agent settings\n",
- "Here is an annotated config for the red agent in the data manipulation scenario.\n",
+ "Here is an annotated config for the red agent in the data manipulation scenario."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
"```yaml\n",
" - ref: data_manipulation_attacker # name of agent\n",
" team: RED # not used, just for human reference\n",
@@ -171,10 +177,21 @@
" start_step: 25 # first attack at step 25\n",
" frequency: 20 # attacks will happen every 20 steps (on average)\n",
" variance: 5 # the timing of attacks will vary by up to 5 steps earlier or later\n",
- "```\n",
- "\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
"### Malicious application settings\n",
- "The red agent uses an application called `DataManipulationBot` which leverages a node's `DatabaseClient` to send a malicious SQL query to the database server. Here's an annotated example of how this is configured in the yaml *(with impertinent config items omitted)*:\n",
+ "The red agent uses an application called `DataManipulationBot` which leverages a node's `DatabaseClient` to send a malicious SQL query to the database server. Here's an annotated example of how this is configured in the yaml *(with impertinent config items omitted)*:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
"```yaml\n",
"simulation:\n",
" network:\n",
diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb
index 26283ae9..f09f9ea7 100644
--- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb
+++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb
@@ -160,139 +160,158 @@
"### Mappings\n",
"\n",
"The dict keys for `node_id` are in the following order:\n",
- "|node_id|node name|\n",
- "|--|--|\n",
- "|1|domain_controller|\n",
- "|2|web_server|\n",
- "|3|database_server|\n",
- "|4|backup_server|\n",
- "|5|security_suite|\n",
- "|6|client_1|\n",
- "|7|client_2|\n",
+ "\n",
+ "| node_id | node name |\n",
+ "|---------|------------------|\n",
+ "| 1 | domain_controller|\n",
+ "| 2 | web_server |\n",
+ "| 3 | database_server |\n",
+ "| 4 | backup_server |\n",
+ "| 5 | security_suite |\n",
+ "| 6 | client_1 |\n",
+ "| 7 | client_2 |\n",
"\n",
"Service 1 on node 2 (web_server) corresponds to the Web Server service. Other services are only there for padding to ensure that each node's observation space has the same shape. They are filled with zeroes.\n",
"\n",
"Folder 1 on node 3 corresponds to the database folder. File 1 in that folder corresponds to the database storage file. Other files and folders are only there for padding to ensure that each node's observation space has the same shape. They are filled with zeroes.\n",
"\n",
"The dict keys for `link_id` are in the following order:\n",
- "|link_id|endpoint_a|endpoint_b|\n",
- "|--|--|--|\n",
- "|1|router_1|switch_1|\n",
- "|1|router_1|switch_2|\n",
- "|1|switch_1|domain_controller|\n",
- "|1|switch_1|web_server|\n",
- "|1|switch_1|database_server|\n",
- "|1|switch_1|backup_server|\n",
- "|1|switch_1|security_suite|\n",
- "|1|switch_2|client_1|\n",
- "|1|switch_2|client_2|\n",
- "|1|switch_2|security_suite|\n",
+ "\n",
+ "| link_id | endpoint_a | endpoint_b |\n",
+ "|---------|------------------|-------------------|\n",
+ "| 1 | router_1 | switch_1 |\n",
+ "| 1 | router_1 | switch_2 |\n",
+ "| 1 | switch_1 | domain_controller |\n",
+ "| 1 | switch_1 | web_server |\n",
+ "| 1 | switch_1 | database_server |\n",
+ "| 1 | switch_1 | backup_server |\n",
+ "| 1 | switch_1 | security_suite |\n",
+ "| 1 | switch_2 | client_1 |\n",
+ "| 1 | switch_2 | client_2 |\n",
+ "| 1 | switch_2 | security_suite |\n",
+ "\n",
"\n",
"The ACL rules in the observation space appear in the same order that they do in the actual ACL. Though, only the first 10 rules are shown, there are default rules lower down that cannot be changed by the agent. The extra rules just allow the network to function normally, by allowing pings, ARP traffic, etc.\n",
"\n",
"Most nodes have only 1 network_interface, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n",
"\n",
"The meaning of the services' operating_state is:\n",
- "|operating_state|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|RUNNING|\n",
- "|2|STOPPED|\n",
- "|3|PAUSED|\n",
- "|4|DISABLED|\n",
- "|5|INSTALLING|\n",
- "|6|RESTARTING|\n",
+ "\n",
+ "| operating_state | label |\n",
+ "|-----------------|------------|\n",
+ "| 0 | UNUSED |\n",
+ "| 1 | RUNNING |\n",
+ "| 2 | STOPPED |\n",
+ "| 3 | PAUSED |\n",
+ "| 4 | DISABLED |\n",
+ "| 5 | INSTALLING |\n",
+ "| 6 | RESTARTING |\n",
"\n",
"The meaning of the services' health_state is:\n",
- "|health_state|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|GOOD|\n",
- "|2|FIXING|\n",
- "|3|COMPROMISED|\n",
- "|4|OVERWHELMED|\n",
+ "\n",
+ "| health_state | label |\n",
+ "|--------------|-------------|\n",
+ "| 0 | UNUSED |\n",
+ "| 1 | GOOD |\n",
+ "| 2 | FIXING |\n",
+ "| 3 | COMPROMISED |\n",
+ "| 4 | OVERWHELMED |\n",
+ "\n",
"\n",
"The meaning of the files' and folders' health_state is:\n",
- "|health_state|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|GOOD|\n",
- "|2|COMPROMISED|\n",
- "|3|CORRUPT|\n",
- "|4|RESTORING|\n",
- "|5|REPAIRING|\n",
+ "\n",
+ "| health_state | label |\n",
+ "|--------------|-------------|\n",
+ "| 0 | UNUSED |\n",
+ "| 1 | GOOD |\n",
+ "| 2 | COMPROMISED |\n",
+ "| 3 | CORRUPT |\n",
+ "| 4 | RESTORING |\n",
+ "| 5 | REPAIRING |\n",
+ "\n",
"\n",
"The meaning of the NICs' operating_status is:\n",
- "|operating_status|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|ENABLED|\n",
- "|2|DISABLED|\n",
+ "\n",
+ "| operating_status | label |\n",
+ "|------------------|----------|\n",
+ "| 0 | UNUSED |\n",
+ "| 1 | ENABLED |\n",
+ "| 2 | DISABLED |\n",
+ "\n",
"\n",
"NMNE (number of malicious network events) means, for inbound or outbound traffic, means:\n",
- "|value|NMNEs|\n",
- "|--|--|\n",
- "|0|None|\n",
- "|1|1 - 5|\n",
- "|2|6 - 10|\n",
- "|3|More than 10|\n",
+ "\n",
+ "| value | NMNEs |\n",
+ "|-------|----------------|\n",
+ "| 0 | None |\n",
+ "| 1 | 1 - 5 |\n",
+ "| 2 | 6 - 10 |\n",
+ "| 3 | More than 10 |\n",
+ "\n",
"\n",
"Link load has the following meaning:\n",
- "|load|percent utilisation|\n",
- "|--|--|\n",
- "|0|exactly 0%|\n",
- "|1|0-11%|\n",
- "|2|11-22%|\n",
- "|3|22-33%|\n",
- "|4|33-44%|\n",
- "|5|44-55%|\n",
- "|6|55-66%|\n",
- "|7|66-77%|\n",
- "|8|77-88%|\n",
- "|9|88-99%|\n",
- "|10|exactly 100%|\n",
+ "\n",
+ "| load | percent utilisation |\n",
+ "|------|---------------------|\n",
+ "| 0 | exactly 0% |\n",
+ "| 1 | 0-11% |\n",
+ "| 2 | 11-22% |\n",
+ "| 3 | 22-33% |\n",
+ "| 4 | 33-44% |\n",
+ "| 5 | 44-55% |\n",
+ "| 6 | 55-66% |\n",
+ "| 7 | 66-77% |\n",
+ "| 8 | 77-88% |\n",
+ "| 9 | 88-99% |\n",
+ "| 10 | exactly 100% |\n",
+ "\n",
"\n",
"ACL permission has the following meaning:\n",
- "|permission|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|ALLOW|\n",
- "|2|DENY|\n",
+ "\n",
+ "| permission | label |\n",
+ "|------------|--------|\n",
+ "| 0 | UNUSED |\n",
+ "| 1 | ALLOW |\n",
+ "| 2 | DENY |\n",
+ "\n",
"\n",
"ACL source / destination node ids actually correspond to IP addresses (since ACLs work with IP addresses)\n",
- "|source / dest node id|ip_address|label|\n",
- "|--|--|--|\n",
- "|0| | UNUSED|\n",
- "|1| |ALL addresses|\n",
- "|2| 192.168.1.10 | domain_controller|\n",
- "|3| 192.168.1.12 | web_server \n",
- "|4| 192.168.1.14 | database_server|\n",
- "|5| 192.168.1.16 | backup_server|\n",
- "|6| 192.168.1.110 | security_suite (eth-1)|\n",
- "|7| 192.168.10.21 | client_1|\n",
- "|8| 192.168.10.22 | client_2|\n",
- "|9| 192.168.10.110| security_suite (eth-2)|\n",
+ "\n",
+ "| source / dest node id | ip_address | label |\n",
+ "|-----------------------|----------------|-------------------------|\n",
+ "| 0 | | UNUSED |\n",
+ "| 1 | | ALL addresses |\n",
+ "| 2 | 192.168.1.10 | domain_controller |\n",
+ "| 3 | 192.168.1.12 | web_server |\n",
+ "| 4 | 192.168.1.14 | database_server |\n",
+ "| 5 | 192.168.1.16 | backup_server |\n",
+ "| 6 | 192.168.1.110 | security_suite (eth-1) |\n",
+ "| 7 | 192.168.10.21 | client_1 |\n",
+ "| 8 | 192.168.10.22 | client_2 |\n",
+ "| 9 | 192.168.10.110 | security_suite (eth-2) |\n",
+ "\n",
"\n",
"ACL source / destination port ids have the following encoding:\n",
- "|port id|port number| port use |\n",
- "|--|--|--|\n",
- "|0||UNUSED|\n",
- "|1||ALL|\n",
- "|2|219|ARP|\n",
- "|3|53|DNS|\n",
- "|4|80|HTTP|\n",
- "|5|5432|POSTGRES_SERVER|\n",
+ "\n",
+ "| port id | port number | port use |\n",
+ "|---------|-------------|-----------------|\n",
+ "| 0 | | UNUSED |\n",
+ "| 1 | | ALL |\n",
+ "| 2 | 219 | ARP |\n",
+ "| 3 | 53 | DNS |\n",
+ "| 4 | 80 | HTTP |\n",
+ "| 5 | 5432 | POSTGRES_SERVER |\n",
+ "\n",
"\n",
"ACL protocol ids have the following encoding:\n",
- "|protocol id|label|\n",
- "|--|--|\n",
- "|0|UNUSED|\n",
- "|1|ALL|\n",
- "|2|ICMP|\n",
- "|3|TCP|\n",
- "|4|UDP|\n",
"\n",
- "protocol"
+ "| protocol id | label |\n",
+ "|-------------|-------|\n",
+ "| 0 | UNUSED|\n",
+ "| 1 | ALL |\n",
+ "| 2 | ICMP |\n",
+ "| 3 | TCP |\n",
+ "| 4 | UDP |\n"
]
},
{
diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb
index 76623697..43d80be6 100644
--- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb
+++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb
@@ -88,13 +88,6 @@
" param_space=config\n",
").fit()"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb
index 60737ee5..21fa4f44 100644
--- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb
+++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb
@@ -79,13 +79,6 @@
" param_space=config\n",
").fit()\n"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb
index 34d6d9b5..b0669472 100644
--- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb
+++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb
@@ -38,6 +38,13 @@
"YAML file with a relative path to the base scenario and a list of paths to be loaded in during each episode.\n",
"\n",
"It takes the following format:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
"```yaml\n",
"base_scenario: base.yaml\n",
"schedule:\n",
@@ -47,8 +54,7 @@
" 1: # list of variations to load in at episode 1 (after the first env.reset() call)\n",
" - laydown_2.yaml\n",
" - attack_2.yaml\n",
- "```\n",
- "\n"
+ "```\n"
]
},
{
diff --git a/src/primaite/notebooks/multi-processing.ipynb b/src/primaite/notebooks/multi-processing.ipynb
index 71addce6..3ac6f4fa 100644
--- a/src/primaite/notebooks/multi-processing.ipynb
+++ b/src/primaite/notebooks/multi-processing.ipynb
@@ -140,7 +140,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.11"
+ "version": "3.8.10"
}
},
"nbformat": 4,
diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb
index 107a2565..0a048b72 100644
--- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb
+++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb
@@ -11,10 +11,11 @@
]
},
{
- "cell_type": "raw",
+ "cell_type": "markdown",
"id": "1",
"metadata": {},
"source": [
+ "``` text\n",
" +------------+\n",
" | domain_ |\n",
" +------------+ controller |\n",
@@ -43,7 +44,8 @@
" | | | | backup_ |\n",
" +------------+ +------------+ server |\n",
" | |\n",
- " +------------+"
+ " +------------+\n",
+ "```"
]
},
{