diff --git a/notebooks/workflow_3_cascaded_mzi.ipynb b/notebooks/workflow_3_cascaded_mzi.ipynb index 9f7954b5..25236457 100644 --- a/notebooks/workflow_3_cascaded_mzi.ipynb +++ b/notebooks/workflow_3_cascaded_mzi.ipynb @@ -11,7 +11,7 @@ "\n", "Each filter stage is formed by 4 cascaded Mach-Zenhder Interferometers (MZIs) with predefined delays for the central wavelength. Symmetrical Direction Couplers (DCs) are used to mix the signals at the ends of the MZI arms. In order to facilitate fabrication, all DC gaps are kept equal, so the power transfer ratios are defined by the coupling length of the DCs.\n", "\n", - "We will design each DC through 3D FDTD simulations to guarantee the desired power ratios, which have been calculated to provide maximally flat response. The S parameters computed through FDTD are latter used in the full circuit simulation along with models for staight and curved waveguide sections, leading to an accurate model that exhibits features similar to those found in experimental data." + "We will design each DC through 3D FDTD simulations to guarantee the desired power ratios, which have been calculated to provide maximally flat response. The S parameters computed through FDTD are later used in the full circuit simulation along with models for staight and curved waveguide sections, leading to an accurate model that exhibits features similar to those found in experimental data." ] }, { @@ -311,7 +311,7 @@ "\n", "We calculate the group index for our waveguides through `tidy3d`'s local mode solver. Because we're interested in precise dispersion, we use a dense mesh and high precision in these calculations.\n", "\n", - "The path length differences for the MZIs are $\\Delta L$, $2\\Delta L$, $L_\\pi - 2\\Delta L$, and $-2\\Delta L$, with $L_\\pi$ the length required for $\\pi$ phase shift (negative values indicate a delay in the opposite arm to positive values).\n" + "The path length differences for the MZIs are -$\\Delta L$, -$2\\Delta L$, $2\\Delta L + L_\\pi$, and $2\\Delta L$, with $L_\\pi$ the length required for $\\pi$ phase shift (negative values indicate a delay in the opposite arm to positive values).\n" ] }, { @@ -352,10 +352,10 @@ "length_delta = mzi_path_difference(waveguide_solver, ng, fsr)\n", "length_pi = lda_c / (2 * ne)\n", "mzi_deltas = (\n", - " length_delta,\n", - " 2 * length_delta,\n", - " length_pi - 2 * length_delta,\n", + " -length_delta,\n", " -2 * length_delta,\n", + " 2 * length_delta + length_pi,\n", + " 2 * length_delta,\n", ")\n", "print(f\"Path difference (ΔL = {length_delta}, Lπ = {length_pi}):\", mzi_deltas)" ] @@ -380,7 +380,7 @@ "layout = gf.c.mzi_lattice(\n", " coupler_gaps=(gap,) * len(lengths),\n", " coupler_lengths=tuple(lengths),\n", - " delta_lengths=tuple([abs(x) for x in mzi_deltas]),\n", + " delta_lengths=mzi_deltas,\n", " cross_section=\"strip\",\n", ")\n", "layout.plot()" @@ -613,29 +613,65 @@ "source": [ "import inspect\n", "\n", + "def _unique_component_name(base_component: str, models: dict) -> str:\n", + " \"\"\"Return a component name like f'{base_component}_v{i}' that is not yet in models.\"\"\"\n", + " i = 0\n", + " new_component = f\"{base_component}_v{i}\"\n", + " while new_component in models:\n", + " i += 1\n", + " new_component = f\"{base_component}_v{i}\"\n", + " return new_component\n", + "\n", + "def _patch_netlist_block(netlist_block: dict, models: dict, models_to_patch: dict) -> None:\n", + " \"\"\"\n", + " Patch a single netlist block in-place.\n", + " Expects netlist_block to have an 'instances' dict.\n", + " \"\"\"\n", + " instances = netlist_block.get(\"instances\", {})\n", + " for instance_name, instance in instances.items():\n", + " component = instance.get(\"component\")\n", + " if component not in models_to_patch:\n", + " continue\n", + "\n", + " new_component = _unique_component_name(component, models)\n", + "\n", + " settings = instance.get(\"settings\", {})\n", + " settings_filtered = {\n", + " k: v\n", + " for k, v in settings.items()\n", + " if k in inspect.signature(models_to_patch[component]).parameters\n", + " }\n", + "\n", + " # Create a specialized model for this instance's settings\n", + " models[new_component] = models_to_patch[component](**settings_filtered)\n", + "\n", + " # Update the instance to reference the specialized model\n", + " instance.pop(\"settings\", None)\n", + " instance[\"component\"] = new_component\n", + "\n", + "def patch_netlist(netlist: dict, models: dict, models_to_patch: dict) -> tuple[dict, dict]:\n", + " \"\"\"\n", + " Patches either:\n", + " - a flat netlist: {'instances': {...}, 'connections': ..., 'ports': ...}\n", + " - or a recursive netlist: {'': {'instances': {...}, ...}, ...}\n", + "\n", + " Mutates netlist + models in-place and returns them for convenience.\n", + " \"\"\"\n", + "\n", + " # Case 1: flat netlist block\n", + " if isinstance(netlist, dict) and \"instances\" in netlist:\n", + " _patch_netlist_block(netlist, models, models_to_patch)\n", + " return netlist, models\n", + "\n", + " # Case 2: recursive netlist: top-level dict of blocks\n", + " if isinstance(netlist, dict):\n", + " for netlist_name, netlist_block in netlist.items():\n", + " if not isinstance(netlist_block, dict):\n", + " continue\n", + " if \"instances\" in netlist_block:\n", + " _patch_netlist_block(netlist_block, models, models_to_patch)\n", + " return netlist, models\n", "\n", - "def patch_netlist(netlist, models, models_to_patch):\n", - " instances = netlist[\"instances\"]\n", - " for name in instances:\n", - " model = instances[name]\n", - " if model[\"component\"] in models_to_patch:\n", - " component = model[\"component\"]\n", - " i = 0\n", - " new_component = f\"{component}_v{i}\"\n", - " while new_component in models:\n", - " i += 1\n", - " new_component = f\"{component}_v{i}\"\n", - " settings = model[\"settings\"]\n", - " settings_fitered = {\n", - " k: v\n", - " for k, v in settings.items()\n", - " if k in inspect.signature(models_to_patch[component]).parameters\n", - " }\n", - " models[new_component] = models_to_patch[model[\"component\"]](\n", - " **settings_fitered\n", - " )\n", - " del model[\"settings\"]\n", - " model[\"component\"] = new_component\n", " return netlist, models\n", "\n", "\n", @@ -720,29 +756,22 @@ "metadata": {}, "outputs": [], "source": [ - "# fig, ax = plt.subplots(1, 1, figsize=(12, 4))\n", - "# netlist, models = patch_netlist(\n", - "# netlist=layout.get_netlist(recursive=True),\n", - "# models={\"straight\": straight_model, \"bend_euler\": bend_model(cross_section=cross_section)},\n", - "# models_to_patch={\"coupler\": coupler_model},\n", - "# )\n", - "# circuit, _ = sax.circuit(netlist, models)\n", - "# lda = np.linspace(1.5, 1.6, 1001)\n", - "# s = circuit(wl=lda)\n", - "# ax.plot(lda, 20 * jnp.log10(jnp.abs(s[(\"o1\", \"o3\")])), label=\"Cross\")\n", - "# ax.plot(lda, 20 * jnp.log10(jnp.abs(s[(\"o1\", \"o4\")])), label=\"Thru\")\n", - "# ax.set_ylim(-30, 0)\n", - "# ax.set_xlabel(\"λ (µm)\")\n", - "# ax.legend()" + "fig, ax = plt.subplots(1, 1, figsize=(12, 4))\n", + "netlist, models = patch_netlist(\n", + " netlist=layout.get_netlist(recursive=True),\n", + " models={\"straight\": straight_model, \"bend_euler\": bend_model(cross_section=cross_section)},\n", + " models_to_patch={\"coupler\": coupler_model},\n", + ")\n", + "circuit, _ = sax.circuit(netlist, models)\n", + "lda = np.linspace(1.5, 1.6, 1001)\n", + "s = circuit(wl=lda)\n", + "ax.plot(lda, 20 * jnp.log10(jnp.abs(s[(\"o1\", \"o3\")])), label=\"Cross\")\n", + "ax.plot(lda, 20 * jnp.log10(jnp.abs(s[(\"o1\", \"o4\")])), label=\"Thru\")\n", + "ax.axvline(lda_c, linestyle=\"dotted\")\n", + "ax.set_ylim(-30, 0)\n", + "ax.set_xlabel(\"λ (µm)\")\n", + "ax.legend()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "36", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -751,7 +780,7 @@ "custom_cell_magics": "kql" }, "kernelspec": { - "display_name": "base", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -765,7 +794,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.8" } }, "nbformat": 4,