{ "cells": [ { "cell_type": "markdown", "id": "822f7e47-18d8-4d22-a48d-9e352a80bb21", "metadata": {}, "source": [ "# Tutorial 5) Dynamic, Flattened Module Access with Ekans" ] }, { "cell_type": "markdown", "id": "6ff070ea-f9f6-4843-9b7e-d59a102d4eac", "metadata": {}, "source": [ "## 0. Introduction" ] }, { "cell_type": "markdown", "id": "0473f6c2-ae75-41f5-9e4c-e1badd67534a", "metadata": {}, "source": [ "Tuning quantum chips is inherently iterative: you run measurements, analyse the results, tweak parameters, and repeat. In practice, this also means frequently modifying your measurement scripts, configurations, and helper functions.\n", "\n", "However, Python’s import system is static. Once a module is imported, the running interpreter does not automatically pick up changes you make on disk. This forces you to either restart your session or manually reload modulesβ€”both of which interrupt workflow and can become error-prone in complex projects.\n", "\n", "This is exactly the problem that Ekans solves." ] }, { "cell_type": "markdown", "id": "4c34d70a-fb40-4622-864a-10e453de5adf", "metadata": {}, "source": [ "## 1. What is Ekans?" ] }, { "cell_type": "markdown", "id": "a7bcd841-bfb3-472c-bf3a-f5e081071204", "metadata": {}, "source": [ "Ekans is a lightweight proxy layer that sits on top of your package and provides:\n", "\n", "**A live view of your codebase**\n", "\n", "> It mirrors your folder and module structure, so your package is accessible exactly as it exists on disk.\n", "\n", "**Explicit and predictable API access**\n", "\n", "> Only objects explicitly exposed via `__init__.py` (through `__all__`) are attached to the namespace. There is no implicit or hidden importing.\n", "\n", "**Full module access**\n", "> All leaf modules can be accessed individually in case objects that that are not exposed to the publkic API (`__all__`) are required.\n", "\n", "**One-line dynamic reloading**\n", "\n", "> Changes to any part of your codebase can be picked up instantly with a single reload call with no kernel restarts required." ] }, { "cell_type": "markdown", "id": "c55b3914-7dfc-4cbf-8f36-bb1c4e671bc1", "metadata": {}, "source": [ "**With Ekans, you can:**\n", "\n", "- πŸ” Iterate faster β€” modify code and immediately test it without restarting your environment\n", "\n", "- 🧠 Reduce mental overhead β€” no need to track what needs re-importing\n", "- 🧩 Work with large codebases comfortably β€” even deeply nested module structures stay manageable\n", "- ⚑ Stay focused on experiments β€” less time fighting Python imports, more time analysing results\n", "\n", "This notebook demonstrates how to use Ekans to dynamically load, reload, and interact with a structured Python package in a seamless way." ] }, { "cell_type": "markdown", "id": "501705be-d8e1-46d7-8570-6812612d1c19", "metadata": {}, "source": [ "## 2. Hands on example" ] }, { "cell_type": "markdown", "id": "6bee63f1-52ca-4e4a-a50b-10ff7ac0b84d", "metadata": {}, "source": [ "We will use the arbok_driver.examples package. This might look intimidating now but will be a handy reference once you start playing around yourself." ] }, { "cell_type": "markdown", "id": "c65344d1-3c54-4f38-8bc7-f1d73cfc5de3", "metadata": {}, "source": [ "Let's start how one would normally set up a driver and a generic measurement." ] }, { "cell_type": "code", "execution_count": 1, "id": "541ee872-1191-4c7a-9ee1-8d2b38b3ec3a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2026-03-28 11:05:15,086 - qm - INFO - Starting session: 69b72c99-5465-4145-b0c6-44db25a0b2d8\n" ] } ], "source": [ "from arbok_driver import ArbokDriver, Device, Ekans, Measurement" ] }, { "cell_type": "code", "execution_count": 2, "id": "a41e6074-3e70-4212-ba1d-21a757da173e", "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.sequences import SquarePulseScalable\n", "from arbok_driver.examples.configurations.hardware import (\n", " opx1000_config,\n", " divider_config,\n", ")\n", "from arbok_driver.examples.configurations.sequence import (\n", " square_pulse_scalable_conf\n", ")" ] }, { "cell_type": "code", "execution_count": 3, "id": "2512926b-9850-4fec-9e28-d4f9038206a0", "metadata": {}, "outputs": [], "source": [ "mock_device = Device(\n", " name = 'mock_device',\n", " opx_config = opx1000_config,\n", " divider_config = divider_config)\n", "\n", "qm_driver = ArbokDriver('qm_driver', mock_device)" ] }, { "cell_type": "code", "execution_count": 4, "id": "fdb4bdeb-977b-448e-81e5-72a42756a00d", "metadata": {}, "outputs": [], "source": [ "mock_measurement = Measurement(qm_driver, 'mock_measurement')" ] }, { "cell_type": "code", "execution_count": 5, "id": "45e340bb-1409-4d1f-acd5-b26bff454d8b", "metadata": {}, "outputs": [], "source": [ "square_pulse_scaled = SquarePulseScalable(\n", " mock_measurement,\n", " 'square_pulse_scaled',\n", " square_pulse_scalable_conf\n", " )" ] }, { "cell_type": "markdown", "id": "33e192a0-09ba-48ce-a684-b3cc9e4a1c51", "metadata": {}, "source": [ "Here we used direct imports from our modules but we can make that dynamic using `Ekans`. Insead of importing the files directly as shown above, you can create an `Ekans` instance for a given (sub)module." ] }, { "cell_type": "code", "execution_count": 6, "id": "e3915de2-1064-4b19-8dc9-afc78d242604", "metadata": {}, "outputs": [], "source": [ "from arbok_driver import examples\n", "ek_sequences = Ekans(examples.sequences)\n", "ek_configs = Ekans(examples.configurations)" ] }, { "cell_type": "markdown", "id": "7958b5f8-834f-4fb0-af6d-7c32062f0ec0", "metadata": {}, "source": [ "Ekans mirrors your folder structure and exposes modules directly.\n", "\n", "Objects are **only available if they are explicitly exported** in a package’s `__init__.py` using `__all__`. This keeps the namespace clean and predictable.\n", "\n", "Leaf modules themselves are always accessible as modules." ] }, { "cell_type": "markdown", "id": "66b36d19-0438-4364-9233-f8b04ff34185", "metadata": {}, "source": [ "Creating an `Ekans` instance will:\n", "\n", "- Discover all submodules\n", "- Import or reload them\n", "- Build a namespace mirroring your folder structure\n", "- Attach only explicitly exported objects (via `__all__`)" ] }, { "cell_type": "code", "execution_count": 7, "id": "63d9199b-35a8-415d-b128-45d8fccf3607", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'P1': {'division': 6},\n", " 'P2': {'division': 6},\n", " 'P3': {'division': 6},\n", " 'gate_2': {'division': 2},\n", " 'readout_element': {'division': 2}}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ek_configs.hardware.divider_config" ] }, { "cell_type": "code", "execution_count": 8, "id": "342c8163-0bc5-401a-b135-06f96edecc60", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "arbok_driver.examples.sequences.coulomb_peaks.CoulombPeaks" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ek_sequences.CoulombPeaks" ] }, { "cell_type": "markdown", "id": "acbd8b1b-5342-4bb0-a84f-07c39c0eda3d", "metadata": {}, "source": [ "### Explicit API via `__init__.py`\n", "\n", "Ekans follows standard Python practices:\n", "\n", "- Each package defines its public API via `__all__`\n", "- Only those names are exposed at the package level\n", "- All other objects remain accessible through their module\n", "\n", "Example:\n", "\n", "```python\n", "# sub_sequences/__init__.py\n", "from .square_pulse import SquarePulse\n", "\n", "__all__ = [\"SquarePulse\"]\n", "```" ] }, { "cell_type": "markdown", "id": "34746bbe-22aa-4205-b5ab-91f5830d7d61", "metadata": {}, "source": [ "This allows:" ] }, { "cell_type": "code", "execution_count": 9, "id": "cb774234-a798-40f6-9504-b58ae71ae51c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "arbok_driver.examples.sequences.square_pulse.SquarePulse" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ek_sequences.SquarePulse # βœ… exported (in __all__)\n", "ek_sequences.square_pulse # βœ… module\n", "ek_sequences.square_pulse.SquarePulse # βœ… always works" ] }, { "cell_type": "markdown", "id": "0edd61fa-ddf6-4d61-9e19-ed40e4519e6f", "metadata": {}, "source": [ "But prevents accidental exposure of internal objects." ] }, { "cell_type": "markdown", "id": "c8971a22-dca8-47f5-83ce-a37901c908b8", "metadata": {}, "source": [ "Have a look at the given folder structure here and play around!" ] }, { "cell_type": "markdown", "id": "f3712a3e-370f-49e6-9ebc-5d9b081f794f", "metadata": {}, "source": [ "```\n", "arbok_driver\n", "β”œβ”€β”€ ...\n", "β”œβ”€β”€ examples\n", "β”‚Β Β  β”œβ”€β”€ configurations\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ hardware\n", "β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ divider_config.py\n", "β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ opx1000_config.py\n", "β”‚Β Β  β”‚Β Β  └── sequence\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ coulomb_peaks_config.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ device_config.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ parity_init_conf.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ parity_readout_conf.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ square_pulse_confs.py\n", "β”‚Β Β  β”‚Β Β  └── stability_8q_conf.py\n", "β”‚Β Β  β”œβ”€β”€ experiments\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”‚Β Β  └── square_pulse_experiments.py\n", "β”‚Β Β  β”œβ”€β”€ readout_classes\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ dc_average.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ dc_chopped_readout.py\n", "β”‚Β Β  β”‚Β Β  β”œβ”€β”€ difference.py\n", "β”‚Β Β  β”‚Β Β  └── threshold.py\n", "β”‚Β Β  └── sub_sequences\n", "β”‚Β Β  β”œβ”€β”€ __init__.py\n", "β”‚Β Β  β”œβ”€β”€ coulomb_peaks.py\n", "β”‚Β Β  β”œβ”€β”€ parity_initialization.py\n", "β”‚Β Β  β”œβ”€β”€ parity_readout.py\n", "β”‚Β Β  β”œβ”€β”€ square_pulse.py\n", "β”‚Β Β  β”œβ”€β”€ square_pulse_scalable.py\n", "β”‚Β Β  └── stability_map.py\n", "β”œβ”€β”€ ...\n", "```" ] }, { "cell_type": "markdown", "id": "6013ed2a-34be-4b7d-b866-697c2050716b", "metadata": {}, "source": [ "Each submodule contains Python files defining classes/functions." ] }, { "cell_type": "markdown", "id": "0d33a0de-6976-4193-a7c2-474eb5cd80ab", "metadata": {}, "source": [ "## 3. Live module re-loading " ] }, { "cell_type": "markdown", "id": "3f7522c5-b0ab-420f-929b-301cbd042b34", "metadata": {}, "source": [ "Assume one has made changes to files on disc. Use `reload_modules` to update the changes from disk and make them available to you." ] }, { "cell_type": "code", "execution_count": 10, "id": "7cbba0f1-c291-44a4-b764-92088aa65a18", "metadata": {}, "outputs": [], "source": [ "ek_configs.reload_modules()" ] }, { "cell_type": "code", "execution_count": 11, "id": "f75d3015-d045-414f-a1c3-f611cf0746c4", "metadata": {}, "outputs": [], "source": [ "ek_sequences.reload_modules()" ] }, { "cell_type": "markdown", "id": "8e98660a-12a4-4c99-862a-1c4245c70e96", "metadata": {}, "source": [ "This will:\n", "\n", "- reload all modules\n", "- rebuild the namespace\n", "- reflect code changes without restarting Python" ] }, { "cell_type": "markdown", "id": "fd52ff6d-1b4b-45ae-9b2d-fa8c64979193", "metadata": {}, "source": [ "Now the previous measurement can be re-constructed and run." ] }, { "cell_type": "code", "execution_count": 12, "id": "85d032f4-1c35-4da8-87fb-489ad0ec0dc6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Deleting measurement: mock_measurement\n" ] } ], "source": [ "qm_driver.reset_measurements()\n", "\n", "mock_measurement = Measurement(qm_driver, 'mock_measurement')\n", "\n", "refreshed_sequence = ek_sequences.SquarePulseScalable(\n", " mock_measurement,\n", " 'square_pulse_scaled',\n", " ek_configs.sequence.square_pulse_scalable_conf\n", ")" ] }, { "cell_type": "markdown", "id": "2b8074d9-5aee-41b8-85c9-69b89a2c2323", "metadata": {}, "source": [ "## 4. Rules and limitations" ] }, { "cell_type": "markdown", "id": "523186aa-af9d-4d74-8556-72a0be022e17", "metadata": {}, "source": [ "To ensure `Ekans` works reliably, your package should follow these conventions:\n", "\n", "### 1. Every folder must be a Python package\n", "Each directory **must contain an `__init__.py` file**.\n", "\n", "- This ensures Python treats the directory as a proper package \n", "- Without it, Python may create a *namespace package*, which can break module discovery and reloading in `Ekans`\n", "\n", "---\n", "\n", "### 2. Only `.py` files are treated as modules\n", "`Ekans` only considers Python source files:\n", "\n", "- βœ… `.py` files are loaded and exposed as modules \n", "- 🚫 Other file types (e.g. `.txt`, `.json`, `.yaml`) are ignored \n", "\n", "These non-Python files do **not** interfere with `Ekans`, but they will not be accessible through it.\n", "\n", "---\n", "\n", "### 3. Modules must be importable\n", "All Python modules should be valid at import time:\n", "\n", "- They must not raise exceptions during import (unless explicitly intended) \n", "- All internal and external imports must resolve correctly \n", "\n", "If a module fails to import, it may prevent parts of your package from being available through `Ekans`.\n", "\n", "---\n", "\n", "Following these rules ensures that `Ekans` can correctly discover, structure, and dynamically reload your codebase.\n", "\n", "---\n", "\n", "### 4. Public API must be defined via `__all__`\n", "\n", "Ekans only exposes objects that are explicitly listed in a module’s `__all__`.\n", "\n", "- βœ… Ensures a clean and predictable namespace \n", "- βœ… Prevents accidental exposure of internal objects \n", "\n", "If `__all__` is not defined, no objects will be attached at the package level." ] }, { "cell_type": "markdown", "id": "e6be1a38-db97-48d4-8d69-d5e1cd6ec565", "metadata": {}, "source": [ "## 5. Summary" ] }, { "cell_type": "markdown", "id": "60092819-139a-40a3-9eb7-3e9a0894cba4", "metadata": {}, "source": [ "Traditional Python imports are static, meaning that changes to your codebase require manual reloading or restarting your session. This becomes cumbersome when working with complex, modular projects that evolve rapidly during development and experimentation.\n", "\n", "`Ekans` addresses this by providing a dynamic interface to your package:\n", "\n", "- It mirrors your folder structure as a nested, intuitive namespace \n", "- It exposes a **clean and explicit API** defined via `__init__.py` and `__all__` \n", "- It enables **one-line reloading**, so changes on disk are immediately reflected in your running session \n", "\n", "By following a few simple rulesβ€”ensuring all directories are proper packages, keeping modules importable, and explicitly defining your public APIβ€”you can fully leverage `Ekans` to create a more predictable and efficient development workflow.\n", "\n", "### Key takeaway\n", "\n", "> `Ekans` lets you treat your codebase as a **live, structured, and explicitly defined object**, rather than a static snapshot.\n", "\n", "This reduces surprises, improves maintainability, and helps you stay focused on designing experiments and analysing results." ] } ], "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.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }