Created
April 9, 2024 08:52
-
-
Save hackfin/6b53ceec5f6213cc36abe7c9b9f9172c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "id": "4ca3db65-15ca-4bf6-b75c-571311419137", | |
| "metadata": {}, | |
| "source": [ | |
| "# Design methods: Contexts, modules, functions\n", | |
| "\n", | |
| "For verification purposes, a reusable construct that is proven valid is most worthy in complex projects. Therefore a classic HDL developer would write a configureable module and integrate that into his hierarchy.\n", | |
| "\n", | |
| "VHDL, for instance, supports function calls, but does not distinguish between synthesizeable and simulation constructs. Here, the story is different: Function calls can just execute and return a result, or they can yield hardware elements (the generator way) that are synthesizeable." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 1, | |
| "id": "fc4bc83a-f313-4b49-ba11-c8c5db140349", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "import sys\n", | |
| "sys.path.insert(0, \"../..\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 2, | |
| "id": "d9ab874e-8c4d-4994-a84f-b39dd74f8a42", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "from cyhdl import *" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "120c2380-10a4-435f-92b4-d2d38bfc64ff", | |
| "metadata": {}, | |
| "source": [ | |
| "We can create a hardware generator function in cyHDL notation. Its differences from a `@block`:\n", | |
| " * It will not infer into a module, thus, has no interface\n", | |
| " * It can not contain a process (`@always..`)\n", | |
| "\n", | |
| "Unlike a `@hdlmacro`, it can contain conditional (if..else) statements. It is therefore context sensitive which is represented by the `rtl` parameter. It is mandatory that the first argument is spelled `rtl`.\n", | |
| "\n", | |
| "Due to reasons with translation, it is currently necessary to wrap such a decorated function inside a class. This is not a bad idea anyhow, to create some structure:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 3, | |
| "id": "96234941-cf0b-40a9-945a-bb869aefc3e1", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "class primitives:\n", | |
| " @cyrite_function\n", | |
| " def muxer(rtl, EN, A, B, Q):\n", | |
| " if EN:\n", | |
| " Q.next = A\n", | |
| " else:\n", | |
| " Q.next = B" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "62289e55-99fe-46ab-82d5-53e34003ed3b", | |
| "metadata": {}, | |
| "source": [ | |
| "We could also consider using a `@hdlmacro`, however there's a catch:\n", | |
| "\n", | |
| "* IRL notation requirement: We must return structures using yield using IRL elements\n", | |
| "* We would have to determine the `If` architecture context beforehand.\n", | |
| "\n", | |
| "The `@cyrite_function` does the selection for us at elaboration or evaluation time. Its internal logic generator inherits the architecture from its calling process.\n", | |
| "\n", | |
| "Note:\n", | |
| "\n", | |
| "* A `@cyrite_function` is an **unbound** function that is internally passed a `rtl` context.\n", | |
| "Therefore, the `primitive` class is not instanced, but merely a container structure.\n", | |
| "* It **prohibits** access to global definitions. Everything you use inside this function\n", | |
| " must be either taken from the `rtl` context (normally, the calling process) or passed as argument.\n", | |
| "* Do not forget to use `rtl` as first argument. You can not use 'self' or another notation.\n", | |
| "* It is a dynamic dual use function that is possibly transpiled to IRL, depending on the context" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "f6bf772b-ed91-420a-a955-c8125b62bdda", | |
| "metadata": {}, | |
| "source": [ | |
| "A test unit making use of this construct may look as follows:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 4, | |
| "id": "c48c7f73-6361-43b2-b305-48d659105b29", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "@block\n", | |
| "def unit1(clk : ClkSignal, a : Signal, b : Signal, en : Signal.Type(bool),\n", | |
| " q : Signal.Output, MODE : int):\n", | |
| " \n", | |
| " # This is a simple expression that can be assigned to a signal:\n", | |
| " expr_and = a & b\n", | |
| " expr_or = a | b\n", | |
| " # This results in a logic generator, where the result\n", | |
| " # is a parameter:\n", | |
| " \n", | |
| " logic = []\n", | |
| " \n", | |
| " if MODE == 1:\n", | |
| " logic += [ q @assign@ expr_and ]\n", | |
| " elif MODE == 2:\n", | |
| " logic = [ q @assign@ expr_or ]\n", | |
| " else:\n", | |
| " @always(clk.posedge)\n", | |
| " def ff():\n", | |
| " primitives.muxer(en, a, b, q)\n", | |
| " \n", | |
| " logic += [ ff ]\n", | |
| "\n", | |
| " return logic" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "5f6df3f7-d488-4dd8-b15e-945bda0d223d", | |
| "metadata": {}, | |
| "source": [ | |
| "We note a few things:\n", | |
| " * The `@cyrite_function` funclet does not allow us to specify a process element\n", | |
| " * The hardware generator function is called **from within** an `@always` process." | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "8f4a6bb6-b80f-4146-9ec0-25d99d65db98", | |
| "metadata": {}, | |
| "source": [ | |
| "Let's create a dummy target context for this module to evaluate the `@hdlmacro` in.\n", | |
| "Then we instance a few signals:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 5, | |
| "id": "60c0bb0e-c356-47fc-afa6-6bf5c7271681", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "from myirl.targets.dummy import DummyTargetModule\n", | |
| "context = DummyTargetModule()\n", | |
| "\n", | |
| "a, b, q = [ Signal(intbv()[8:], name = n) for n in \"abc\" ]\n", | |
| "en = Signal(bool(), name = 'en')" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "342e9de0-8a13-4cc6-b302-2f8593abf6d6", | |
| "metadata": {}, | |
| "source": [ | |
| "We create a local `muxer` instance, initialize the signals and evaluate each of the actions manually. Note two things:\n", | |
| "* The muxer is created in the first line, but no decision on the context was made yet\n", | |
| "* During `.evaluate()`, the function is actually called with the required context." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 6, | |
| "id": "6ce86a60-c03f-464c-a6e6-648ceee1b673", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "m = primitives.muxer(en, a, b, q)\n", | |
| "m.evaluate(context)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "6aa49a8a-ecb4-44c5-bc68-63fd5ca09f96", | |
| "metadata": {}, | |
| "source": [ | |
| "After evaluation, we can actually inspect the true Python code behind the function.\n", | |
| "This is only valid, if we call it from a hardware generation context." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 7, | |
| "id": "fd02328b-1f08-456f-8e19-219b82548145", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "\n", | |
| "\n", | |
| "class __wrapper_muxer__():\n", | |
| "\n", | |
| " @cyrite_function\n", | |
| " def muxer(rtl, EN, A, B, Q):\n", | |
| " (yield [rtl.If(EN).Then(Q.set(A)).Else(Q.set(B))])\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "print(m.unparse())" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "4c77675d-ba18-4a0e-aa37-97dcc7b513c1", | |
| "metadata": {}, | |
| "source": [ | |
| "We cycle through the possible `en` values (only digital '0' and '1' for Bool signals) and check the result:" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 8, | |
| "id": "af33e3b0-cde3-4ad9-991b-ff9b9d052097", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "01\n", | |
| "02\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "for val, res in (True, a), (False, b):\n", | |
| " init = [ en.set(val), a.set(1), b.set(2) ]\n", | |
| " [ i.evaluate() for i in init ]\n", | |
| " m.evaluate(context)\n", | |
| " assert q.evaluate() == res.evaluate()\n", | |
| " print(q.evaluate())" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "57d1b752-f455-4d65-b6e5-69d6c5a99ce4", | |
| "metadata": {}, | |
| "source": [ | |
| "Using this technique, we evaluate if the muxer function is doing the right thing for our signal type. The same should then happen in the hardware and corresponding simulation." | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "46413499-65e0-47c6-9f68-66075ce640a9", | |
| "metadata": {}, | |
| "source": [ | |
| "## Creating a test bench\n", | |
| "\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 9, | |
| "id": "259d0330-b16f-4e40-abe0-ac4534cc59e1", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "from myirl.test.icarus import ICARUS\n", | |
| "from cyrite.simulation import sim\n", | |
| "\n", | |
| "@sim.testbench(ICARUS, 'ns')\n", | |
| "@block\n", | |
| "def tb_unit1():\n", | |
| " a, b, q = [ Signal(intbv()[8:]) for _ in range(3) ]\n", | |
| " en = Signal(bool())\n", | |
| " clk = ClkSignal(name = 'clk')\n", | |
| " \n", | |
| " uut = unit1(clk = clk, en = en, a = a, b = b, q = q, MODE = 3)\n", | |
| " \n", | |
| " @always(delay(1))\n", | |
| " def clkgen():\n", | |
| " clk.next = ~clk\n", | |
| " \n", | |
| " @sequence\n", | |
| " def main():\n", | |
| " \"\"\"Yes, we call the main stimulation routine 'main' to\n", | |
| "denote the sequence creating a wave trace\"\"\"\n", | |
| " en.next = False\n", | |
| " yield delay(1)\n", | |
| " a.next = 12\n", | |
| " b.next = 4\n", | |
| " en.next = True\n", | |
| " for i in range(4):\n", | |
| " yield delay(1)\n", | |
| " # assert q == 4\n", | |
| " yield clk.posedge\n", | |
| " en.next = ~en\n", | |
| " # assert q == 12\n", | |
| " \n", | |
| " yield delay(20)\n", | |
| " \n", | |
| " raise StopSimulation\n", | |
| " \n", | |
| " return instances()\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "0d4f2c20-1b17-43d1-9841-0988c3f62704", | |
| "metadata": {}, | |
| "source": [ | |
| "## Running the test bench" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 10, | |
| "id": "b9db1017-27d8-47d9-bd00-7bc284c51ba7", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| " Writing 'unit1' to file /tmp/unit1.v \n", | |
| " Writing 'tb_unit1' to file /tmp/tb_unit1.v \n", | |
| " Note: Changing library path prefix to /tmp/ \n", | |
| " Creating library file /tmp/module_defs.v \n", | |
| "DEBUG FILES ['/tmp/unit1.v', '/tmp/tb_unit1.v']\n", | |
| "==== COSIM stdout ====\n", | |
| "VCD info: dumpfile tb_unit1.vcd opened for output.\n", | |
| "Stop Simulation\n", | |
| "\n" | |
| ] | |
| }, | |
| { | |
| "data": { | |
| "text/plain": [ | |
| "0" | |
| ] | |
| }, | |
| "execution_count": 10, | |
| "metadata": {}, | |
| "output_type": "execute_result" | |
| } | |
| ], | |
| "source": [ | |
| "tb = tb_unit1()\n", | |
| "tb.run(80)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "51065f65-ac86-445d-afbf-705113d75324", | |
| "metadata": {}, | |
| "source": [ | |
| "### Displaying the trace" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 11, | |
| "id": "9a2c1bac-ca80-42a4-8f4d-6c57afcd68af", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "data": { | |
| "text/html": [ | |
| "<div><script type=\"WaveDrom\">{\"signal\": [{\"name\": \"tb_unit1.q[7:0]\", \"wave\": \"x=...=.=.=.=..................\", \"data\": \"0c 04 0c 04 0c \"}, {\"name\": \"tb_unit1.a[7:0]\", \"wave\": \"x=............................\", \"data\": \"0c \"}, {\"name\": \"tb_unit1.b[7:0]\", \"wave\": \"x=............................\", \"data\": \"04 \"}, {\"name\": \"tb_unit1.clk\", \"wave\": \"010101010101010101010101010101\", \"data\": \"010101010101010101010101010101\"}, {\"name\": \"tb_unit1.en\", \"wave\": \"01.0.1.0.1....................\", \"data\": \"010101\"}, {\"name\": \"tb_unit1.unit1_0.a[7:0]\", \"wave\": \"x=............................\", \"data\": \"0c \"}, {\"name\": \"tb_unit1.unit1_0.b[7:0]\", \"wave\": \"x=............................\", \"data\": \"04 \"}, {\"name\": \"tb_unit1.unit1_0.q[7:0]\", \"wave\": \"x=...=.=.=.=..................\", \"data\": \"0c 04 0c 04 0c \"}, {\"name\": \"tb_unit1.MAIN.i[31:0]\", \"wave\": \"x=.=.=.=.=....................\", \"data\": \"00 01 02 03 04 \"}]}</script></div>" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "output_type": "display_data" | |
| }, | |
| { | |
| "data": { | |
| "application/javascript": [ | |
| "new Promise(function(resolve, reject) {\n", | |
| "\tvar script = document.createElement(\"script\");\n", | |
| "\tscript.onload = resolve;\n", | |
| "\tscript.onerror = reject;\n", | |
| "\tscript.src = \"https://wavedrom.com/wavedrom.min.js\";\n", | |
| "\tdocument.head.appendChild(script);\n", | |
| "}).then(() => {\n", | |
| "new Promise(function(resolve, reject) {\n", | |
| "\tvar script = document.createElement(\"script\");\n", | |
| "\tscript.onload = resolve;\n", | |
| "\tscript.onerror = reject;\n", | |
| "\tscript.src = \"https://wavedrom.com/skins/narrow.js\";\n", | |
| "\tdocument.head.appendChild(script);\n", | |
| "}).then(() => {\n", | |
| "WaveDrom.ProcessAll();\n", | |
| "});\n", | |
| "});" | |
| ] | |
| }, | |
| "metadata": {}, | |
| "output_type": "display_data" | |
| } | |
| ], | |
| "source": [ | |
| "from cyrite import waveutils\n", | |
| "waveutils.draw_wavetrace(tb, 'tb_unit1.vcd', 'clk')" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "057888c6-8adc-44b8-9198-9c502cd39fca", | |
| "metadata": {}, | |
| "source": [ | |
| "### RTL specific signals\n", | |
| "\n", | |
| "A `@cyrite_function` may create internal auxiliary signals that are also depending on the target architecture." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 12, | |
| "id": "fdd40c0b-40bd-4880-b299-2d1b79f94604", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "class primi_x(primitives):\n", | |
| " @cyrite_function\n", | |
| " def muxer2(rtl, EN, A, B, Q, wire):\n", | |
| " q = rtl.Signal(rtl.intbv()[8:])\n", | |
| " \n", | |
| " if EN:\n", | |
| " q.next = A\n", | |
| " else:\n", | |
| " q.next = B\n", | |
| " \n", | |
| " Q.next = q\n", | |
| " " | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "acba85ac-4280-4f90-8639-aab58fb0d3fc", | |
| "metadata": {}, | |
| "source": [ | |
| "Unlike a sequential process that would create a variable, a `@cyrite_function` allows to create an internal signal, as if it was outside the process.\n", | |
| "\n", | |
| "However, there are dragons:\n", | |
| "* When called from a clock synchronous process, `Q` will receive the value of `q` with a delay of one clock period, so this is not a continuous assignment. This is a feature, not a bug!" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 13, | |
| "id": "2b45950a-6b31-44fb-bc05-bb20ae1769c3", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "@block\n", | |
| "def tb(muxer):\n", | |
| " a, b, q = [ Signal(intbv()[8:]) for _ in range(3) ]\n", | |
| " en = Signal(bool())\n", | |
| " clk = ClkSignal(name = 'clk')\n", | |
| " \n", | |
| " @always(clk.posedge)\n", | |
| " def ff():\n", | |
| " muxer(en, a, b, q, intbv)\n", | |
| " \n", | |
| " ff.intbv = intbv\n", | |
| " return instances()" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 14, | |
| "id": "4c456dd7-4b90-4130-bc37-72b3816c4253", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stderr", | |
| "output_type": "stream", | |
| "text": [ | |
| "../../myirl/kernel/components.py:118: UserWarning: Fallback: Pass through other argument for arg muxer (<class 'function'>)\n", | |
| " base.warnings.warn(msg)\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "t = tb(primi_x.muxer2)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 15, | |
| "id": "162a704c-8d0a-4d6c-98a8-b10133485fd4", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "\n", | |
| "\n", | |
| "class __wrapper_muxer2__():\n", | |
| "\n", | |
| " @cyrite_function\n", | |
| " def muxer2(rtl, EN, A, B, Q, wire):\n", | |
| " q = rtl.Signal(rtl.intbv()[8:])\n", | |
| " (yield [rtl.If(EN).Then(q.set(A)).Else(q.set(B)), Q.set(q)])\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "a, b, q = [ Signal(intbv()[8:]) for _ in range(3) ]\n", | |
| "en = Signal(bool())\n", | |
| "clk = ClkSignal(name = 'clk')\n", | |
| "\n", | |
| "context.Signal = Signal\n", | |
| "\n", | |
| "# Examine translated IRL:\n", | |
| "m = primi_x.muxer2(en, a, b, q, intbv)\n", | |
| "m.this = context\n", | |
| "m._run(context)\n", | |
| "print(m.unparse())" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 16, | |
| "id": "71021796-2ada-4090-9f43-1020ecb0c884", | |
| "metadata": {}, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| " Writing 'tb' to file /tmp/myirl_tb_lx_tfzkt/tb.vhdl \n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "f = t.elab(targets.VHDL)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": 17, | |
| "id": "3e85d0fd-a396-4526-a66b-63c13abb68ef", | |
| "metadata": { | |
| "tags": [] | |
| }, | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "-- File generated from source:\n", | |
| "-- /tmp/ipykernel_22424/235411190.py\n", | |
| "-- (c) 2016-2022 section5.ch\n", | |
| "-- Modifications may be lost, edit the source file instead.\n", | |
| "\n", | |
| "library IEEE;\n", | |
| "use IEEE.std_logic_1164.all;\n", | |
| "use IEEE.numeric_std.all;\n", | |
| "\n", | |
| "library work;\n", | |
| "\n", | |
| "use work.txt_util.all;\n", | |
| "use work.myirl_conversion.all;\n", | |
| "\n", | |
| "entity tb is\n", | |
| "end entity tb;\n", | |
| "\n", | |
| "architecture cyriteHDL of tb is\n", | |
| " -- Local type declarations\n", | |
| " -- Signal declarations\n", | |
| " signal s_19c7 : unsigned(7 downto 0);\n", | |
| " signal q : unsigned(7 downto 0);\n", | |
| " signal b : unsigned(7 downto 0);\n", | |
| " signal a : unsigned(7 downto 0);\n", | |
| " signal en : std_ulogic;\n", | |
| "begin\n", | |
| " \n", | |
| "ff:\n", | |
| " process(clk)\n", | |
| " begin\n", | |
| " if rising_edge(clk) then\n", | |
| " if (en = '1') then\n", | |
| " s_19c7 <= a;\n", | |
| " else\n", | |
| " s_19c7 <= b;\n", | |
| " end if;\n", | |
| " q <= s_19c7;\n", | |
| " end if;\n", | |
| " end process;\n", | |
| "end architecture cyriteHDL;\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "source": [ | |
| "!cat {f[0]}" | |
| ] | |
| } | |
| ], | |
| "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.9.2" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 5 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment