{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "0", "metadata": {}, "outputs": [], "source": [ "% --- jupyter:\n", "% jupytext:\n", "% text_representation:\n", "% extension: .m\n", "% format_name: percent\n", "% format_version: '1.3'\n", "% jupytext_version: 1.19.1\n", "% kernelspec:\n", "% display_name: MATLAB Kernel\n", "% language: matlab\n", "% name: jupyter_matlab_kernel\n", "% ---" ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "\n", "# Call qpdk from MATLAB\n", "\n", "This notebook demonstrates calling `qpdk` (and through it, `gdsfactory`) **directly from MATLAB**\n", "using MATLAB's built-in Python interface (`py.module.function(...)`); see [Ways to Call Python\n", "from MATLAB](https://se.mathworks.com/help/matlab/matlab_external/ways-to-call-python-from-matlab.\n", "html).\n", "\n", "The notebook itself is written for the **MATLAB Jupyter kernel** provided by\n", "[jupyter-matlab-proxy](https://github.com/mathworks/jupyter-matlab-proxy); see also the [MathWorks\n", "Jupyter reference\n", "architecture](https://se.mathworks.com/products/reference-architectures/jupyter.html).\n", "\n", "## Prerequisites\n", "\n", "- A Python environment with `qpdk` installed including the `models` extra:\n", " ```bash\n", " uv pip install \"qpdk[models]\"\n", " ```\n", "- MATLAB R2024a or newer (older releases may also work with `pyenv`). - `jupyter-matlab-proxy`\n", "installed alongside Jupyter:\n", " ```bash\n", " uv pip install jupyter jupyter-matlab-proxy\n", " ```\n", "- The environment variable `QPDK_PYTHON` set to the absolute path of the\n", " Python interpreter that has `qpdk` installed. With `uv`:\n", " ```bash\n", " export QPDK_PYTHON=$(uv run python -c 'import sys; print(sys.executable)')\n", " ```" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "\n", "## Configure MATLAB's Python interpreter and activate the PDK\n", "\n", "MATLAB's `pyenv` selects the Python interpreter used by `py.*` calls." ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "qpdk_python = getenv('QPDK_PYTHON');\n", "if isempty(qpdk_python)\n", " error('matlab_integration:noPython', ...\n", " 'Set the QPDK_PYTHON environment variable to a Python interpreter that has qpdk installed.');\n", "end\n", "\n", "pe = pyenv('Version', qpdk_python, 'ExecutionMode', 'OutOfProcess');\n", "disp(pe);\n", "\n", "% MATLAB parses ``py.qpdk.PDK`` as a function reference (PDK is a module variable, not a callable),\n", "% so we fetch the attribute explicitly via py.getattr — see the *Limitations to Indexing into Python\n", "% Objects* section of the MATLAB documentation.\n", "qpdk_mod = py.importlib.import_module('qpdk');\n", "PDK = py.getattr(qpdk_mod, 'PDK');\n", "PDK.activate();\n", "fprintf('Activated PDK: %s\\n', string(py.getattr(PDK, 'name')));" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "\n", "## Hello-world: build a coupled resonator and write a GDS\n", "\n", "Instantiate `qpdk.cells.resonator_coupled` with default parameters, query its size and ports from\n", "MATLAB, then write the layout to disk." ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "results_dir = fullfile(tempdir, 'qpdk_matlab_demo');\n", "if ~exist(results_dir, 'dir'); mkdir(results_dir); end\n", "\n", "component = py.qpdk.cells.resonator_coupled();\n", "gds_path = fullfile(results_dir, 'resonator_coupled.gds');\n", "component.write_gds(gds_path);\n", "\n", "size_info = component.size_info;\n", "width_um = double(size_info.width);\n", "height_um = double(size_info.height);\n", "\n", "fprintf('Wrote %s\\n', gds_path);\n", "fprintf(' size: %.1f x %.1f um\\n', width_um, height_um);" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "\n", "## Frequency sweep using `qpdk.models.resonator.resonator_frequency`\n", "\n", "MATLAB drives a `linspace` of resonator lengths, calls the analytical Python model for each value,\n", "and plots the resulting fundamental frequency. This is the basic pattern of *MATLAB driving the\n", "parameter sweep, Python providing the physics*." ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ "lengths_um = linspace(2000, 10000, 81);\n", "freqs_hz = zeros(size(lengths_um));\n", "for k = 1:numel(lengths_um)\n", " freqs_hz(k) = double(py.qpdk.models.resonator.resonator_frequency( ...\n", " pyargs('length', lengths_um(k), 'is_quarter_wave', true)));\n", "end\n", "\n", "figure;\n", "plot(lengths_um, freqs_hz / 1e9, 'LineWidth', 1.5); grid on;\n", "xlabel('Resonator length (\\mum)');\n", "ylabel('Resonance frequency (GHz)');\n", "title('Quarter-wave CPW resonator: f_0(L)');\n", "hold on;\n", "for f = [4 6 8]\n", " yline(f, '--', sprintf('%d GHz', f));\n", "end\n", "hold off;" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "\n", "## Inverse design with MATLAB's `fzero`\n", "\n", "Use MATLAB's built-in root finder to invert the analytical model: for each target frequency, find\n", "the resonator length that hits it, then build the corresponding `resonator_coupled` cell and write\n", "the GDS. Combining MATLAB's optimisation built-ins with `qpdk`'s physics models is the most direct\n", "value-add of this integration." ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "target_ghz = [5, 6, 7];\n", "solved_lengths = zeros(size(target_ghz));\n", "for k = 1:numel(target_ghz)\n", " f_target_hz = target_ghz(k) * 1e9;\n", " objective = @(L) double(py.qpdk.models.resonator.resonator_frequency( ...\n", " pyargs('length', L, 'is_quarter_wave', true))) - f_target_hz;\n", " solved_lengths(k) = fzero(objective, [500, 50000]);\n", "\n", " fprintf('Target %d GHz -> length %.2f um\\n', target_ghz(k), solved_lengths(k));\n", "\n", " component_k = py.qpdk.cells.resonator_coupled( ...\n", " pyargs('length', solved_lengths(k)));\n", " out_path = fullfile(results_dir, sprintf('resonator_%dGHz.gds', target_ghz(k)));\n", " component_k.write_gds(out_path);\n", "end" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "\n", "## Parametric chip variants\n", "\n", "Build a 2-D `ndgrid` of (coupling gap, resonator length) and call\n", "`qpdk.samples.resonator_test_chip.resonator_test_chip_python` for each combination. Collect\n", "bounding-box areas in a MATLAB `table` for inspection." ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "gaps_um = [12, 16, 20];\n", "res_lengths_um = [3500, 4500];\n", "[GG, LL] = ndgrid(gaps_um, res_lengths_um);\n", "n = numel(GG);\n", "areas_mm2 = zeros(n, 1);\n", "files = strings(n, 1);\n", "\n", "for k = 1:n\n", " chip = py.qpdk.samples.resonator_test_chip.resonator_test_chip_python( ...\n", " pyargs('coupling_gap', GG(k), 'resonator_length', LL(k)));\n", " sz = chip.size_info;\n", " w_um = double(sz.width);\n", " h_um = double(sz.height);\n", " areas_mm2(k) = (w_um * h_um) / 1e6;\n", " files(k) = fullfile(results_dir, sprintf('chip_gap%g_len%g.gds', GG(k), LL(k)));\n", " chip.write_gds(files(k));\n", "end\n", "\n", "T = table(GG(:), LL(:), areas_mm2, files, ...\n", " 'VariableNames', {'coupling_gap_um', 'resonator_length_um', 'area_mm2', 'gds_file'});\n", "disp(T);" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "\n", "## What's next\n", "\n", "- Open the generated GDS files in [KLayout](https://www.klayout.de/) to view\n", " the layouts.\n", "- Browse the qpdk [model catalog](all_models.ipynb) for additional\n", " analytical models that compose nicely with MATLAB-driven sweeps." ] } ], "metadata": { "kernelspec": { "display_name": "MATLAB Kernel", "language": "matlab", "name": "jupyter_matlab_kernel" } }, "nbformat": 4, "nbformat_minor": 5 }