diff --git a/.readthedocs.yml b/.readthedocs.yml
index 9130356..7dca34d 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -11,7 +11,8 @@ build:
apt_packages:
- cmake
tools:
- python: "3.10"
+ python: "3.11"
+ nodejs: "20"
# Build documentation in the docs/ directory with Sphinx
sphinx:
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 85e42cb..66ca12e 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,95 +1,4 @@
Contributing
============
-This is an instruction of the developer tools that help you contributing.
-
-Running tests
--------------
-
-Tests can be with run using
-
-.. code-block:: bash
-
- tox -e tests
-
-You can source the test environment of tox after a run using
-
-.. code-block:: bash
-
- source .tox/tests/bin/activate
-
-Converting notebook test files
-##############################
-
-The we store the notebooks as converted Python file using `jupytext` for the conversion
-for better versioning
-
-.. code-block:: bash
-
- jupytext tests/notebooks/*.py --to ipynb
-
-Be aware that when runnng the tests with tox all `*.ipynb` are overwriten by their
-corresponding `*.py` file. For example, the file `test_widget_cue_box.ipynb` is
-overwritten by the conversion of `test_widget_cue_box.py` when running the test.
-
-
-Running with UI
-###############
-
-We use selenium to test the widgets on user actions (like a button click). To run it in
-in the CI where no display is available. We run the browsers in headless model to not
-load the UI. For debugging a test however, it is often benificial to see what is
-happening in the window. To run the tests with the browser UI please use
-
-.. code-block:: bash
-
- pytest --driver Firefox
-
-Port issues
-###########
-
-For the notebook server we fixed the port to 8815, if this port is not available you .
-It can happen that notebook process is not properly killed and remains as zombie
-process. You can check if a notebook with this port is already running using
-
-.. code-block:: bash
-
- jupyter notebook list | grep 8815
-
-or if some process is using it
-
-.. code-block:: bash
-
- netstat -l | grep 8815
-
-Formatting code
----------------
-
-Your code can be formatted using
-
-.. code-block:: bash
-
- tox -e format
-
-The formatting should fix most issues with linting, but sometimes you need to manually
-fix some issues. To run the linter use
-
-.. code-block:: bash
-
- tox -e lint
-
-
-Building documentation
-----------------------
-
-To build the docs please use
-
-.. code-block:: bash
-
- tox -e docs
-
-To open the doc with for example firefox you can run
-
-.. code-block:: bash
-
- firefox docs/build/html/index.html
+Please read the `developer documentation `_.
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..6cd7876
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,46 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+scwidgets = {file = "."}
+anywidget = "==0.9.13"
+asttokens = "==3.0.0"
+comm = "==0.2.2"
+contourpy = "==1.3.1"
+cycler = "==0.12.1"
+decorator = "==5.1.1"
+executing = "==2.1.0"
+fonttools = "==4.55.3"
+ipython = "==8.30.0"
+ipywidgets = "==8.1.5"
+jedi = "==0.19.2"
+jupyterlab-widgets = "==3.0.13"
+kiwisolver = "==1.4.7"
+matplotlib = "==3.9.4"
+matplotlib-inline = "==0.1.7"
+numpy = "==1.26.4"
+packaging = "==24.2"
+parso = "==0.8.4"
+pexpect = "==4.9.0"
+pillow = "==11.0.0"
+prompt-toolkit = "==3.0.48"
+psygnal = "==0.11.1"
+ptyprocess = "==0.7.0"
+pure-eval = "==0.2.3"
+pygments = "==2.18.0"
+pyparsing = "==3.2.0"
+python-dateutil = "==2.9.0.post0"
+six = "==1.17.0"
+stack-data = "==0.6.3"
+termcolor = "==2.5.0"
+traitlets = "==5.14.3"
+typing-extensions = "==4.12.2"
+wcwidth = "==0.2.13"
+widgetsnbextension = "==4.0.13"
+
+[dev-packages]
+
+[requires]
+python_version = "3.11"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..495c523
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,710 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "0e04da4d7d5d4391e36df94242e784acfbbdc0564625db7bd157619a933915dd"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.11"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "anywidget": {
+ "hashes": [
+ "sha256:43d1658f1043b8c95cd350b2f5deccb123fd37810a36f656d6163aefe8163705",
+ "sha256:c655455bf51f82182eb23c5947d37cc41f0b1ffacaf7e2b763147a2332cb3f07"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==0.9.13"
+ },
+ "asttokens": {
+ "hashes": [
+ "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7",
+ "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==3.0.0"
+ },
+ "comm": {
+ "hashes": [
+ "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e",
+ "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==0.2.2"
+ },
+ "contourpy": {
+ "hashes": [
+ "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1",
+ "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda",
+ "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d",
+ "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509",
+ "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6",
+ "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f",
+ "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e",
+ "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751",
+ "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86",
+ "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b",
+ "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc",
+ "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546",
+ "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec",
+ "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f",
+ "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82",
+ "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c",
+ "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b",
+ "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c",
+ "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c",
+ "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53",
+ "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80",
+ "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242",
+ "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85",
+ "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124",
+ "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5",
+ "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2",
+ "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3",
+ "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d",
+ "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc",
+ "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342",
+ "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1",
+ "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1",
+ "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595",
+ "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30",
+ "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab",
+ "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3",
+ "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2",
+ "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd",
+ "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7",
+ "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277",
+ "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453",
+ "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697",
+ "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b",
+ "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454",
+ "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9",
+ "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1",
+ "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6",
+ "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291",
+ "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750",
+ "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699",
+ "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e",
+ "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81",
+ "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9",
+ "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.10'",
+ "version": "==1.3.1"
+ },
+ "cycler": {
+ "hashes": [
+ "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30",
+ "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
+ },
+ "decorator": {
+ "hashes": [
+ "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
+ "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.5'",
+ "version": "==5.1.1"
+ },
+ "executing": {
+ "hashes": [
+ "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf",
+ "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2.1.0"
+ },
+ "fonttools": {
+ "hashes": [
+ "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7",
+ "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b",
+ "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261",
+ "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0",
+ "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02",
+ "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841",
+ "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45",
+ "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4",
+ "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b",
+ "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a",
+ "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048",
+ "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90",
+ "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd",
+ "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674",
+ "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72",
+ "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c",
+ "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07",
+ "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b",
+ "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de",
+ "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926",
+ "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e",
+ "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628",
+ "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca",
+ "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29",
+ "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa",
+ "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe",
+ "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427",
+ "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d",
+ "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765",
+ "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5",
+ "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d",
+ "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314",
+ "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b",
+ "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af",
+ "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831",
+ "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3",
+ "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56",
+ "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e",
+ "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276",
+ "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0",
+ "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851",
+ "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5",
+ "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54",
+ "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b",
+ "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f",
+ "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4",
+ "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977",
+ "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f",
+ "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35",
+ "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==4.55.3"
+ },
+ "ipython": {
+ "hashes": [
+ "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321",
+ "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.10'",
+ "version": "==8.30.0"
+ },
+ "ipywidgets": {
+ "hashes": [
+ "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245",
+ "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.5"
+ },
+ "jedi": {
+ "hashes": [
+ "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0",
+ "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.6'",
+ "version": "==0.19.2"
+ },
+ "jupyterlab-widgets": {
+ "hashes": [
+ "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed",
+ "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==3.0.13"
+ },
+ "kiwisolver": {
+ "hashes": [
+ "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a",
+ "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95",
+ "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5",
+ "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0",
+ "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d",
+ "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18",
+ "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b",
+ "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258",
+ "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95",
+ "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e",
+ "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383",
+ "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02",
+ "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b",
+ "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523",
+ "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee",
+ "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88",
+ "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd",
+ "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb",
+ "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4",
+ "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e",
+ "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c",
+ "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935",
+ "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee",
+ "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e",
+ "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038",
+ "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d",
+ "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b",
+ "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5",
+ "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107",
+ "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f",
+ "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2",
+ "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17",
+ "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb",
+ "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674",
+ "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706",
+ "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327",
+ "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3",
+ "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a",
+ "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2",
+ "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f",
+ "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948",
+ "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3",
+ "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e",
+ "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545",
+ "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc",
+ "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f",
+ "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650",
+ "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a",
+ "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8",
+ "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750",
+ "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b",
+ "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34",
+ "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225",
+ "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51",
+ "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c",
+ "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3",
+ "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde",
+ "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599",
+ "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c",
+ "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76",
+ "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6",
+ "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39",
+ "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9",
+ "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933",
+ "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad",
+ "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520",
+ "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1",
+ "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503",
+ "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b",
+ "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36",
+ "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a",
+ "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643",
+ "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60",
+ "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483",
+ "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf",
+ "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d",
+ "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6",
+ "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644",
+ "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2",
+ "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9",
+ "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2",
+ "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640",
+ "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade",
+ "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a",
+ "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c",
+ "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6",
+ "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00",
+ "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27",
+ "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2",
+ "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4",
+ "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379",
+ "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54",
+ "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09",
+ "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a",
+ "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c",
+ "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89",
+ "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407",
+ "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904",
+ "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376",
+ "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583",
+ "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278",
+ "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a",
+ "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d",
+ "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935",
+ "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb",
+ "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895",
+ "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b",
+ "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417",
+ "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608",
+ "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07",
+ "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05",
+ "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a",
+ "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d",
+ "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==1.4.7"
+ },
+ "matplotlib": {
+ "hashes": [
+ "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df",
+ "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a",
+ "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca",
+ "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00",
+ "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50",
+ "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3",
+ "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3",
+ "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64",
+ "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249",
+ "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c",
+ "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099",
+ "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0",
+ "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6",
+ "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c",
+ "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45",
+ "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c",
+ "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d",
+ "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483",
+ "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965",
+ "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c",
+ "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e",
+ "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799",
+ "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041",
+ "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5",
+ "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb",
+ "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e",
+ "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b",
+ "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423",
+ "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865",
+ "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb",
+ "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db",
+ "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50",
+ "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c",
+ "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7",
+ "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff",
+ "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764",
+ "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26",
+ "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70",
+ "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac",
+ "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858",
+ "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==3.9.4"
+ },
+ "matplotlib-inline": {
+ "hashes": [
+ "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90",
+ "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==0.1.7"
+ },
+ "numpy": {
+ "hashes": [
+ "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
+ "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818",
+ "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20",
+ "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0",
+ "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010",
+ "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a",
+ "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea",
+ "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c",
+ "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71",
+ "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110",
+ "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be",
+ "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a",
+ "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a",
+ "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5",
+ "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed",
+ "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd",
+ "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c",
+ "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e",
+ "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0",
+ "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c",
+ "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a",
+ "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b",
+ "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0",
+ "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6",
+ "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2",
+ "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a",
+ "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30",
+ "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218",
+ "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5",
+ "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07",
+ "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2",
+ "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4",
+ "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764",
+ "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef",
+ "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3",
+ "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==1.26.4"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==24.2"
+ },
+ "parso": {
+ "hashes": [
+ "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18",
+ "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.6'",
+ "version": "==0.8.4"
+ },
+ "pexpect": {
+ "hashes": [
+ "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523",
+ "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"
+ ],
+ "index": "pypi",
+ "version": "==4.9.0"
+ },
+ "pillow": {
+ "hashes": [
+ "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7",
+ "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5",
+ "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903",
+ "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2",
+ "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38",
+ "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2",
+ "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9",
+ "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f",
+ "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc",
+ "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8",
+ "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d",
+ "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2",
+ "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316",
+ "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a",
+ "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25",
+ "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd",
+ "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba",
+ "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc",
+ "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273",
+ "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa",
+ "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a",
+ "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b",
+ "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a",
+ "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae",
+ "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291",
+ "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97",
+ "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06",
+ "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904",
+ "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b",
+ "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b",
+ "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8",
+ "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527",
+ "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947",
+ "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb",
+ "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003",
+ "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5",
+ "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f",
+ "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739",
+ "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944",
+ "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830",
+ "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f",
+ "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3",
+ "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4",
+ "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84",
+ "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7",
+ "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6",
+ "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6",
+ "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9",
+ "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de",
+ "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4",
+ "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47",
+ "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd",
+ "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50",
+ "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c",
+ "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086",
+ "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba",
+ "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306",
+ "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699",
+ "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e",
+ "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488",
+ "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa",
+ "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2",
+ "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3",
+ "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9",
+ "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923",
+ "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2",
+ "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790",
+ "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734",
+ "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916",
+ "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1",
+ "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f",
+ "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798",
+ "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb",
+ "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2",
+ "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==11.0.0"
+ },
+ "prompt-toolkit": {
+ "hashes": [
+ "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90",
+ "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.0.48"
+ },
+ "psygnal": {
+ "hashes": [
+ "sha256:04255fe28828060a80320f8fda937c47bc0c21ca14f55a13eb7c494b165ea395",
+ "sha256:09c75d21eb090e2ffafb32893bc5d104b98ed237ed64bebccb45cca759c7dcf4",
+ "sha256:0b55cb42e468f3a7de75392520778604fef2bc518b7df36c639b35ce4ed92016",
+ "sha256:1c2388360a9ffcd1381e9b36d0f794287a270d58e69bf17658a194bbf86685c1",
+ "sha256:24e69ea57ee39e3677298f38a18828af87cdc0bf0aa64685d44259e608bae3ec",
+ "sha256:2deec4bf7adbb9e3ef0513ae8b9e98bb815eb62b76a7bf1986f1d6ed626c8784",
+ "sha256:36cd667dd1d3e70e3fd970463a8571436e5ae58f02cc05a4a1669e6d8550d263",
+ "sha256:3c04baec10f882cdf784a7312e23892416188417ad85607e6d1de2e8a9e70709",
+ "sha256:713dfb96a1315378ce9120376d975671ede3133de4985884a43d4b6b332faeee",
+ "sha256:7676e89225abc2f37ca7022c300ffd26fefaf21bdc894bc7c41dffbad5e969df",
+ "sha256:885922a6e65ece9ff8ccf2b6810f435ca8067f410889f7a8fffb6b0d61421a0d",
+ "sha256:8d9187700fc608abefeb287bf2e0980a26c62471921ffd1a3cd223ccc554181b",
+ "sha256:8f77317cbd11fbed5bfdd40ea41b4e551ee0cf37881cdbc325b67322af577485",
+ "sha256:c05f474b297e2577506b354132c3fed054f0444ccce6d431f299d3750c2ede4b",
+ "sha256:c392f638aac2cdc4f13fffb904455224ae9b4dbb2f26d7f3264e4208fee5334d",
+ "sha256:c7dd3cf809c9c1127d90c6b11fbbd1eb2d66d512ccd4d5cab048786f13d11220",
+ "sha256:c9dde42a2cdf34f9c5fe0cd7515e2ab1524e3207afb37d096733c7a3dcdf388a",
+ "sha256:cec87aee468a1fe564094a64bc3c30edc86ce34d7bb37ab69332c7825b873396",
+ "sha256:d77f1a71fe9859c0335c87d92afe1b17c520a4137326810e94351839342d8fc7",
+ "sha256:dc260f19349485bd58e276e731cf8be40d8891cc6ff1c165762bd2c1b84f1ff7",
+ "sha256:f9b02ca246ab0adb108c4010b4a486e464f940543201074591e50370cd7b0cc0",
+ "sha256:fe70023fe4cf8bb6a0f27e89fd8f1cf715893dfb004b790937a0bc59d9071aab"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==0.11.1"
+ },
+ "ptyprocess": {
+ "hashes": [
+ "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35",
+ "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"
+ ],
+ "index": "pypi",
+ "version": "==0.7.0"
+ },
+ "pure-eval": {
+ "hashes": [
+ "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0",
+ "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"
+ ],
+ "index": "pypi",
+ "version": "==0.2.3"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
+ "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==2.18.0"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84",
+ "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==3.2.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==2.9.0.post0"
+ },
+ "scwidgets": {
+ "git": "https://github.com/osscar-org/scicode-widgets",
+ "markers": "python_version >= '3.9'",
+ "ref": "92337fb3cf17a4cf9867bfd2acbfd47278c89048"
+ },
+ "six": {
+ "hashes": [
+ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
+ "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==1.17.0"
+ },
+ "stack-data": {
+ "hashes": [
+ "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9",
+ "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"
+ ],
+ "index": "pypi",
+ "version": "==0.6.3"
+ },
+ "termcolor": {
+ "hashes": [
+ "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8",
+ "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==2.5.0"
+ },
+ "traitlets": {
+ "hashes": [
+ "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7",
+ "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==5.14.3"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
+ "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==4.12.2"
+ },
+ "wcwidth": {
+ "hashes": [
+ "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859",
+ "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"
+ ],
+ "index": "pypi",
+ "version": "==0.2.13"
+ },
+ "widget-code-input": {
+ "git": "git+https://github.com/agoscinski/widget-code-input",
+ "markers": "python_version >= '3.9'",
+ "ref": "90f731ca929725ef3fcad3da7bcb3e3672d9eefd"
+ },
+ "widgetsnbextension": {
+ "hashes": [
+ "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71",
+ "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==4.0.13"
+ }
+ },
+ "develop": {}
+}
diff --git a/README.rst b/README.rst
index 4f6d957..c3a6cc8 100644
--- a/README.rst
+++ b/README.rst
@@ -1,19 +1,35 @@
-Important
-=========
-
-So far scicode-widget has been created by prototyping without much concern about the code quality. This resulted in faster development time but in cost of readability and maintanability of the code. Since we finished now the prototype phase and have converged on a set of functionalities we are satisfied with, we are in the process of refactoring the resulting code in this branch. While we are refactoring we recommend the usage of the `vertical-slice branch `_ till all features have been implemented in the refactor.
-
-
scicode-widgets
===============
.. marker-package-description
-A collection of ipywidgets for the creation of interactive code demos and educational notebooks with exercises that can be checked and exported.
+*scicode-widgets* is a widget library for the purpose of creating computational
+experiments for educational content. It is targeted to teach students how to
+code and interpret computational experiments while hiding technical details and
+boiler plate code that are not essential for the learning experience. The logic
+is purely written in Python to simplify the development process for scientific
+researchers as they usually are more capable in writing Python code than
+JavaScript. It therefore builds on top of widgets provided by ipywidgets to
+creating a framework out of it.
+
+Features
+--------
+
+The core features of scicode-widgets are:
+
+**Customizable coding exercises and demos**
+
+**Checks for students to verify their solution**
+
+**Automatic grading using nbgrader**
+
+Please continue with our `getting started page `_
+for a more detailed overview of our features.
-Installation
-------------
+Supportde jupyter environments
+------------------------------
-.. code-block:: bash
+We support the following widget environments
- pip install .
+* jupyterlab
+* notebook < 7
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 6966869..555c4a2 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1 +1,5 @@
+ipython
+ipykernel
sphinx
+nbsphinx
+sphinx-material
diff --git a/docs/src/api.rst b/docs/src/api.rst
new file mode 100644
index 0000000..43d0ead
--- /dev/null
+++ b/docs/src/api.rst
@@ -0,0 +1,29 @@
+API
+===
+
+.. automodule:: scwidgets
+ :members:
+
+exercise
+--------
+
+.. automodule:: scwidgets.exercise
+ :members:
+
+check
+-----
+
+.. automodule:: scwidgets.check
+ :members:
+
+code
+----
+
+.. automodule:: scwidgets.code
+ :members:
+
+cue
+---
+
+.. automodule:: scwidgets.cue
+ :members:
diff --git a/docs/src/check.ipynb b/docs/src/check.ipynb
new file mode 100644
index 0000000..160baf0
--- /dev/null
+++ b/docs/src/check.ipynb
@@ -0,0 +1,448 @@
+{
+ "cells": [
+ {
+ "cell_type": "raw",
+ "id": "0",
+ "metadata": {
+ "editable": true,
+ "raw_mimetype": "text/restructuredtext",
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ ".. important::\n",
+ " Jupyter widgets cannot be run within in documentation. To be able to interact with the widget you must run a mybinder instance. To run a mybinder instance of this notebook please use this link https://mybinder.org/v2/gh/osscar-org/scicode-widgets/HEAD?labpath=docs%2Fsrc%2Fexercises.ipynb."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {},
+ "source": [
+ "# Writing checks"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {},
+ "source": [
+ "Checks intention is to give students a way to validate their code solution. The student's code can be validated by providing a list of inputs and references comparing the output of the student's code with the references, or by directy testing certain functional behavior of the code. In cases when reference outputs need to be obfuscated the outputs that are compared can be passed through a _fingerprint_ function. This notebook goes through each of these features and presents an example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import CodeInput, CodeExercise, Check, CheckRegistry\n",
+ "\n",
+ "import numpy as np "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {},
+ "source": [
+ "Similar as for the `ExerciseRegistry` we need to define a `CheckRegistry` that registers the checks for each exercise."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "check_registry = CheckRegistry()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6",
+ "metadata": {},
+ "source": [
+ "## Checks using inputs and output references"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "def sin(arr):\n",
+ " import numpy as np\n",
+ " return np.cos(arr) # oops! wrong solution\n",
+ "\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " exercise_key=\"sinus_with_references_1\",\n",
+ " code=sin,\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "# An assert function returns a string that specifies\n",
+ "# the error message to the student, or is empty if the\n",
+ "# check passes\n",
+ "def my_assert_allclose(outputs, references) -> str:\n",
+ " if not np.allclose(outputs, references):\n",
+ " return \"Your output is not close to the references.\"\n",
+ " else: \n",
+ " return \"\" # We use empty strings means it passes\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " my_assert_allclose\n",
+ " ],\n",
+ " inputs_parameters=[{\"arr\": np.asarray([0., np.pi, 2*np.pi])}],\n",
+ " outputs_references=[(np.asarray([0., 0., 0.]),)]\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8",
+ "metadata": {},
+ "source": [
+ "Because there are asserts that are repeatedly needed for almost any kind of exercise, we provide of a set of asserts"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import (\n",
+ " assert_numpy_allclose,\n",
+ " assert_shape,\n",
+ " assert_type,\n",
+ ")\n",
+ "\n",
+ "def sinus(arr):\n",
+ " import numpy as np\n",
+ " return np.cos(arr) # oops! wrong solution\n",
+ "\n",
+ "code_input_sinus = CodeInput(sinus)\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " exercise_key=\"sinus_with_references_2\",\n",
+ " code=code_input_sinus,\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_type, # checks if same type as reference values \n",
+ " assert_shape, # checks if same shape as reference values\n",
+ " assert_numpy_allclose, # checks if allclose to reference values\n",
+ " ],\n",
+ " inputs_parameters=[{\"arr\": np.asarray([0., 0.78539816, 1.57079633, 2.35619449, 3.14159265])}],\n",
+ " outputs_references=[(np.asarray([0., 7.07106781e-01, 1.00000000e+00, 7.07106781e-01, 0.]),)]\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(code_input_sinus.full_function_code)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11",
+ "metadata": {},
+ "source": [
+ "One can adapt the default arguments by using partial functions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "assert_numpy_allclose?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "13",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from functools import partial\n",
+ "\n",
+ "custom_assert_numpy_allclose = partial(assert_numpy_allclose, rtol=1e-7)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "14",
+ "metadata": {},
+ "source": [
+ "## Testing functional behavior"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "def sinus(arr):\n",
+ " import numpy as np\n",
+ " return np.cos(arr) # oops! wrong solution\n",
+ "\n",
+ "code_input_sinus = CodeInput(sinus)\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " exercise_key=\"sinus_functional_behavior\",\n",
+ " code=code_input_sinus,\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "def assert_2pi_periodic() -> str:\n",
+ " out = code_input_sinus.get_function_object()([0, 2*np.pi])\n",
+ " if not np.allclose(out[0], out[1]):\n",
+ " return \"Function is not periodic.\"\n",
+ " return \"\" # empty strings means it passes\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_2pi_periodic,\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "16",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Obfuscating the reference solution with fingerprint"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets.check import (\n",
+ " assert_equal\n",
+ ")\n",
+ "\n",
+ "def riddle():\n",
+ " \"\"\"\n",
+ " Please return as string the answer to this riddle:\n",
+ " \n",
+ " What has wings but in the air it not swings.\n",
+ " I looked to the north, but it was not worth.\n",
+ " What I am looking for?\n",
+ " \"\"\"\n",
+ " return \"\"\n",
+ "code_input_sinus = CodeInput(riddle)\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " exercise_key=\"riddle\",\n",
+ " code=code_input_sinus,\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "#def assert_equal(output, reference):\n",
+ "# return \"\" if output == reference else \"Not correct solution. Hint: it is an animal in the antarctica.\"\n",
+ "\n",
+ "char_to_num = {char: num for num, char in enumerate(\"abcdefghijklmnopqrmnstuvwxyz\")}\n",
+ "def string_to_int(output):\n",
+ " return sum([char_to_num[char] for char in output])\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_equal,\n",
+ " ],\n",
+ " fingerprint = string_to_int,\n",
+ " inputs_parameters=[{}],\n",
+ " outputs_references=[(93,),],\n",
+ " suppress_fingerprint_asserts = True # By default we do not show the error message, since it is confusing with the fingerprint\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18",
+ "metadata": {
+ "raw_mimetype": "text/markdown",
+ "tags": []
+ },
+ "source": [
+ "### Solution"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Once you enter the solution you can get the reference output with\n",
+ "string_to_int(check_code_ex.compute_output_to_check())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20",
+ "metadata": {},
+ "source": [
+ "## Checking all widgets"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "21",
+ "metadata": {},
+ "source": [
+ "The check registry also provides the possibility to check all the widgets. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "22",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "check_registry"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "23",
+ "metadata": {},
+ "source": [
+ "We create a widget that will raise an error to show how this is visualized."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "def error(arr):\n",
+ " raise ValueError(\"Oops!\")\n",
+ " return arr\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " exercise_key=\"will_raise_error\",\n",
+ " code=error,\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_type,\n",
+ " ],\n",
+ " inputs_parameters=[{\"arr\": np.asarray([1., 2., 3.5])}],\n",
+ " outputs_references=[(np.asarray([1., 2., 3.5]),)]\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "25",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# For the demo to automatically run we simulate a button press using the private function that should not be used\n",
+ "check_registry._check_all_widgets_button.click()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/conf.py b/docs/src/conf.py
index 278cf74..a855ac3 100644
--- a/docs/src/conf.py
+++ b/docs/src/conf.py
@@ -1,5 +1,7 @@
# Sphinx documentation build configuration file
+import sphinx_material
+
import scwidgets
extensions = [
@@ -10,15 +12,40 @@
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
- "sphinx.ext.inheritance_diagram",
+ "sphinx.ext.mathjax",
+ "nbsphinx",
]
+
+html_theme = "sphinx_material"
+html_theme_path = sphinx_material.html_theme_path()
+html_context = sphinx_material.get_html_context()
+html_theme_options = {
+ # Set the name of the project to appear in the navigation.
+ "nav_title": "scicode-widgets",
+ # Set the repo location to get a badge with stats
+ "repo_url": "https://github.com/osscar-org/scicode-widgets/",
+ "repo_name": "scicode-widgets",
+ # Visible levels of the global TOC; -1 means unlimited
+ "globaltoc_depth": 1,
+ # If False, expand all TOC entries
+ "globaltoc_collapse": False,
+ "color_primary": "orange",
+ "color_accent": "cyan",
+}
+
+html_sidebars = {
+ "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]
+}
+
+# htmlhelp_basename = ""
+html_title = "scicode-widgets documentation"
+html_short_title = "Documentation"
+
templates_path = ["_templates"]
exclude_patterns = ["_build"]
project = "scwidget"
-copyright = "BSD 3-Clause License, Copyright (c) 2023, scicode-widgets developer team"
+copyright = "BSD 3-Clause License, Copyright (c) 2024, scicode-widgets developer team"
version = scwidgets.__version__
release = version
-
-htmlhelp_basename = "scicode-widget-doc"
diff --git a/docs/src/developers.rst b/docs/src/developers.rst
new file mode 100644
index 0000000..1404bf7
--- /dev/null
+++ b/docs/src/developers.rst
@@ -0,0 +1,95 @@
+Developers
+==========
+
+This is an instruction of the developer tools that help you contributing.
+
+Running tests
+-------------
+
+Tests can be with run using
+
+.. code-block:: bash
+
+ tox -e tests
+
+You can source the test environment of tox after a run using
+
+.. code-block:: bash
+
+ source .tox/tests/bin/activate
+
+Converting notebook test files
+##############################
+
+The we store the notebooks as converted Python file using `jupytext` for the conversion
+for better versioning
+
+.. code-block:: bash
+
+ jupytext tests/notebooks/*.py --to ipynb
+
+Be aware that when runnng the tests with tox all `*.ipynb` are overwriten by their
+corresponding `*.py` file. For example, the file `test_widget_cue_box.ipynb` is
+overwritten by the conversion of `test_widget_cue_box.py` when running the test.
+
+
+Running in browser
+##################
+
+We use selenium to test the widgets on user actions (like a button click). To run it in
+in the CI where no display is available. We run the browsers in headless model to not
+load the UI. For debugging a test however, it is often benificial to see what is
+happening in the window. To run the tests with the browser UI please use
+
+.. code-block:: bash
+
+ pytest --driver Firefox
+
+Port issues
+###########
+
+For the notebook server we fixed the port to 8815, if this port is not available you .
+It can happen that notebook process is not properly killed and remains as zombie
+process. You can check if a notebook with this port is already running using
+
+.. code-block:: bash
+
+ jupyter notebook list | grep 8815
+
+or if some process is using it
+
+.. code-block:: bash
+
+ netstat -l | grep 8815
+
+Formatting code
+---------------
+
+Your code can be formatted using
+
+.. code-block:: bash
+
+ tox -e format
+
+The formatting should fix most issues with linting, but sometimes you need to manually
+fix some issues. To run the linter use
+
+.. code-block:: bash
+
+ tox -e lint
+
+
+Building documentation
+----------------------
+
+To build the docs please use
+
+.. code-block:: bash
+
+ tox -e docs
+
+To open the doc with for example firefox you can run
+
+.. code-block:: bash
+
+ firefox docs/build/html/index.html
diff --git a/docs/src/exercises.ipynb b/docs/src/exercises.ipynb
new file mode 100644
index 0000000..02b8786
--- /dev/null
+++ b/docs/src/exercises.ipynb
@@ -0,0 +1,435 @@
+{
+ "cells": [
+ {
+ "cell_type": "raw",
+ "id": "0",
+ "metadata": {
+ "editable": true,
+ "raw_mimetype": "text/restructuredtext",
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ ".. important::\n",
+ " Jupyter widgets cannot be run within in documentation. To be able to interact with the widget you must run a mybinder instance. To run a mybinder instance of this notebook please use this link https://mybinder.org/v2/gh/osscar-org/scicode-widgets/HEAD?labpath=docs%2Fsrc%2Fexercises.ipynb."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# Writing exercises"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {},
+ "source": [
+ "The scicode-widgets mainly offers a flexible code widget that allows instant feedback to evaluate the code for interactive plots."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import (\n",
+ " CodeExercise,\n",
+ " CodeInput,\n",
+ " CueFigure,\n",
+ " CueOutput,\n",
+ " CueObject,\n",
+ " ExerciseRegistry,\n",
+ " ParametersPanel,\n",
+ " TextExercise,\n",
+ ")\n",
+ "\n",
+ "from ipywidgets import FloatSlider, IntSlider\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {},
+ "source": [
+ "## ExerciseRegistry to store and load answers to exercises"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "Due to limitations of jupyter notebooks in saving the widgets state, we store and load the widget state by ourself using the `ExerciseRegstry`. It allows you specify a filename for the json file to store all the answers to all registered exercises. The exercises are registered by passing the `ExerciseRegstry` instance as input argument."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "exercise_registry = ExerciseRegistry()\n",
+ "exercise_registry"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "## TextExercise to create text exercises"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8",
+ "metadata": {},
+ "source": [
+ "A simple textarea to save students answers. Note that the save and load buttons only appear when a exercise key and registry is given"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "TextExercise(\n",
+ " exercise_title=\"Exercise 01: Derive a solution for weights\",\n",
+ " exercise_description=\"\"\"\n",
+ " We can define ridge regression by extending the ordinary least\n",
+ " square solution by a penalization $\\lambda\\|\\mathbf{w}\\|_2^2$. Please derive the solution\n",
+ " for the weights from the optimization problem:
\n",
+ " $$\\hat{\\mathbf{w}} = \\min_\\mathbf{w} \\|\\mathbf{y}-\\mathbf{X}\\mathbf{w}\\|^2 + \\lambda\\|\\mathbf{w}\\|^2$$\"\"\",\n",
+ " key=\"ex01\", # the key it is stored under in the json file\n",
+ " exercise_registry=exercise_registry\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "10",
+ "metadata": {},
+ "source": [
+ "## Interactive coding exercises"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11",
+ "metadata": {},
+ "source": [
+ "This is an example how to create a simple exercise."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# This is what the students sees and can adapt\n",
+ "def sin(x: int, omega=1.0):\n",
+ " \"\"\"\n",
+ " Implements ridge regression\n",
+ "\n",
+ " :param x: An array of data points\n",
+ " :param omega: The frequency\n",
+ " \"\"\"\n",
+ " import numpy as np\n",
+ " return np.sin(x*omega)\n",
+ "\n",
+ "def update_func(code_ex):\n",
+ " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n",
+ " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n",
+ " ax = code_ex.figure.gca()\n",
+ " ax.plot(x, y)\n",
+ " \n",
+ "code_ex_description = \"\"\"\n",
+ "Implements a sinus function $\\sin(x\\omega)$.\n",
+ "\"\"\"\n",
+ "code_ex = CodeExercise(\n",
+ " code=sin,\n",
+ " parameters={'omega': (0.5, 3.14, 0.1)},\n",
+ " outputs=plt.figure(),\n",
+ " update=update_func,\n",
+ " update_mode=\"continuous\", # we also support [\"manual\", \"release\"]\n",
+ " title=\"Sinus function\",\n",
+ " description=code_ex_description,\n",
+ " key=\"sin_local\",\n",
+ " exercise_registry=exercise_registry\n",
+ ")\n",
+ "\n",
+ "code_ex.run_update() # For the demonstration we run the widget one time\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
+ "source": [
+ "### Creating widget components beforehand"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "14",
+ "metadata": {},
+ "source": [
+ "More complex widgets might need creation beforehand to allow full customization"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from ipywidgets import HTML\n",
+ "\n",
+ "# One can pass a function also by\n",
+ "code_input = CodeInput(\n",
+ " function_name=\"sin\",\n",
+ " function_parameters=\"x: int, omega=1.0\",\n",
+ " function_body=\"import numpy as np\\nreturn np.sin(x*omega)\",\n",
+ " docstring=\"Implements ridge regression\\n\\n:param x: An array of data points\\n:param omega: The frequency\"\n",
+ ")\n",
+ "# customization of figure toolbar, only important in widget mode (use %matplotlib widget)\n",
+ "figure = CueFigure(plt.figure(), show_toolbars=True)\n",
+ "# to use a custom output for own widgets\n",
+ "output = CueOutput()\n",
+ "# to use display custom widgets\n",
+ "table = CueObject(HTML(value=\"\"))\n",
+ "\n",
+ "\n",
+ "# to customize sliders, one can directy\n",
+ "parameter_panel = ParametersPanel(\n",
+ " omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n",
+ ")\n",
+ "# alternatively if passed to CodeExercise this also works\n",
+ "#parameter_panel = dict(\n",
+ "# omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n",
+ "#)\n",
+ "\n",
+ "\n",
+ "def update_func(code_ex):\n",
+ " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n",
+ " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n",
+ " ax = code_ex.outputs[0].figure.gca()\n",
+ " ax.plot(x, y)\n",
+ " with code_ex.outputs[1]:\n",
+ " print(\"Some text after the figure\")\n",
+ "\n",
+ " code_ex.outputs[2].object.value = \"x | y |
\" + \\\n",
+ " \"\".join([f\"{x[i]:.2f} | {y[i]:.2f} |
\" for i in range(0, len(x), 20)]) + \\\n",
+ " \"
\"\n",
+ " # the captured text in the function is always printed before any other output\n",
+ " print(\"Some text before the figure\")\n",
+ " \n",
+ " \n",
+ "\n",
+ "code_ex = CodeExercise(\n",
+ " code=code_input,\n",
+ " parameters=parameter_panel,\n",
+ " outputs=[figure, output, table],\n",
+ " update=update_func,\n",
+ ")\n",
+ "\n",
+ "code_ex.run_update() # For the demonstration we run the widget one time\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "16",
+ "metadata": {},
+ "source": [
+ "### Include imports to code input"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "17",
+ "metadata": {},
+ "source": [
+ "So far we always added the imports required for the code inside the code input. We need to do this because the widget is its creates its own environment (own globals) so no function from the notebook is accidently used. This however does not solve the problem when one wants to include typehints in the function signature that require imports. For that one can add the library to the globals."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def sin(x: np.ndarray, omega=1.0): # using np.ndarray requires import numpy before the function body\n",
+ " \"\"\"\n",
+ " Implements ridge regression\n",
+ "\n",
+ " :param x: An array of data points\n",
+ " :param omega: The frequency\n",
+ " \"\"\"\n",
+ " return np.sin(x*omega)\n",
+ "\n",
+ "def update_func(code_ex):\n",
+ " x = np.linspace(-2*np.pi, 2*np.pi, 100)\n",
+ " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n",
+ " ax = code_ex.figure.gca()\n",
+ " ax.plot(x, y)\n",
+ " \n",
+ "code_ex = CodeExercise(\n",
+ " code=CodeInput(sin, builtins={'np': np}),\n",
+ " parameters={'omega': (0.5, 3.14, 0.1)},\n",
+ " outputs=plt.figure(),\n",
+ " update=update_func,\n",
+ ")\n",
+ "\n",
+ "code_ex.run_update() # For the demonstration we run the widget one time\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19",
+ "metadata": {},
+ "source": [
+ "### Interactive coding exercises with globals variables"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "20",
+ "metadata": {},
+ "source": [
+ "This is an example how to create a simple exercise using globals in the update function. This can be more convenient in certain cases but a bit more prone to errors as when creating multiple exercises the global names can easily conflict with each other and result in unwanted behavior. Therefore we recommend use the code demo instance that is passed through the update function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# This is what the students sees and can adapt\n",
+ "def sin(x, omega):\n",
+ " \"\"\"\n",
+ " Implements ridge regression\n",
+ "\n",
+ " :param x: An array of data points\n",
+ " :param omega: The frequency\n",
+ " \"\"\"\n",
+ " import numpy as np\n",
+ " return np.sin(x*omega)\n",
+ "\n",
+ "code_input = CodeInput(sin)\n",
+ "cue_figure = CueFigure(plt.figure())\n",
+ "parameter_panel = ParametersPanel(\n",
+ " omega=FloatSlider(value=1, min=0.5, max=3.14, step=0.1, description=\"$\\\\omega$\")\n",
+ ")\n",
+ "\n",
+ "x = np.linspace(-2*np.pi, 2*np.pi, 100)\n",
+ "def update_func():\n",
+ " global x, code_input, cue_figure, parameter_panel \n",
+ " y = code_ex.code(x, parameter_panel.parameters[\"omega\"])\n",
+ " ax = cue_figure.figure.gca()\n",
+ " ax.plot(x, y)\n",
+ " \n",
+ " \n",
+ "code_ex = CodeExercise(\n",
+ " code=code_input,\n",
+ " parameters=parameter_panel,\n",
+ " outputs=cue_figure,\n",
+ " update=update_func,\n",
+ ")\n",
+ "\n",
+ "\n",
+ "code_ex.run_update() # For the demonstration we run the widget one time\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "22",
+ "metadata": {},
+ "source": [
+ "## ParametersPanel short constructors"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "23",
+ "metadata": {},
+ "source": [
+ "The `ParametersPanel` can be also used with the same shorthand constructors as [interact](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html). Here some examples"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from ipywidgets import fixed\n",
+ "ParametersPanel(\n",
+ " frequency=(0.5, 2*np.pi, 0.1),\n",
+ " amplitude=(1, 5, 1),\n",
+ " inverted=True,\n",
+ " type=[\"sin\", \"cos\"],\n",
+ " plot_title=\"trigonometric curve\",\n",
+ " const=fixed(1) # this argument will be passed but is not changeable and therefore not displayed\n",
+ ")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/getting_started.ipynb b/docs/src/getting_started.ipynb
new file mode 100644
index 0000000..7df694b
--- /dev/null
+++ b/docs/src/getting_started.ipynb
@@ -0,0 +1,271 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ "# Getting started\n",
+ "\n",
+ "scicode-widgets can be installed with:\n",
+ "\n",
+ "```bash\n",
+ " pip install scicode-widgets\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "raw",
+ "id": "1",
+ "metadata": {
+ "editable": true,
+ "raw_mimetype": "text/restructuredtext",
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ ".. important::\n",
+ " Jupyter widgets cannot be run within in documentation. To be able to interact with the widget you must run a mybinder instance. To run a mybinder instance of this notebook please use this link https://mybinder.org/v2/gh/osscar-org/scicode-widgets/HEAD?labpath=docs%2Fsrc%2Fgetting_started.ipynb."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ "## Creating a code execrcise\n",
+ "This is how a simple coding exercise can look like."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import CodeExercise\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "# This is what the students sees and can adapt\n",
+ "def sin(x, omega):\n",
+ " import numpy as np\n",
+ " # We provide already the solution for the demo\n",
+ " return np.sin(x*omega)\n",
+ "\n",
+ "\n",
+ "x = np.linspace(-2*np.pi, 2*np.pi, 100)\n",
+ "def update_func(code_ex):\n",
+ " y = code_ex.code(x, code_ex.parameters[\"omega\"])\n",
+ " ax = code_ex.figure.gca()\n",
+ " ax.plot(x, y)\n",
+ "\n",
+ "code_ex = CodeExercise(\n",
+ " code=sin,\n",
+ " outputs=plt.figure(),\n",
+ " parameters={\"omega\": (0.5, 3.14, 0.1)},\n",
+ " update=update_func,\n",
+ " update_mode=\"continuous\",\n",
+ " exercise_title=\"Sinus function\",\n",
+ " exercise_description=\"Implements $\\sin(x\\omega)$\",\n",
+ ")\n",
+ "\n",
+ "code_ex.run_update() # For the demonstration we run the widget one time\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {},
+ "source": [
+ "Please look at __[section exercises](./exercises.html)__ for more information about the execrises that can be created and their customization options."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5",
+ "metadata": {},
+ "source": [
+ "## Check students solution\n",
+ "\n",
+ "You can create checks for student that work like unit tests helping the student to verify the students solution"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {
+ "lines_to_next_cell": 2,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import (\n",
+ " CheckRegistry,\n",
+ " assert_numpy_allclose,\n",
+ " assert_shape,\n",
+ " assert_type,\n",
+ ")\n",
+ "\n",
+ "\n",
+ "check_registry = CheckRegistry()\n",
+ "\n",
+ "def sinus(arr):\n",
+ " import numpy as np\n",
+ " return np.cos(arr) # oops! wrong solution\n",
+ "\n",
+ "check_code_ex = CodeExercise(\n",
+ " code=sinus,\n",
+ " update=lambda code_ex: print(code_ex.code(np.pi)),\n",
+ " check_registry=check_registry,\n",
+ ")\n",
+ "\n",
+ "def assert_2pi_periodic() -> str:\n",
+ " out = check_code_ex.code([0, 2*np.pi])\n",
+ " if not np.allclose(out[0], out[1]):\n",
+ " return \"Function is not periodic.\"\n",
+ " return \"\" # empty strings means it passes\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_2pi_periodic,\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " check_code_ex,\n",
+ " asserts=[\n",
+ " assert_type, # checks if same type as reference values \n",
+ " assert_shape, # checks if same shape as reference values\n",
+ " assert_numpy_allclose, # checks if allclose to reference values\n",
+ " ],\n",
+ " inputs_parameters=[{\"arr\": np.asarray([0., 0.78539816, 1.57079633, 2.35619449, 3.14159265])}],\n",
+ " outputs_references=[(np.asarray([0., 7.07106781e-01, 1.00000000e+00, 7.07106781e-01, 0.]),)]\n",
+ ")\n",
+ "\n",
+ "check_code_ex.run_check()\n",
+ "check_code_ex"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
+ "source": [
+ "Please look at the __[section how to add checks](check.html)__ for all options to create checks."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
+ "source": [
+ "## nbgrader integration \n",
+ "\n",
+ "One can use nbgrader by using their macros.\n",
+ "\n",
+ "```python\n",
+ "def sin(arr: np.ndarray):\n",
+ " \"\"\"\n",
+ " :param arr: array of arbitrary shape\n",
+ " :return: returns the sinus\n",
+ " \"\"\"\n",
+ " import numpy as np\n",
+ " ### BEGIN SOLUTION\n",
+ " sin_arr = np.sin(arr)\n",
+ " ### END SOLTUION\n",
+ " return sin_arr\n",
+ "```\n",
+ "\n",
+ "Then nbgrader will convert this to"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "def sin(arr: np.ndarray):\n",
+ " \"\"\"\n",
+ " :param arr: array of arbitrary shape\n",
+ " :return: returns the sinus\n",
+ " \"\"\"\n",
+ " import numpy as np\n",
+ " # YOUR CODE HERE\n",
+ " raise NotImplementedError()\n",
+ " return sin_arr\n",
+ "\n",
+ "code_ex = CodeExercise(\n",
+ " code=sin\n",
+ ")\n",
+ "\n",
+ "display(code_ex)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "10",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "It requires to add a hook in the config to copy over the students answers to the grading subfolder. A step-by-step tutorial how to make an nbgrader project compatible with scicode-widget can be seen in the repo TODO."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "tags": []
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/index.rst b/docs/src/index.rst
index fb4a5cb..9eb294f 100644
--- a/docs/src/index.rst
+++ b/docs/src/index.rst
@@ -5,4 +5,11 @@ scicode-widgets
:start-after: marker-package-description
.. toctree::
- contributing
+ :hidden:
+
+ getting_started
+ exercises
+ check
+ nbgrader
+ api
+ developers
diff --git a/docs/src/nbgrader.ipynb b/docs/src/nbgrader.ipynb
new file mode 100644
index 0000000..cac1196
--- /dev/null
+++ b/docs/src/nbgrader.ipynb
@@ -0,0 +1,600 @@
+{
+ "cells": [
+ {
+ "cell_type": "raw",
+ "id": "0",
+ "metadata": {
+ "editable": true,
+ "raw_mimetype": "text/restructuredtext",
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "source": [
+ ".. important::\n",
+ " Jupyter widgets cannot be run within in documentation. To be able to interact with the widget you must run a mybinder instance. To run a mybinder instance of this notebook please use this link https://mybinder.org/v2/gh/osscar-org/scicode-widgets/HEAD?labpath=docs%2Fsrc%2Fexercises.ipynb."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {},
+ "source": [
+ "# Integrating with nbgrader"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-20d1e686f2f5e6eb",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "source": [
+ "## Problem1 from the quickstart of nbgrader using scwidgets"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-d44feca970fbc35c",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "source": [
+ "When you initialize the quickstart project from nbgrader a `problem1.ipynb` file is generated. In this notebook we show how scwidgets can be used with nbgrader. We transformed the `problem1.ipynb` to a version that uses scwidgets."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-cbffb5535580d50a",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-e7b0d7238de01566",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import CodeExercise, TextExercise, ExerciseRegistry, CheckRegistry, CodeInput, assert_type, assert_equal"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "cell-bfcc1fc18abb908d",
+ "locked": true,
+ "points": 0,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "if 'NAME' not in globals():\n",
+ " NAME = \"reference solution\"\n",
+ "### END HIDDEN TESTS\n",
+ "exercise_registry = ExerciseRegistry(filename_prefix=\"problem1\")\n",
+ "exercise_registry"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-1f434402bd4a0796",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "check_registry = CheckRegistry()\n",
+ "check_registry"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "squares",
+ "locked": false,
+ "schema_version": 3,
+ "solution": true,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "def squares(n):\n",
+ " \"\"\"Compute the squares of numbers from 1 to n, such that the \n",
+ " ith element of the returned list equals i^2.\n",
+ " \n",
+ " \"\"\"\n",
+ " ### BEGIN SOLUTION\n",
+ " if n < 1:\n",
+ " raise ValueError(\"n must be greater than or equal to 1\")\n",
+ " return [i ** 2 for i in range(1, n + 1)]\n",
+ " ### END SOLUTION\n",
+ "\n",
+ "exercise_description = \"\"\"\n",
+ "Write a function that returns a list of numbers,\n",
+ "such that $x_i=i^2$, for $1\\leq i \\leq n$.\n",
+ "Make sure it handles the case where $n<1$ by raising a `ValueError`.\n",
+ "\"\"\"\n",
+ "\n",
+ "code_ex_squares = CodeExercise(\n",
+ " code=squares,\n",
+ " parameters={\"n\": (1, 10, 1)},\n",
+ " update=lambda code_ex: print(code_ex.code(code_ex.parameters['n'])),\n",
+ " check_registry=check_registry,\n",
+ " exercise_registry=exercise_registry,\n",
+ " key=\"Part A (2 points)\",\n",
+ " description=exercise_description\n",
+ ")\n",
+ "\n",
+ "# Check that squares returns the correct output for several inputs\n",
+ "check_registry.add_check(\n",
+ " code_ex_squares,\n",
+ " asserts=[assert_type, assert_equal],\n",
+ " inputs_parameters=[{\"n\": i} for i in [1, 2, 10, 11]],\n",
+ " outputs_references=[([1],), ([1, 4],), ([1, 4, 9, 16, 25, 36, 49, 64, 81, 100],), ([1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121],)],\n",
+ ")\n",
+ "\n",
+ "# Check that squares raises an error for invalid inputs\n",
+ "def assert_raise_error() -> str:\n",
+ " try:\n",
+ " code_ex_squares.code.unwrapped_function(0)\n",
+ " except ValueError:\n",
+ " return \"\"\n",
+ " else:\n",
+ " return \"Did not raise error for zero\"\n",
+ " \n",
+ " try:\n",
+ " code_ex_squares.code.unwrapped_function(-4)\n",
+ " except ValueError:\n",
+ " return \"\"\n",
+ " else:\n",
+ " return \"Did not error for negative number\"\n",
+ "\n",
+ "check_registry.add_check(\n",
+ " code_ex_squares,\n",
+ " asserts=[\n",
+ " assert_raise_error,\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "code_ex_squares"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "correct_squares",
+ "locked": true,
+ "points": 1,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "exercise_registry.load_answer_from_student_name(NAME, code_ex_squares)\n",
+ "checks = check_registry.check_widget(code_ex_squares)\n",
+ "assert checks[0].successful, checks[0].message()\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "squares_invalid_input",
+ "locked": true,
+ "points": 1,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "assert checks[1].successful, checks[1].message()\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "11",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "sum_of_squares",
+ "locked": false,
+ "schema_version": 3,
+ "solution": true,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from scwidgets import assert_type, assert_equal\n",
+ "\n",
+ "def sum_of_squares(n):\n",
+ " \"\"\"Compute the sum of the squares of numbers from 1 to n.\"\"\"\n",
+ " ### BEGIN SOLUTION\n",
+ " return sum(squares(n))\n",
+ " ### END SOLUTION\n",
+ "\n",
+ "exercise_description = \"\"\"\n",
+ "Using your `squares` function, write a function\n",
+ "that computes the sum of the squares of the numbers\n",
+ "from 1 to $n$. Your function should call the `squares`\n",
+ "function -- it should NOT reimplement its functionality.\n",
+ "\"\"\"\n",
+ "\n",
+ "code_ex_sum_of_squares = CodeExercise(\n",
+ " code=CodeInput(sum_of_squares, builtins={\"squares\": code_ex_squares.code.function}),\n",
+ " parameters={\"n\": (1, 10, 1)},\n",
+ " update=lambda code_ex: print(code_ex.code(code_ex.parameters['n'])),\n",
+ " check_registry=check_registry,\n",
+ " exercise_registry=exercise_registry,\n",
+ " key=\"Part B (1 point)\",\n",
+ " description=exercise_description\n",
+ ")\n",
+ "\n",
+ "# Check that sum_of_squares returns the correct answer for various inputs\n",
+ "check_registry.add_check(\n",
+ " code_ex_sum_of_squares,\n",
+ " asserts=[assert_type, assert_equal],\n",
+ " inputs_parameters=[{\"n\": i} for i in [1, 2, 10, 11]],\n",
+ " outputs_references=[(1,), (5,), (385,), (506,)],\n",
+ ")\n",
+ "\n",
+ "# Check that sum_of_squares relies on squares\n",
+ "def assert_uses_squares() -> str:\n",
+ " \"\"\"Check that sum_of_squares relies on squares.\"\"\"\n",
+ " code_ex_sum_of_squares.code.builtins = {}\n",
+ " try:\n",
+ " code_ex_sum_of_squares.code.unwrapped_function(1) # not using builtins\n",
+ " except NameError:\n",
+ " result = \"\"\n",
+ " else:\n",
+ " result = \"sum_of_squares does not use squares\"\n",
+ " code_ex_sum_of_squares.code.builtins = {\"squares\": code_ex_squares.code.function}\n",
+ " return result\n",
+ " \n",
+ "check_registry.add_check(\n",
+ " code_ex_sum_of_squares,\n",
+ " asserts=[\n",
+ " assert_uses_squares,\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "code_ex_sum_of_squares"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "correct_sum_of_squares",
+ "locked": true,
+ "points": 0.5,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "exercise_registry.load_answer_from_student_name(NAME, code_ex_sum_of_squares)\n",
+ "checks = check_registry.check_widget(code_ex_sum_of_squares)\n",
+ "assert checks[0].successful, checks[0].message()\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "13",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "sum_of_squares_uses_squares",
+ "locked": true,
+ "points": 0.5,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "assert checks[1].successful, checks[1].message()\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "sum_of_squares_equation",
+ "locked": false,
+ "schema_version": 3,
+ "solution": true,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "value = \"\"\"\n",
+ "### BEGIN SOLUTION\n",
+ "$\\sum_{i=1}^n i^2$\n",
+ "### END SOLUTION\n",
+ "\"\"\"\n",
+ "\n",
+ "exercise_description = \"\"\"\n",
+ "Using LaTeX math notation, write out the equation\n",
+ "that is implemented by your `sum_of_squares` function.\"\"\"\n",
+ "\n",
+ "text_ex = TextExercise(\n",
+ " value=value,\n",
+ " exercise_registry=exercise_registry,\n",
+ " key=\"Part C (1 point)\",\n",
+ " description=exercise_description\n",
+ ")\n",
+ "text_ex"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "cell-f21df018970765c7",
+ "locked": true,
+ "points": 1,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "exercise_registry.load_answer_from_student_name(NAME, text_ex)\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "16",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "sum_of_squares_application",
+ "locked": false,
+ "schema_version": 3,
+ "solution": true,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "def pyramidal_number(n):\n",
+ " \"\"\"Returns the n^th pyramidal number\"\"\"\n",
+ " summation = sum_of_squares(n)\n",
+ " ### BEGIN SOLUTION\n",
+ " ### END SOLUTION\n",
+ "\n",
+ "exercise_description = \"\"\"\n",
+ "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below.\n",
+ "\"\"\"\n",
+ "\n",
+ "code_ex_pyramidal_number = CodeExercise(\n",
+ " code=CodeInput(pyramidal_number, builtins={\"sum_of_squares\": code_ex_sum_of_squares.code.function}),\n",
+ " parameters={\"n\": (1, 10, 1)},\n",
+ " update=lambda code_ex: print(code_ex.code(code_ex.parameters['n'])),\n",
+ " exercise_registry=exercise_registry,\n",
+ " key=\"Part D (2 points)\",\n",
+ " description=exercise_description\n",
+ ")\n",
+ "\n",
+ "code_ex_pyramidal_number"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "cell-bf4d174b9af19a27",
+ "locked": true,
+ "points": 4,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "exercise_registry.load_answer_from_student_name(NAME, code_ex_pyramidal_number)\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "18",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "arithmetic_geometric_sum_equation",
+ "locked": false,
+ "schema_version": 3,
+ "solution": true,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "exercise_description = \"\"\"\n",
+ "State the formulae for an arithmetic and geometric\n",
+ "sum and verify them numerically for an example of\n",
+ "your choice.\"\"\"\n",
+ "\n",
+ "text_ex = TextExercise(\n",
+ " value=\"\"\"\n",
+ "### BEGIN SOLUTION\n",
+ "### END SOLUTION\n",
+ "\"\"\",\n",
+ " exercise_registry=exercise_registry,\n",
+ " key=\"Part E (4 points)\",\n",
+ " description=exercise_description\n",
+ ")\n",
+ "\n",
+ "text_ex"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19",
+ "metadata": {
+ "nbgrader": {
+ "grade": true,
+ "grade_id": "cell-ef75037ccb87de3a",
+ "locked": true,
+ "points": 2,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "### BEGIN HIDDEN TESTS\n",
+ "exercise_registry.load_answer_from_student_name(NAME, text_ex)\n",
+ "### END HIDDEN TESTS"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "20",
+ "metadata": {
+ "nbgrader": {
+ "grade": false,
+ "grade_id": "cell-3b0ef02c9f5f10f0",
+ "locked": true,
+ "schema_version": 3,
+ "solution": false,
+ "task": false
+ }
+ },
+ "outputs": [],
+ "source": [
+ "try:\n",
+ " exercise_registry.load_file_from_student_name(NAME)\n",
+ "except FileNotFoundError:\n",
+ " exercise_registry.create_new_file_from_student_name(NAME)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/problem1-reference_solution.json b/docs/src/problem1-reference_solution.json
new file mode 100644
index 0000000..6ba761d
--- /dev/null
+++ b/docs/src/problem1-reference_solution.json
@@ -0,0 +1 @@
+{"Part A (2 points)": {"code": "### BEGIN SOLUTION\nif n < 1:\n raise ValueError(\"n must be greater than or equal to 1\")\nreturn [i ** 2 for i in range(1, n + 1)]\n### END SOLUTION", "parameters_panel": {"n": 5}}, "Part B (1 point)": {"code": "### BEGIN SOLUTION\nreturn sum(squares(n))\n### END SOLUTION", "parameters_panel": {"n": 5}}, "Part C (1 point)": {"textarea": "\n### BEGIN SOLUTION\n$\\sum_{i=1}^n i^2$\n### END SOLUTION\n"}, "Part D (2 points)": {"code": "summation = sum_of_squares(n)\n### BEGIN SOLUTION\n### END SOLUTION", "parameters_panel": {"n": 5}}, "Part E (4 points)": {"textarea": "\n### BEGIN SOLUTION\n### END SOLUTION\n"}}
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
index 1ffb510..656a3f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,6 +3,12 @@ lint_folders =
"{toxinidir}/src" \
"{toxinidir}/tests" \
"{toxinidir}/docs/src/"
+
+notebooks =
+ {toxinidir}/docs/src/check.ipynb \
+ {toxinidir}/docs/src/exercises.ipynb \
+ {toxinidir}/docs/src/getting_started.ipynb \
+ {toxinidir}/docs/src/nbgrader.ipynb
envlist =
lint
@@ -101,7 +107,9 @@ exclude_also =
[testenv:docs]
deps =
-r docs/requirements.txt
-commands = sphinx-build {posargs:-E} -b html docs/src docs/build/html
+commands =
+ ipython kernel install --sys-prefix --name "python"
+ sphinx-build {posargs:-E} -b html docs/src docs/build/html
[testenv:format]
# formats project source code files
@@ -110,10 +118,13 @@ deps =
black
blackdoc
isort
+ nbstripout
commands =
+ nbstripout {[tox]notebooks}
black {[tox]lint_folders}
blackdoc {[tox]lint_folders}
isort {[tox]lint_folders}
+ nbstripout {[tox]|docs_folder}docs/src/*ipynb
[testenv:lint]
# this environement lints the Python code with flake8 (code linter),
@@ -131,8 +142,9 @@ deps =
blackdoc
isort
mypy
-
+ nbstripout
commands =
+ nbstripout --verify {[tox]notebooks}
flake8 {[tox]lint_folders}
black --check --diff {[tox]lint_folders}
blackdoc --check --diff {[tox]lint_folders}