From 39c30882a5e4190970f890bad1696c18e8324b95 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 11:49:05 +0100 Subject: [PATCH 01/10] feat: callable API for MEEP models, energy_decay runner, viz refactors - Add __call__(**kwargs) to Geometry, ModeSource, Domain, FDTD for fluent updates: sim.source(port="o1", wavelength=1.55) - Add energy_decay stopping mode to StoppingConfig and cloud runner using mp.stop_when_energy_decayed() with _make_time_cap() callable - Extract shared helpers: stack/_layer_utils.py (layer classification), viz/_mesh_helpers.py (Delaunay triangulation, batch prism geometry) - Remove Three.js backend (render3d_threejs.py + viewer.html) - Add view buttons (Iso/Top/Front/Right) to Open3D+Plotly 3D viewer - Trim gsim.common public exports to essentials - Update notebooks to callable style, add meep_ring_coupler notebook - Update meep-overview.md with all architectural changes --- nbs/meep_ring_coupler.ipynb | 148 +++++++ nbs/meep_ybranch.ipynb | 43 +- src/gsim/common/__init__.py | 35 +- src/gsim/common/stack/_layer_utils.py | 115 ++++++ src/gsim/common/stack/extractor.py | 89 +--- src/gsim/common/stack/visualization.py | 55 +-- src/gsim/common/viz/__init__.py | 3 - src/gsim/common/viz/_mesh_helpers.py | 98 +++++ src/gsim/common/viz/render3d.py | 2 - src/gsim/common/viz/render3d_open3d.py | 127 +++--- src/gsim/common/viz/render3d_pyvista.py | 93 +---- src/gsim/common/viz/render3d_threejs.py | 233 ----------- src/gsim/common/viz/templates/viewer.html | 482 ---------------------- src/gsim/meep/__init__.py | 11 +- src/gsim/meep/meep-overview.md | 62 ++- src/gsim/meep/models/api.py | 123 +++++- src/gsim/meep/models/config.py | 56 ++- src/gsim/meep/script.py | 28 +- tests/meep/test_meep_models.py | 13 + tests/meep/test_simulation.py | 102 ++++- 20 files changed, 793 insertions(+), 1125 deletions(-) create mode 100644 nbs/meep_ring_coupler.ipynb create mode 100644 src/gsim/common/stack/_layer_utils.py delete mode 100644 src/gsim/common/viz/render3d_threejs.py delete mode 100644 src/gsim/common/viz/templates/viewer.html diff --git a/nbs/meep_ring_coupler.ipynb b/nbs/meep_ring_coupler.ipynb new file mode 100644 index 00000000..8ee7e83e --- /dev/null +++ b/nbs/meep_ring_coupler.ipynb @@ -0,0 +1,148 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Running MEEP Simulations\n", + "\n", + "[MEEP](https://meep.readthedocs.io/) is an open-source FDTD electromagnetic simulator. This notebook demonstrates using the `gsim.meep` API to run an S-parameter simulation on a photonic Y-branch.\n", + "\n", + "**Requirements:**\n", + "\n", + "- UBC PDK: `uv pip install ubcpdk`\n", + "- [GDSFactory+](https://gdsfactory.com) account for cloud simulation" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "### Load a pcell from UBC PDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from ubcpdk import PDK, cells\n", + "\n", + "PDK.activate()\n", + "\n", + "c = cells.coupler_ring()\n", + "# ebeam_DC_te895\n", + "# ebeam_MMI_2x2_5050_te1310\n", + "# ebeam_YBranch_te1310\n", + "# ebeam_adiabatic_te1550\n", + "# ebeam_crossing4\n", + "# ebeam_y_1310\n", + "# ebeam_y_adiabatic\n", + "\n", + "c" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### Configure and run simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", + "\n", + "print(sim.validate_config())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "sim.plot_2d(slices=\"xyz\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### Run simulation on cloud" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Run on GDSFactory+ cloud\n", + "result = sim.run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "result.plot(db=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "result.show_animation()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gsim", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbs/meep_ybranch.ipynb b/nbs/meep_ybranch.ipynb index 480f15d0..49ce4731 100644 --- a/nbs/meep_ybranch.ipynb +++ b/nbs/meep_ybranch.ipynb @@ -57,35 +57,12 @@ "\n", "sim = meep.Simulation()\n", "\n", - "sim.geometry.component = c\n", - "sim.geometry.z_crop = \"auto\"\n", - "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "\n", - "sim.source.port = \"o1\"\n", - "sim.source.wavelength = 1.55\n", - "sim.source.bandwidth = 0.01\n", - "sim.source.num_freqs = 11\n", - "\n", + "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "\n", - "sim.domain.pml = 1.0\n", - "sim.domain.margin = 0.5\n", - "sim.domain.margin_z_above = 0.5\n", - "sim.domain.margin_z_below = 0.5\n", - "sim.domain.port_margin = 0.5\n", - "\n", - "sim.solver.resolution = 20\n", - "sim.solver.stopping = \"dft_decay\"\n", - "sim.solver.max_time = 200\n", - "sim.solver.stopping_threshold = 1e-3\n", - "sim.solver.stopping_min_time = 100\n", - "sim.solver.subpixel = False\n", - "sim.solver.simplify_tol = 0.01\n", - "sim.solver.save_geometry = True\n", - "sim.solver.save_fields = True\n", - "sim.solver.save_animation = True\n", - "sim.solver.verbose_interval = 5.0\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", "\n", "print(sim.validate_config())" ] @@ -142,12 +119,20 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "gsim", "language": "python", "name": "python3" }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/src/gsim/common/__init__.py b/src/gsim/common/__init__.py index e1f52134..fd9c3804 100644 --- a/src/gsim/common/__init__.py +++ b/src/gsim/common/__init__.py @@ -12,50 +12,17 @@ from gsim.common.geometry import Geometry from gsim.common.geometry_model import GeometryModel, Prism, extract_geometry_model -from gsim.common.stack import ( - MATERIALS_DB, - Layer, - LayerStack, - MaterialProperties, - StackLayer, - ValidationResult, - extract_from_pdk, - extract_layer_stack, - get_material_properties, - get_stack, - load_stack_yaml, - material_is_conductor, - material_is_dielectric, - parse_layer_stack, - plot_stack, - print_stack, - print_stack_table, -) +from gsim.common.stack import LayerStack, ValidationResult # Alias for backward compatibility Stack = LayerStack __all__ = [ - "MATERIALS_DB", "Geometry", "GeometryModel", - "Layer", "LayerStack", - "MaterialProperties", "Prism", "Stack", - "StackLayer", "ValidationResult", - "extract_from_pdk", "extract_geometry_model", - "extract_layer_stack", - "get_material_properties", - "get_stack", - "load_stack_yaml", - "material_is_conductor", - "material_is_dielectric", - "parse_layer_stack", - "plot_stack", - "print_stack", - "print_stack_table", ] diff --git a/src/gsim/common/stack/_layer_utils.py b/src/gsim/common/stack/_layer_utils.py new file mode 100644 index 00000000..229a90ce --- /dev/null +++ b/src/gsim/common/stack/_layer_utils.py @@ -0,0 +1,115 @@ +"""Shared layer classification and GDS-layer extraction helpers. + +Used by both ``extractor`` and ``visualization`` modules. +""" + +from __future__ import annotations + +import logging +from typing import Any, Literal + +from gdsfactory.technology import LayerLevel + +logger = logging.getLogger(__name__) + + +def get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int] | None: + """Extract GDS layer tuple (layer, datatype) from a gdsfactory LayerLevel. + + Returns ``None`` when the layer cannot be parsed (callers decide fallback). + """ + layer: Any = layer_level.layer + + if isinstance(layer, tuple): + return (int(layer[0]), int(layer[1])) + + if isinstance(layer, int): + return (int(layer), 0) + + if hasattr(layer, "layer"): + inner = layer.layer + if hasattr(inner, "layer") and hasattr(inner, "datatype"): + return (int(inner.layer), int(inner.datatype)) + if isinstance(inner, int): + datatype = getattr(layer, "datatype", 0) + return (int(inner), int(datatype) if datatype else 0) + if hasattr(inner, "layer"): + innermost = inner.layer + if isinstance(innermost, int): + datatype = getattr(inner, "datatype", 0) + return (int(innermost), int(datatype) if datatype else 0) + + if hasattr(layer, "layer") and hasattr(layer, "datatype"): + return (int(layer.layer), int(layer.datatype)) + + if hasattr(layer, "value"): + if isinstance(layer.value, tuple): + return (int(layer.value[0]), int(layer.value[1])) + if isinstance(layer.value, int): + return (int(layer.value), 0) + + if isinstance(layer, str): + if "/" in layer: + parts = layer.split("/") + return (int(parts[0]), int(parts[1])) + return None + + try: + return (int(layer), 0) + except (TypeError, ValueError): + logger.warning("Could not parse layer %s", layer) + return None + + +def classify_layer_type( + layer_name: str, + material: str | None = None, +) -> Literal["conductor", "via", "dielectric", "substrate"]: + """Classify a layer as conductor, via, dielectric, or substrate. + + When *material* is provided, the materials DB is consulted for additional + classification hints (conductor vs dielectric). When ``None``, only + name-based heuristics are used. + """ + name_lower = layer_name.lower() + + if "via" in name_lower or "cont" in name_lower: + return "via" + + if any( + m in name_lower for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5"] + ): + return "conductor" + + if "substrate" in name_lower or name_lower == "sub": + return "substrate" + + # Material-based lookup (only when caller supplies material) + if material is not None: + from gsim.common.stack.materials import get_material_properties + + props = get_material_properties(material) + if props: + if props.type == "conductor": + return "conductor" + if props.type == "semiconductor" and "substrate" in name_lower: + return "substrate" + if props.type == "dielectric": + return "dielectric" + + material_lower = material.lower() + if material_lower in [ + "aluminum", + "copper", + "tungsten", + "gold", + "al", + "cu", + "w", + ]: + return "conductor" + + if "poly" in name_lower or "active" in name_lower: + return "conductor" + + return "dielectric" diff --git a/src/gsim/common/stack/extractor.py b/src/gsim/common/stack/extractor.py index fd8a6090..63e437a8 100644 --- a/src/gsim/common/stack/extractor.py +++ b/src/gsim/common/stack/extractor.py @@ -8,13 +8,13 @@ import logging from pathlib import Path -from typing import Any, Literal +from typing import Literal import yaml -from gdsfactory.technology import LayerLevel from gdsfactory.technology import LayerStack as GfLayerStack from pydantic import BaseModel, ConfigDict, Field +from gsim.common.stack._layer_utils import classify_layer_type, get_gds_layer_tuple from gsim.common.stack.materials import ( MATERIALS_DB, get_material_properties, @@ -279,87 +279,6 @@ def to_yaml(self, path: Path | None = None) -> str: return yaml_str -def _get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int]: - """Extract GDS layer tuple from LayerLevel.""" - layer: Any = layer_level.layer - - if isinstance(layer, tuple): - return (int(layer[0]), int(layer[1])) - - if isinstance(layer, int): - return (int(layer), 0) - - if hasattr(layer, "layer"): - inner = layer.layer - if hasattr(inner, "layer") and hasattr(inner, "datatype"): - return (int(inner.layer), int(inner.datatype)) # type: ignore[arg-type] - if isinstance(inner, int): - datatype = getattr(layer, "datatype", 0) - return (int(inner), int(datatype) if datatype else 0) - if hasattr(inner, "layer"): - innermost = inner.layer - if isinstance(innermost, int): - datatype = getattr(inner, "datatype", 0) - return (int(innermost), int(datatype) if datatype else 0) - - if hasattr(layer, "layer") and hasattr(layer, "datatype"): - return (int(layer.layer), int(layer.datatype)) # type: ignore[arg-type] - - if hasattr(layer, "value"): - if isinstance(layer.value, tuple): - return (int(layer.value[0]), int(layer.value[1])) # type: ignore[arg-type] - if isinstance(layer.value, int): - return (int(layer.value), 0) - - if isinstance(layer, str): - if "/" in layer: - parts = layer.split("/") - return (int(parts[0]), int(parts[1])) - return (0, 0) - - try: - return (int(layer), 0) # type: ignore[arg-type] - except (TypeError, ValueError): - logger.warning("Could not parse layer %s, using (0, 0)", layer) - return (0, 0) - - -def _classify_layer_type( - layer_name: str, material: str -) -> Literal["conductor", "via", "dielectric", "substrate"]: - """Classify a layer as conductor, via, dielectric, or substrate.""" - name_lower = layer_name.lower() - material_lower = material.lower() - - if "via" in name_lower: - return "via" - - if any( - m in name_lower for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5"] - ): - return "conductor" - - if "substrate" in name_lower or name_lower == "sub": - return "substrate" - - props = get_material_properties(material) - if props: - if props.type == "conductor": - return "conductor" - if props.type == "semiconductor" and "substrate" in name_lower: - return "substrate" - if props.type == "dielectric": - return "dielectric" - - if material_lower in ["aluminum", "copper", "tungsten", "gold", "al", "cu", "w"]: - return "conductor" - - if "poly" in name_lower: - return "conductor" - - return "dielectric" - - def extract_layer_stack( gf_layer_stack: GfLayerStack, pdk_name: str = "unknown", @@ -392,8 +311,8 @@ def extract_layer_stack( thickness = layer_level.thickness if layer_level.thickness is not None else 0.0 zmax = zmin + thickness material = layer_level.material or "unknown" - gds_layer = _get_gds_layer_tuple(layer_level) - layer_type = _classify_layer_type(layer_name, material) + gds_layer = get_gds_layer_tuple(layer_level) or (0, 0) + layer_type = classify_layer_type(layer_name, material) sidewall_angle = getattr(layer_level, "sidewall_angle", 0.0) or 0.0 if layer_type == "substrate" and not include_substrate: diff --git a/src/gsim/common/stack/visualization.py b/src/gsim/common/stack/visualization.py index 18a99731..7bbeb0ef 100644 --- a/src/gsim/common/stack/visualization.py +++ b/src/gsim/common/stack/visualization.py @@ -7,12 +7,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any import plotly.graph_objects as go -from gdsfactory.technology import LayerLevel from gdsfactory.technology import LayerStack as GfLayerStack +from gsim.common.stack._layer_utils import classify_layer_type, get_gds_layer_tuple + @dataclass class StackLayer: @@ -27,52 +27,6 @@ class StackLayer: layer_type: str = "conductor" # conductor, via, dielectric, substrate -def _get_gds_layer_number(layer_level: LayerLevel) -> int | None: - """Extract GDS layer number from LayerLevel.""" - layer: Any = layer_level.layer - - # Handle tuple - if isinstance(layer, tuple): - return int(layer[0]) - - # Handle int - if isinstance(layer, int): - return int(layer) - - # Handle LogicalLayer or enum with nested layer - if hasattr(layer, "layer"): - inner = layer.layer - if hasattr(inner, "layer"): - return int(inner.layer) # type: ignore[arg-type] - if isinstance(inner, int): - return int(inner) - - # Handle enum with value - if hasattr(layer, "value"): - if isinstance(layer.value, tuple): - return int(layer.value[0]) # type: ignore[arg-type] - return int(layer.value) # type: ignore[arg-type] - - return None - - -def _classify_layer(name: str) -> str: - """Classify layer type based on name.""" - name_lower = name.lower() - - if "via" in name_lower or "cont" in name_lower: - return "via" - if "substrate" in name_lower or name_lower == "sub": - return "substrate" - if any( - m in name_lower - for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5", "poly", "active"] - ): - return "conductor" - - return "dielectric" - - def parse_layer_stack(layer_stack: GfLayerStack) -> list[StackLayer]: """Parse a gdsfactory LayerStack into a list of StackLayer objects. @@ -89,8 +43,9 @@ def parse_layer_stack(layer_stack: GfLayerStack) -> list[StackLayer]: thickness = level.thickness if level.thickness is not None else 0.0 zmax = zmin + thickness material = level.material or None - gds_layer = _get_gds_layer_number(level) - layer_type = _classify_layer(name) + tup = get_gds_layer_tuple(level) + gds_layer = tup[0] if tup else None + layer_type = classify_layer_type(name) layers.append( StackLayer( diff --git a/src/gsim/common/viz/__init__.py b/src/gsim/common/viz/__init__.py index e472cc85..030b67d3 100644 --- a/src/gsim/common/viz/__init__.py +++ b/src/gsim/common/viz/__init__.py @@ -6,7 +6,6 @@ 3D backends: - PyVista (desktop, ``plot_prisms_3d``) - Open3D + Plotly (Jupyter, ``plot_prisms_3d_open3d``) - - Three.js / FastAPI (browser, ``serve_threejs_visualization``) 2D backends: - matplotlib (``plot_prism_slices``) @@ -18,7 +17,6 @@ export_3d_mesh, plot_prisms_3d, plot_prisms_3d_open3d, - serve_threejs_visualization, ) __all__ = [ @@ -27,5 +25,4 @@ "plot_prism_slices", "plot_prisms_3d", "plot_prisms_3d_open3d", - "serve_threejs_visualization", ] diff --git a/src/gsim/common/viz/_mesh_helpers.py b/src/gsim/common/viz/_mesh_helpers.py index 690b579c..97feb6fa 100644 --- a/src/gsim/common/viz/_mesh_helpers.py +++ b/src/gsim/common/viz/_mesh_helpers.py @@ -6,6 +6,8 @@ from __future__ import annotations +from typing import Any + import numpy as np from gsim.common.geometry_model import GeometryModel, Prism @@ -72,3 +74,99 @@ def simulation_box_corners( ] return points, lines + + +# --------------------------------------------------------------------------- +# Shared Delaunay triangulation for polygons with holes / concave polygons +# --------------------------------------------------------------------------- + + +def triangulate_polygon_with_holes( + shapely_polygon: Any, + z_base: float, + z_top: float, +) -> tuple[np.ndarray, list[np.ndarray], list[list[int]]] | None: + """Triangulate a Shapely polygon (possibly with holes) via Delaunay. + + Returns: + ``(verts_3d, valid_triangles, boundary_segments)`` where + *verts_3d* is (2*N, 3) — bottom then top copies of the 2D points, + *valid_triangles* is a list of index-triplet arrays (into the first + N points), and *boundary_segments* is a list of ``[i, j]`` pairs. + Returns ``None`` when triangulation fails. + """ + try: + import shapely.geometry as sg + from scipy.spatial import Delaunay + except ImportError: + return None + + all_points: list[tuple[float, float]] = [] + boundary_segments: list[list[int]] = [] + + exterior_coords = list(shapely_polygon.exterior.coords[:-1]) + start_idx = 0 + all_points.extend(exterior_coords) + boundary_segments = [ + [start_idx + i, start_idx + (i + 1) % len(exterior_coords)] + for i in range(len(exterior_coords)) + ] + + for interior in shapely_polygon.interiors: + interior_coords = list(interior.coords[:-1]) + start_idx = len(all_points) + all_points.extend(interior_coords) + boundary_segments.extend( + [start_idx + i, start_idx + (i + 1) % len(interior_coords)] + for i in range(len(interior_coords)) + ) + + if len(all_points) < 3: + return None + + points_2d = np.array(all_points) + tri = Delaunay(points_2d) + + valid_triangles = [ + simplex + for simplex in tri.simplices + if shapely_polygon.contains(sg.Point(*np.mean(points_2d[simplex], axis=0))) + ] + + if not valid_triangles: + return None + + verts_3d = np.array( + [[pt[0], pt[1], z_base] for pt in points_2d] + + [[pt[0], pt[1], z_top] for pt in points_2d] + ) + + return verts_3d, valid_triangles, boundary_segments + + +def collect_triangular_prism_geometry( + prisms: list[Prism], +) -> tuple[np.ndarray, list[int]] | None: + """Collect vertices from many triangular prisms for batch meshing. + + Returns: + ``(combined_vertices, prism_offsets)`` where *combined_vertices* + is (M, 3) and *prism_offsets[i]* is the vertex offset of the + *i*-th prism in that array. Returns ``None`` if *prisms* is empty. + """ + all_vertices: list[np.ndarray] = [] + prism_offsets: list[int] = [] + offset = 0 + + for prism in prisms: + base, top = prism_base_top_vertices(prism) + verts = np.vstack([base, top]) + all_vertices.append(verts) + prism_offsets.append(offset) + offset += len(verts) + + if not all_vertices: + return None + + combined = np.vstack(all_vertices) + return combined, prism_offsets diff --git a/src/gsim/common/viz/render3d.py b/src/gsim/common/viz/render3d.py index ae71fbcc..db9ad48d 100644 --- a/src/gsim/common/viz/render3d.py +++ b/src/gsim/common/viz/render3d.py @@ -12,12 +12,10 @@ export_3d_mesh, plot_prisms_3d, ) -from gsim.common.viz.render3d_threejs import serve_threejs_visualization __all__ = [ "create_web_export", "export_3d_mesh", "plot_prisms_3d", "plot_prisms_3d_open3d", - "serve_threejs_visualization", ] diff --git a/src/gsim/common/viz/render3d_open3d.py b/src/gsim/common/viz/render3d_open3d.py index 528211b3..778fd215 100644 --- a/src/gsim/common/viz/render3d_open3d.py +++ b/src/gsim/common/viz/render3d_open3d.py @@ -13,8 +13,10 @@ from gsim.common.geometry_model import GeometryModel, Prism from gsim.common.viz._colors import generate_layer_colors_with_opacity from gsim.common.viz._mesh_helpers import ( + collect_triangular_prism_geometry, prism_base_top_vertices, simulation_box_corners, + triangulate_polygon_with_holes, ) try: @@ -120,23 +122,58 @@ def plot_prisms_3d_open3d( cx = cy = cz = 0.0 rs = 10.0 + # Standard camera views for view buttons + d = 2.0 # distance multiplier for orthographic views + _views = { + "Iso": dict(eye=dict(x=1.5, y=1.5, z=1.5)), + "Top": dict(eye=dict(x=0, y=0, z=d), up=dict(x=0, y=1, z=0)), + "Front": dict(eye=dict(x=0, y=-d, z=0), up=dict(x=0, y=0, z=1)), + "Right": dict(eye=dict(x=d, y=0, z=0), up=dict(x=0, y=0, z=1)), + } + view_buttons = [ + dict( + label=name, + method="relayout", + args=[ + { + "scene.camera.eye": cam["eye"], + "scene.camera.up": cam.get("up", dict(x=0, y=0, z=1)), + "scene.camera.center": dict(x=0, y=0, z=0), + } + ], + ) + for name, cam in _views.items() + ] + fig.update_layout( scene=dict( xaxis_title="X (um)", yaxis_title="Y (um)", zaxis_title="Z (um)", - aspectmode="data", + aspectmode="cube", camera=dict( eye=dict(x=1.5, y=1.5, z=1.5), center=dict(x=0, y=0, z=0), - projection=dict(type="perspective"), + projection=dict(type="orthographic"), ), xaxis=dict(range=[cx - rs * 0.6, cx + rs * 0.6]), yaxis=dict(range=[cy - rs * 0.6, cy + rs * 0.6]), zaxis=dict(range=[cz - rs * 0.6, cz + rs * 0.6]), ), - title="3D Geometry Visualization", + title="", dragmode="orbit", + updatemenus=[ + dict( + type="buttons", + direction="down", + x=0.01, + y=0.99, + xanchor="left", + yanchor="top", + bgcolor="rgba(255,255,255,0.8)", + buttons=view_buttons, + ), + ], **kwargs, ) @@ -199,16 +236,15 @@ def _convert_prisms_to_open3d( def _merge_triangular_prisms_o3d(prisms: list[Prism]) -> Any: """Merge many triangular prisms into one Open3D mesh for performance.""" - all_vertices: list[np.ndarray] = [] - all_triangles: list[list[int]] = [] - offset = 0 + result = collect_triangular_prism_geometry(prisms) + if result is None: + return None - for prism in prisms: - base, top = prism_base_top_vertices(prism) - verts = np.vstack([base, top]) - all_vertices.append(verts) - n = len(base) + combined, prism_offsets = result + n = 3 # triangular prisms + all_triangles: list[list[int]] = [] + for offset in prism_offsets: # bottom all_triangles.append([offset, offset + 2, offset + 1]) # top @@ -219,16 +255,11 @@ def _merge_triangular_prisms_o3d(prisms: list[Prism]) -> Any: all_triangles.append([offset + i, offset + ni, offset + ni + n]) all_triangles.append([offset + i, offset + ni + n, offset + i + n]) - offset += len(verts) - - if all_vertices: - combined = np.vstack(all_vertices) - mesh = o3d.geometry.TriangleMesh() - mesh.vertices = o3d.utility.Vector3dVector(combined) - mesh.triangles = o3d.utility.Vector3iVector(all_triangles) - mesh.compute_vertex_normals() - return mesh - return None + mesh = o3d.geometry.TriangleMesh() + mesh.vertices = o3d.utility.Vector3dVector(combined) + mesh.triangles = o3d.utility.Vector3iVector(all_triangles) + mesh.compute_vertex_normals() + return mesh def _create_prism_mesh_o3d( @@ -273,62 +304,26 @@ def _create_prism_mesh_with_holes_o3d( top_vertices: np.ndarray, ) -> Any: """Create Open3D mesh from a Shapely polygon with holes via Delaunay.""" - try: - import shapely.geometry as sg - from scipy.spatial import Delaunay - except ImportError: - return _create_prism_mesh_o3d(base_vertices, top_vertices) - - all_points: list[tuple[float, float]] = [] - boundary_segments: list[list[int]] = [] - - exterior_coords = list(shapely_polygon.exterior.coords[:-1]) - start_idx = 0 - all_points.extend(exterior_coords) - boundary_segments = [ - [start_idx + i, start_idx + (i + 1) % len(exterior_coords)] - for i in range(len(exterior_coords)) - ] - - for interior in shapely_polygon.interiors: - interior_coords = list(interior.coords[:-1]) - start_idx = len(all_points) - all_points.extend(interior_coords) - boundary_segments.extend( - [start_idx + i, start_idx + (i + 1) % len(interior_coords)] - for i in range(len(interior_coords)) - ) - - if len(all_points) < 3: - return _create_prism_mesh_o3d(base_vertices, top_vertices) - - points_2d = np.array(all_points) - tri = Delaunay(points_2d) - - valid_triangles = [ - simplex - for simplex in tri.simplices - if shapely_polygon.contains(sg.Point(*np.mean(points_2d[simplex], axis=0))) - ] - - if not valid_triangles: - return _create_prism_mesh_o3d(base_vertices, top_vertices) - z_base = base_vertices[0, 2] if len(base_vertices) > 0 else 0 z_top = top_vertices[0, 2] if len(top_vertices) > 0 else z_base + 1 - verts_3d: list[list[float]] = [[pt[0], pt[1], z_base] for pt in points_2d] + [ - [pt[0], pt[1], z_top] for pt in points_2d - ] + result = triangulate_polygon_with_holes(shapely_polygon, z_base, z_top) + if result is None: + return _create_prism_mesh_o3d(base_vertices, top_vertices) + + verts_3d, valid_triangles, boundary_segments = result + n_pts = len(verts_3d) // 2 - n_pts = len(points_2d) + # bottom triangles faces_3d: list[list[int]] = [ [tri_idx[0], tri_idx[1], tri_idx[2]] for tri_idx in valid_triangles ] + # top triangles (reversed winding) faces_3d.extend( [tri_idx[0] + n_pts, tri_idx[2] + n_pts, tri_idx[1] + n_pts] for tri_idx in valid_triangles ) + # side quads (2 tris each) for seg in boundary_segments: i, j = seg faces_3d.append([i, j, j + n_pts]) diff --git a/src/gsim/common/viz/render3d_pyvista.py b/src/gsim/common/viz/render3d_pyvista.py index f614e5d1..2dd13a28 100644 --- a/src/gsim/common/viz/render3d_pyvista.py +++ b/src/gsim/common/viz/render3d_pyvista.py @@ -12,7 +12,9 @@ from gsim.common.geometry_model import GeometryModel, Prism from gsim.common.viz._colors import generate_layer_colors from gsim.common.viz._mesh_helpers import ( + collect_triangular_prism_geometry, prism_base_top_vertices, + triangulate_polygon_with_holes, ) try: @@ -215,16 +217,15 @@ def _convert_prisms_to_meshes( def _merge_triangular_prisms(prisms: list[Prism]) -> Any: """Merge many triangular prisms into one PyVista mesh for performance.""" - all_vertices: list[np.ndarray] = [] - all_faces: list[int] = [] - offset = 0 + result = collect_triangular_prism_geometry(prisms) + if result is None: + return None - for prism in prisms: - base, top = prism_base_top_vertices(prism) - verts = np.vstack([base, top]) - all_vertices.append(verts) - n = len(base) + combined, prism_offsets = result + n = 3 # triangular prisms + all_faces: list[int] = [] + for offset in prism_offsets: # bottom face all_faces.extend([3, offset + 2, offset + 1, offset + 0]) # top face @@ -233,20 +234,10 @@ def _merge_triangular_prisms(prisms: list[Prism]) -> Any: for i in range(n): ni = (i + 1) % n all_faces.extend( - [ - 4, - offset + i, - offset + ni, - offset + ni + n, - offset + i + n, - ] + [4, offset + i, offset + ni, offset + ni + n, offset + i + n] ) - offset += len(verts) - if all_vertices: - combined = np.vstack(all_vertices) - return pv.PolyData(combined, all_faces) - return None + return pv.PolyData(combined, all_faces) def _create_prism_mesh( @@ -288,68 +279,24 @@ def _create_prism_mesh_with_holes( top_vertices: np.ndarray, ) -> Any: """Create PyVista mesh from a Shapely polygon with holes using Delaunay.""" - try: - import shapely.geometry as sg - from scipy.spatial import Delaunay - except ImportError: - return _create_prism_mesh(base_vertices, top_vertices) - - all_points: list[tuple[float, float]] = [] - boundary_segments: list[list[int]] = [] - - exterior_coords = list(shapely_polygon.exterior.coords[:-1]) - start_idx = 0 - all_points.extend(exterior_coords) - boundary_segments = [ - [start_idx + i, start_idx + (i + 1) % len(exterior_coords)] - for i in range(len(exterior_coords)) - ] - - for interior in shapely_polygon.interiors: - interior_coords = list(interior.coords[:-1]) - start_idx = len(all_points) - all_points.extend(interior_coords) - boundary_segments.extend( - [start_idx + i, start_idx + (i + 1) % len(interior_coords)] - for i in range(len(interior_coords)) - ) - - if len(all_points) < 3: - return _create_prism_mesh(base_vertices, top_vertices) - - points_2d = np.array(all_points) - tri = Delaunay(points_2d) - - valid_triangles = [ - simplex - for simplex in tri.simplices - if shapely_polygon.contains(sg.Point(*np.mean(points_2d[simplex], axis=0))) - ] - - if not valid_triangles: - return _create_prism_mesh(base_vertices, top_vertices) - z_base = base_vertices[0, 2] if len(base_vertices) > 0 else 0 z_top = top_vertices[0, 2] if len(top_vertices) > 0 else z_base + 1 - verts_3d: list[list[float]] = [[pt[0], pt[1], z_base] for pt in points_2d] + [ - [pt[0], pt[1], z_top] for pt in points_2d - ] + result = triangulate_polygon_with_holes(shapely_polygon, z_base, z_top) + if result is None: + return _create_prism_mesh(base_vertices, top_vertices) - n_pts = len(points_2d) + verts_3d, valid_triangles, boundary_segments = result + n_pts = len(verts_3d) // 2 faces_pv: list[int] = [] + # bottom triangles for tri_idx in valid_triangles: faces_pv.extend([3, tri_idx[0], tri_idx[1], tri_idx[2]]) + # top triangles (reversed winding) for tri_idx in valid_triangles: - faces_pv.extend( - [ - 3, - tri_idx[0] + n_pts, - tri_idx[2] + n_pts, - tri_idx[1] + n_pts, - ] - ) + faces_pv.extend([3, tri_idx[0] + n_pts, tri_idx[2] + n_pts, tri_idx[1] + n_pts]) + # side quads for seg in boundary_segments: i, j = seg faces_pv.extend([4, i, j, j + n_pts, i + n_pts]) diff --git a/src/gsim/common/viz/render3d_threejs.py b/src/gsim/common/viz/render3d_threejs.py deleted file mode 100644 index d9daf3c0..00000000 --- a/src/gsim/common/viz/render3d_threejs.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Three.js / FastAPI-based 3D rendering for GeometryModel. - -Provides browser-based interactive 3D visualisation via a local FastAPI server -that serves a Three.js HTML page. -""" - -from __future__ import annotations - -import json -import logging -from pathlib import Path -from typing import Any - -import numpy as np - -from gsim.common.geometry_model import GeometryModel -from gsim.common.viz._colors import generate_layer_colors_with_opacity -from gsim.common.viz._mesh_helpers import simulation_box_corners -from gsim.common.viz.render3d_open3d import _convert_prisms_to_open3d - -logger = logging.getLogger(__name__) - -# --------------------------------------------------------------------------- -# Public API -# --------------------------------------------------------------------------- - - -def serve_threejs_visualization( - geometry_model: GeometryModel, - *, - show_edges: bool = False, - color_by_layer: bool = True, - show_simulation_box: bool = True, - layer_opacity: dict[str, float] | None = None, - port: int = 8000, - auto_open: bool = True, - show_stats: bool = False, - **kwargs: Any, -) -> str: - """Start a FastAPI server with a Three.js 3D viewer. - - Args: - geometry_model: GeometryModel containing prisms and bbox. - show_edges: Show wireframe edges. - color_by_layer: Colour each layer differently. - show_simulation_box: Draw the simulation box. - layer_opacity: Per-layer opacity override. - port: Port to serve on (default 8000). - auto_open: Open the browser automatically. - show_stats: Show FPS counter. - **kwargs: Extra Three.js options. - - Returns: - URL of the running server. - """ - try: - import threading - import time - import webbrowser - - import uvicorn - from fastapi import FastAPI - from fastapi.responses import HTMLResponse - except ImportError as err: - raise ImportError( - "FastAPI and uvicorn required. Install with: pip install fastapi uvicorn" - ) from err - - app = FastAPI(title="3D Geometry Viewer") - - layer_meshes = _convert_prisms_to_open3d(geometry_model) - colors, opacity_dict = generate_layer_colors_with_opacity( - list(layer_meshes.keys()), layer_opacity - ) - - logger.info("Converting geometry: %d layers found", len(layer_meshes)) - for layer_name, meshes in layer_meshes.items(): - logger.info(" Layer '%s': %d meshes", layer_name, len(meshes)) - - threejs_data = _convert_to_threejs_data( - layer_meshes, colors, opacity_dict, color_by_layer - ) - - if show_simulation_box: - threejs_data["simulation_box"] = _create_simulation_box_threejs(geometry_model) - - total_vertices = 0 - total_faces = 0 - for layer in threejs_data.get("layers", []): - for mesh in layer.get("meshes", []): - total_vertices += len(mesh.get("vertices", [])) // 3 - total_faces += len(mesh.get("faces", [])) // 3 - logger.info( - "Three.js data prepared: %d vertices, %d faces", - total_vertices, - total_faces, - ) - - @app.get("/", response_class=HTMLResponse) - def get_visualization(): - """Return the Three.js viewer HTML page.""" - return _generate_threejs_html( - threejs_data, - show_edges=show_edges, - show_stats=show_stats, - **kwargs, - ) - - @app.get("/api/geometry") - def get_geometry_data(): - """Return geometry mesh data as JSON.""" - return threejs_data - - @app.get("/api/info") - def get_info(): - """Return summary info about the geometry.""" - return { - "layers": len(threejs_data.get("layers", [])), - "total_meshes": sum( - len(layer.get("meshes", [])) for layer in threejs_data.get("layers", []) - ), - "has_simulation_box": "simulation_box" in threejs_data, - "layer_names": [ - layer.get("name") for layer in threejs_data.get("layers", []) - ], - } - - server_url = f"http://localhost:{port}" - - def run_server(): - try: - uvicorn.run(app, host="127.0.0.1", port=port, log_level="info") - except Exception as e: - logger.info("Server error: %s", e) - - server_thread = threading.Thread(target=run_server, daemon=True) - server_thread.start() - time.sleep(1) - - if auto_open: - webbrowser.open(server_url) - - logger.info("Server started at: %s", server_url) - logger.info("Geometry API available at: %s/api/geometry", server_url) - logger.info("Server running in background. Keep Python session alive to view.") - - return server_url - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - - -def _convert_to_threejs_data( - layer_meshes: dict[str, list[Any]], - colors: dict[str, list[float]], - opacity_dict: dict[str, float], - color_by_layer: bool, -) -> dict[str, Any]: - """Convert Open3D meshes to Three.js-compatible JSON data.""" - threejs_data: dict[str, Any] = {"layers": []} - - for layer_name, meshes in layer_meshes.items(): - layer_color = colors[layer_name] if color_by_layer else [0.7, 0.7, 0.7] - layer_opacity = opacity_dict.get(layer_name, 0.8) - - layer_data: dict[str, Any] = { - "name": layer_name, - "color": [int(c * 255) for c in layer_color[:3]], - "opacity": layer_opacity, - "meshes": [], - } - - for i, mesh in enumerate(meshes): - vertices = np.asarray(mesh.vertices).flatten().tolist() - faces = np.asarray(mesh.triangles).flatten().tolist() - - layer_data["meshes"].append( - { - "vertices": vertices, - "faces": faces, - "id": f"{layer_name}_{i}", - } - ) - - threejs_data["layers"].append(layer_data) - - return threejs_data - - -def _create_simulation_box_threejs( - geometry_model: GeometryModel, -) -> dict[str, Any]: - """Create simulation box data for Three.js.""" - points, lines = simulation_box_corners(geometry_model) - return { - "vertices": points.flatten().tolist(), - "lines": [idx for pair in lines for idx in pair], - "color": [0, 0, 0], - } - - -def _generate_threejs_html( - threejs_data: dict[str, Any], - *, - show_edges: bool = False, - show_stats: bool = False, - **kwargs: Any, -) -> str: - """Generate HTML using the viewer.html template.""" - template_path = Path(__file__).parent / "templates" / "viewer.html" - - with open(template_path) as f: - template = f.read() - - width = kwargs.get("width", "100vw") - height = kwargs.get("height", "100vh") - background_color = kwargs.get("background_color", "#f0f0f0") - show_wireframe = str(show_edges).lower() - stats_display = "block" if show_stats else "none" - - data_json = json.dumps(threejs_data, indent=2) - - return template.format( - geometry_data=data_json, - show_wireframe=show_wireframe, - show_stats=str(show_stats).lower(), - width=width, - height=height, - background_color=background_color, - stats_display=stats_display, - ) diff --git a/src/gsim/common/viz/templates/viewer.html b/src/gsim/common/viz/templates/viewer.html deleted file mode 100644 index 23323878..00000000 --- a/src/gsim/common/viz/templates/viewer.html +++ /dev/null @@ -1,482 +0,0 @@ - - - - - - 3D Geometry - Three.js Viewer - - - -
-
-

🔬 3D Geometry Visualization

-

Mouse: Rotate view

-

Wheel: Zoom in/out (enhanced sensitivity)

-

Right-click: Pan camera

-

Double-click: Reset view

-
- -
- -
- -
- -
- -
-

📊 Layers

-
-
- -
-
FPS: --
-
Vertices: --
-
Faces: --
-
-
- - - - - - - - - diff --git a/src/gsim/meep/__init__.py b/src/gsim/meep/__init__.py index dec415b4..d62f4365 100644 --- a/src/gsim/meep/__init__.py +++ b/src/gsim/meep/__init__.py @@ -9,12 +9,13 @@ from gsim import meep sim = meep.Simulation() - sim.geometry.component = ybranch - sim.source.port = "o1" + sim.geometry(component=ybranch, z_crop="auto") + sim.materials = {"si": 3.47, "SiO2": 1.44} + sim.source(port="o1", wavelength=1.55, bandwidth=0.01, num_freqs=11) sim.monitors = ["o1", "o2"] - sim.solver.stopping = "dft_decay" - sim.solver.max_time = 200 - result = sim.run("./meep-sim") + sim.domain(pml=1.0, margin=0.5) + sim.solver(resolution=32, simplify_tol=0.01) + result = sim.run() """ from gsim.meep.models import ( diff --git a/src/gsim/meep/meep-overview.md b/src/gsim/meep/meep-overview.md index b15cbabd..01d513ba 100644 --- a/src/gsim/meep/meep-overview.md +++ b/src/gsim/meep/meep-overview.md @@ -19,34 +19,24 @@ GDS + PDK ──► 3D Geometry ──► Client Viz ──► SimConfig J from gsim import meep sim = meep.Simulation() -sim.geometry.component = ybranch -sim.geometry.z_crop = "auto" +sim.geometry(component=ybranch, z_crop="auto") sim.materials = {"si": 3.47, "SiO2": 1.44} # float shorthand -sim.source.port = "o1" -sim.source.wavelength = 1.55 -sim.source.bandwidth = 0.1 -sim.source.num_freqs = 11 +sim.source(port="o1", wavelength=1.55, bandwidth=0.1, num_freqs=11) sim.monitors = ["o1", "o2"] -sim.domain.pml = 1.0 -sim.domain.margin = 0.5 -sim.solver.resolution = 32 -sim.solver.stopping = "dft_decay" -sim.solver.max_time = 200 -sim.solver.simplify_tol = 0.01 -sim.solver.save_geometry = True -sim.solver.save_fields = True +sim.domain(pml=1.0, margin=0.5) +sim.solver(resolution=32, simplify_tol=0.01) sim.plot_2d(slices="xyz") -result = sim.run("./meep-sim") +result = sim.run() ``` ### Design principles - **6 typed physics objects** -- `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD` + `monitors: list[str]` -- assigned to a `Simulation` container. No ordering dependencies. -- **Attribute style or constructor style** -- `sim.source.port = "o1"` or `sim.source = ModeSource(port="o1")`. Both work via Pydantic `validate_assignment=True`. +- **Callable, attribute, or constructor style** -- `sim.source(port="o1")` (callable), `sim.source.port = "o1"` (attribute), or `sim.source = ModeSource(port="o1")` (constructor). All work via Pydantic `validate_assignment=True`. - **Float shorthand for materials** -- `{"si": 3.47}` auto-normalizes to `Material(n=3.47)` via validator. -- **Flat stopping fields** -- `stopping` mode string + flat fields (`max_time`, `stopping_threshold`, etc.) on `FDTD`. +- **Flat stopping fields + method API** -- `stopping` mode string (`energy_decay`/`dft_decay`/`decay`/`fixed`) + flat fields on `FDTD`, plus convenience methods (`stop_when_energy_decayed()`, `stop_when_dft_decayed()`, `stop_when_fields_decayed()`, `stop_after()`). `StoppingConfig.mode` now includes `energy_decay`. - **Source defines spectral window** -- `WavelengthConfig` derived from `ModeSource.wavelength/bandwidth/num_freqs`. Monitors are just port name strings. -- **JSON contract unchanged** -- `write_config()` translates new API -> existing `SimConfig` -> JSON. Runner template untouched. +- **JSON contract** -- `write_config()` translates new API -> existing `SimConfig` -> JSON. `StoppingConfig.mode` extended with `"energy_decay"` (runner updated to handle it). --- @@ -54,25 +44,32 @@ result = sim.run("./meep-sim") ### `gsim.meep` -- Public API -| Module | Purpose | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| `__init__.py` | Exports: `Simulation`, all model classes | -| `models/api.py` | Declarative models: `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD`, `Symmetry` | -| `simulation.py` | `Simulation` container -- `write_config()`, `run()`, `validate_config()`, `plot_2d()`/`plot_3d()`, `estimate_meep_np()` | -| `viz.py` | Standalone viz helpers -- `build_geometry_model()`, `plot_2d()`, `plot_3d()` (calls `common/viz`) | -| `models/config.py` | `SimConfig` + all sub-configs (JSON serialization layer) | -| `models/results.py` | `SParameterResult` -- CSV + debug JSON + diagnostic PNGs | -| `ports.py` | `extract_port_info()` -- port center/direction/normal from gdsfactory | -| `materials.py` | `resolve_materials()` -- material names -> (n, k) via common DB | -| `script.py` | `generate_meep_script()` -- cloud runner template (string in Python) | -| `overlay.py` | `SimOverlay` + `PortOverlay` + `DielectricOverlay` -- viz metadata | +| Module | Purpose | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `__init__.py` | Exports: `Simulation`, all model classes | +| `models/api.py` | Declarative models: `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD`, `Symmetry`. All except `Material`/`Symmetry` have `__call__(**kwargs)` for fluent updates | +| `simulation.py` | `Simulation` container -- `write_config()`, `run()`, `validate_config()`, `plot_2d()`/`plot_3d()`, `estimate_meep_np()` | +| `viz.py` | Standalone viz helpers -- `build_geometry_model()`, `plot_2d()`, `plot_3d()` (calls `common/viz`) | +| `models/config.py` | `SimConfig` + all sub-configs (JSON serialization layer) | +| `models/results.py` | `SParameterResult` -- CSV + debug JSON + diagnostic PNGs | +| `ports.py` | `extract_port_info()` -- port center/direction/normal from gdsfactory | +| `materials.py` | `resolve_materials()` -- material names -> (n, k) via common DB | +| `script.py` | `generate_meep_script()` -- cloud runner template (string in Python) | +| `overlay.py` | `SimOverlay` + `PortOverlay` + `DielectricOverlay` -- viz metadata | ### `gsim.common` -- Shared infrastructure -Solver-agnostic: `LayeredComponentBase`, `GeometryModel`/`Prism`, `LayerStack`, `MaterialProperties`, `viz/` (Matplotlib 2D, PyVista/Open3D/Three.js 3D). +Solver-agnostic: `LayeredComponentBase`, `GeometryModel`/`Prism`, `LayerStack`, `ValidationResult`, `viz/` (Matplotlib 2D, PyVista/Open3D 3D). + +Key sub-modules: + +- `stack/_layer_utils.py` — shared `get_gds_layer_tuple()` and `classify_layer_type()` (used by both `extractor` and `visualization`) +- `viz/_mesh_helpers.py` — shared Delaunay triangulation (`triangulate_polygon_with_holes`) and batch prism geometry (`collect_triangular_prism_geometry`), used by both PyVista and Open3D renderers ### Visualization pipeline +3D backends: PyVista (desktop) and Open3D+Plotly (Jupyter, with view buttons for Iso/Top/Front/Right). Three.js backend removed. + ``` Simulation.plot_2d() -> viz.build_geometry_model(component, stack, domain_config) @@ -96,7 +93,8 @@ Simulation.plot_2d() - **Z-crop** -- Auto-crops stack around core layer. Full UBC stack is 15um; cropped to ~1.5um (massive compute savings). - **Port extension into PML** -- `gf.components.extend_ports()` at `write_config()` time. Original bbox stored in `SimConfig.component_bbox` for correct cell sizing. - **Symmetries disabled for S-params** -- `add_mode_monitor` uses `use_symmetry=false` internally; `get_eigenmode_coefficients` doesn't apply `S.multiplicity()`. Source port coefficients underestimated ~2x. gplugins also never uses `mp.Mirror`. -- **`dft_min_run_time` default 100** -- Prevents false convergence before pulse traverses device. Must exceed `device_length * n_group`. +- **`energy_decay` default stopping** -- `dft_decay` suffers from false early convergence (DFTs can converge on near-zero fields). `energy_decay` monitors total field energy and waits for it to peak then decay — more robust for directional couplers and similar devices. Runner uses `mp.stop_when_energy_decayed(dt, decay_by)` with a `_make_time_cap(max_time)` as a secondary callable in the `until_after_sources` list (OR logic). The `decay` mode also uses `_make_time_cap` instead of passing a raw float to `until_after_sources`. +- **`dft_min_run_time` is absolute sim time** -- Not time-after-sources. With a broadband source turning off at ~t=78 and min_run_time=100, DFT checking starts at t=100 (only ~22 units after source ends). Must exceed `device_length * n_group`. - **Stack resolved lazily** -- `_ensure_stack()` falls back to `get_stack()` (active PDK defaults) when no explicit stack kwargs are set. Same path for `write_config()` and viz. --- diff --git a/src/gsim/meep/models/api.py b/src/gsim/meep/models/api.py index 2cee14cc..0a82e198 100644 --- a/src/gsim/meep/models/api.py +++ b/src/gsim/meep/models/api.py @@ -31,6 +31,12 @@ class Geometry(BaseModel): description='Z-crop mode: "auto" | layer_name | None (no crop)', ) + def __call__(self, **kwargs: Any) -> Geometry: + """Update fields in place. Returns self for chaining.""" + for k, v in kwargs.items(): + setattr(self, k, v) + return self + # --------------------------------------------------------------------------- # Material @@ -76,6 +82,12 @@ class ModeSource(BaseModel): description="Number of frequency points", ) + def __call__(self, **kwargs: Any) -> ModeSource: + """Update fields in place. Returns self for chaining.""" + for k, v in kwargs.items(): + setattr(self, k, v) + return self + # --------------------------------------------------------------------------- # Domain @@ -123,6 +135,12 @@ class Domain(BaseModel): description="Mirror symmetry planes. Not yet used in production runs.", ) + def __call__(self, **kwargs: Any) -> Domain: + """Update fields in place. Returns self for chaining.""" + for k, v in kwargs.items(): + setattr(self, k, v) + return self + # --------------------------------------------------------------------------- # FDTD solver @@ -137,24 +155,35 @@ class FDTD(BaseModel): resolution: int = Field(default=32, ge=4, description="Pixels per micrometer") # Stopping criteria (flat fields instead of variant classes) - stopping: Literal["fixed", "decay", "dft_decay"] = Field( - default="dft_decay", - description="Stopping mode: fixed time, field decay, or DFT convergence", + stopping: Literal["fixed", "decay", "dft_decay", "energy_decay"] = Field( + default="energy_decay", + description=( + "Stopping mode: 'energy_decay' (recommended) monitors total field " + "energy decay; 'dft_decay' waits for DFT convergence; 'decay' " + "monitors a field component at a point; 'fixed' runs for max_time." + ), ) max_time: float = Field( - default=200.0, gt=0, description="Max run time after sources (um/c)" + default=2000.0, gt=0, description="Max run time after sources (um/c)" ) stopping_threshold: float = Field( default=1e-3, gt=0, lt=1, description="Decay/convergence threshold" ) stopping_min_time: float = Field( - default=100.0, ge=0, description="Min run time for dft_decay mode" + default=100.0, + ge=0, + description=( + "Minimum absolute sim time for dft_decay mode (not time-after-sources). " + "Must exceed pulse transit time to avoid false convergence." + ), ) stopping_component: str = Field( default="Ey", description="Field component for decay mode" ) stopping_dt: float = Field( - default=50.0, gt=0, description="Decay measurement window for decay mode" + default=50.0, + gt=0, + description="Decay measurement window for decay/energy_decay modes", ) stopping_monitor_port: str | None = Field( default=None, description="Port to monitor for decay mode" @@ -173,6 +202,88 @@ class FDTD(BaseModel): description="Shapely simplification tolerance in um (0=off)", ) + # -- Convenience methods for stopping configuration -- + + def stop_when_energy_decayed( + self, dt: float = 50.0, decay_by: float = 1e-3 + ) -> FDTD: + """Stop when total field energy in the cell decays (recommended). + + Monitors total electromagnetic energy and stops when it has decayed + by ``decay_by`` from its peak value. More robust than ``dft_decay`` + for devices where DFTs can falsely converge on near-zero fields. + + Args: + dt: Time window between energy checks (MEEP time units). + decay_by: Fractional energy decay threshold (e.g. 1e-3 = 0.1%). + + Returns: + self (for fluent chaining). + """ + self.stopping = "energy_decay" + self.stopping_dt = dt + self.stopping_threshold = decay_by + return self + + def stop_when_dft_decayed(self, tol: float = 1e-3, min_time: float = 100.0) -> FDTD: + """Stop when all DFT monitors converge. + + Args: + tol: DFT convergence tolerance. + min_time: Minimum absolute sim time before checking convergence. + + Returns: + self (for fluent chaining). + """ + self.stopping = "dft_decay" + self.stopping_threshold = tol + self.stopping_min_time = min_time + return self + + def stop_when_fields_decayed( + self, + dt: float = 50.0, + component: str = "Ey", + decay_by: float = 1e-3, + monitor_port: str | None = None, + ) -> FDTD: + """Stop when a field component decays at a point. + + Args: + dt: Decay measurement time window. + component: Field component name (e.g. "Ey", "Hz"). + decay_by: Fractional decay threshold. + monitor_port: Port to monitor (None = first non-source port). + + Returns: + self (for fluent chaining). + """ + self.stopping = "decay" + self.stopping_dt = dt + self.stopping_component = component + self.stopping_threshold = decay_by + self.stopping_monitor_port = monitor_port + return self + + def stop_after(self, time: float) -> FDTD: + """Run for a fixed time after sources turn off. + + Args: + time: Run time after sources in MEEP time units (um/c). + + Returns: + self (for fluent chaining). + """ + self.stopping = "fixed" + self.max_time = time + return self + + def __call__(self, **kwargs: Any) -> FDTD: + """Update fields in place. Returns self for chaining.""" + for k, v in kwargs.items(): + setattr(self, k, v) + return self + # Diagnostics — output control for plots, fields, animations save_geometry: bool = Field(default=True) save_fields: bool = Field(default=True) diff --git a/src/gsim/meep/models/config.py b/src/gsim/meep/models/config.py index 52abcfa7..973191f8 100644 --- a/src/gsim/meep/models/config.py +++ b/src/gsim/meep/models/config.py @@ -33,7 +33,7 @@ class DomainConfig(BaseModel): Cell size formula: cell_x = bbox_width + 2*(margin_xy + dpml) cell_y = bbox_height + 2*(margin_xy + dpml) - cell_z = z_extent + 2*dpml (z-margins baked into z_extent via set_z_crop) + cell_z = z_extent + 2*dpml (z-margins baked into z_extent) """ model_config = ConfigDict(validate_assignment=True) @@ -51,7 +51,7 @@ class DomainConfig(BaseModel): port_margin: float = Field( default=0.5, ge=0, - description="Margin on each side of port waveguide width for mode monitors in um", + description="Margin on each side of port width for monitors (um)", ) extend_ports: float = Field( default=0.0, @@ -64,19 +64,31 @@ class DomainConfig(BaseModel): class StoppingConfig(BaseModel): """Controls when the MEEP simulation stops. - ``fixed`` mode runs for a fixed time after sources turn off. + ``energy_decay`` mode (recommended) monitors total electromagnetic + energy in the cell and stops when it decays by ``threshold`` from + its peak. Robust against false early convergence. + + ``dft_decay`` mode monitors convergence of all DFT monitors and + stops when they stabilize. ``dft_min_run_time`` is an *absolute* + sim time (not time-after-sources) — with a broadband source turning + off at ~t=78, a min_run_time=100 starts checking at t=100, only ~22 + time units after the source ends. + ``decay`` mode monitors field decay at a point and stops when the fields have decayed by ``threshold``, with ``max_time`` as a numeric time cap (whichever fires first). - ``dft_decay`` mode monitors convergence of all DFT monitors and - stops when they stabilize, with built-in min/max time bounds. - Best for S-parameter extraction. + + ``fixed`` mode runs for a fixed time after sources turn off. """ model_config = ConfigDict(validate_assignment=True) - mode: Literal["fixed", "decay", "dft_decay"] = Field(default="fixed") - max_time: float = Field(default=100.0, gt=0, serialization_alias="run_after_sources") + mode: Literal["fixed", "decay", "dft_decay", "energy_decay"] = Field( + default="fixed" + ) + max_time: float = Field( + default=100.0, gt=0, serialization_alias="run_after_sources" + ) decay_dt: float = Field(default=50.0, gt=0) decay_component: str = Field(default="Ey") threshold: float = Field(default=1e-3, gt=0, lt=1, serialization_alias="decay_by") @@ -84,9 +96,9 @@ class StoppingConfig(BaseModel): dft_min_run_time: float = Field( default=100, ge=0, - description="Minimum run time after sources for dft_decay mode. " - "Must exceed pulse transit time through the device to avoid " - "false convergence on near-zero fields at output ports.", + description="Minimum absolute sim time for dft_decay mode (not " + "time-after-sources). Must exceed pulse transit time through the " + "device to avoid false convergence on near-zero fields.", ) @@ -104,7 +116,9 @@ class SourceConfig(BaseModel): bandwidth: float | None = Field( default=None, - description="Source Gaussian bandwidth in wavelength um. None = auto (~3x monitor bw).", + description=( + "Source Gaussian bandwidth in wavelength um. None = auto (~3x monitor bw)." + ), ) port: str | None = Field( default=None, @@ -225,7 +239,9 @@ class DiagnosticsConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - save_geometry: bool = Field(default=True, description="Pre-run geometry cross-section plots") + save_geometry: bool = Field( + default=True, description="Pre-run geometry cross-section plots" + ) save_fields: bool = Field(default=True, description="Post-run field snapshot") save_epsilon_raw: bool = Field( default=False, description="Raw epsilon .npy (advanced)" @@ -234,7 +250,8 @@ class DiagnosticsConfig(BaseModel): default=False, description="Field animation MP4 (heavy, needs ffmpeg)" ) animation_interval: float = Field( - default=0.5, gt=0, + default=0.5, + gt=0, description="MEEP time units between animation frames", ) preview_only: bool = Field( @@ -242,7 +259,8 @@ class DiagnosticsConfig(BaseModel): description="Init sim and save geometry diagnostics, skip FDTD run", ) verbose_interval: float = Field( - default=0, ge=0, + default=0, + ge=0, description="MEEP time units between progress prints (0=off)", ) @@ -302,7 +320,9 @@ class SimConfig(BaseModel): ) component_bbox: list[float] | None = Field( default=None, - description="Original component bbox [xmin, ymin, xmax, ymax] before port extension.", + description=( + "Original component bbox [xmin, ymin, xmax, ymax] before port extension." + ), ) layer_stack: list[LayerStackEntry] = Field(default_factory=list) dielectrics: list[dict[str, Any]] = Field(default_factory=list) @@ -322,9 +342,7 @@ class SimConfig(BaseModel): diagnostics: DiagnosticsConfig = Field(default_factory=DiagnosticsConfig) symmetries: list[SymmetryEntry] = Field(default_factory=list) split_chunks_evenly: bool = Field(default=False) - meep_np: int = Field( - default=1, ge=1, description="Recommended MPI process count" - ) + meep_np: int = Field(default=1, ge=1, description="Recommended MPI process count") def to_json(self, path: str | Path) -> Path: """Write config to JSON file. diff --git a/src/gsim/meep/script.py b/src/gsim/meep/script.py index 3d08b362..8f001f7b 100644 --- a/src/gsim/meep/script.py +++ b/src/gsim/meep/script.py @@ -238,6 +238,22 @@ def build_symmetries(config): } +def _make_time_cap(cap): + """Wrap a numeric time cap as a callable for until_after_sources lists. + + When until_after_sources receives a list, every element must be callable. + This wraps a float (time units) into a function that returns True once + ``cap`` time units have elapsed since the first call (i.e. after sources + turn off). + """ + _t0 = [None] + def _check(sim_obj): + if _t0[0] is None: + _t0[0] = sim_obj.meep_time() + return (sim_obj.meep_time() - _t0[0]) >= cap + return _check + + def resolve_decay_monitor_point(config): """Return center mp.Vector3 for decay monitoring. @@ -1082,6 +1098,16 @@ def _capture_frame(sim_obj): maximum_run_time=run_after, ), ) + elif stop_mode == "energy_decay": + dt = stopping.get("decay_dt", 50.0) + decay_by = stopping.get("decay_by", 1e-3) + logger.info( + "Running simulation (energy_decay mode: dt=%s, " + "decay_by=%s, cap=%.1f)...", + dt, decay_by, run_after, + ) + energy_fn = mp.stop_when_energy_decayed(dt=dt, decay_by=decay_by) + sim.run(*step_funcs, until_after_sources=[energy_fn, _make_time_cap(run_after)]) elif stop_mode == "decay": dt = stopping.get("decay_dt", 50.0) comp_name = stopping.get("decay_component", "Ey") @@ -1093,7 +1119,7 @@ def _capture_frame(sim_obj): # Decay condition + numeric time cap (list = OR logic, first wins) decay_fn = mp.stop_when_fields_decayed(dt, comp, monitor_pt, decay_by) - sim.run(*step_funcs, until_after_sources=[decay_fn, run_after]) + sim.run(*step_funcs, until_after_sources=[decay_fn, _make_time_cap(run_after)]) else: logger.info("Running simulation (until_after_sources=%.1f)...", run_after) sim.run(*step_funcs, until_after_sources=run_after) diff --git a/tests/meep/test_meep_models.py b/tests/meep/test_meep_models.py index a3ea3e98..f0237d56 100644 --- a/tests/meep/test_meep_models.py +++ b/tests/meep/test_meep_models.py @@ -659,6 +659,12 @@ def test_decay_mode(self): assert cfg.threshold == 1e-4 assert cfg.decay_dt == 25.0 + def test_energy_decay_mode(self): + cfg = StoppingConfig(mode="energy_decay", decay_dt=100, threshold=1e-4) + assert cfg.mode == "energy_decay" + assert cfg.decay_dt == 100 + assert cfg.threshold == 1e-4 + def test_invalid_mode(self): with pytest.raises(ValidationError): StoppingConfig(mode="invalid") # ty: ignore[invalid-argument-type] @@ -783,6 +789,13 @@ def test_script_has_component_map(self): assert "mp.Ez" in script assert "mp.Ey" in script + def test_script_has_stop_when_energy_decayed(self): + from gsim.meep.script import generate_meep_script + + script = generate_meep_script() + assert "stop_when_energy_decayed" in script + assert "energy_decay" in script + def test_script_valid_python(self): from gsim.meep.script import generate_meep_script diff --git a/tests/meep/test_simulation.py b/tests/meep/test_simulation.py index b84009cc..fa827aa3 100644 --- a/tests/meep/test_simulation.py +++ b/tests/meep/test_simulation.py @@ -82,8 +82,8 @@ def test_symmetries(self): class TestStoppingFields: def test_defaults(self): f = FDTD() - assert f.stopping == "dft_decay" - assert f.max_time == 200.0 + assert f.stopping == "energy_decay" + assert f.max_time == 2000.0 assert f.stopping_threshold == 1e-3 assert f.stopping_min_time == 100.0 assert f.stopping_component == "Ey" @@ -102,12 +102,23 @@ def test_decay_mode(self): assert f.stopping_dt == 25 def test_dft_decay_custom(self): - f = FDTD(max_time=300, stopping_threshold=1e-4, stopping_min_time=80) + f = FDTD( + stopping="dft_decay", + max_time=300, + stopping_threshold=1e-4, + stopping_min_time=80, + ) assert f.stopping == "dft_decay" assert f.max_time == 300 assert f.stopping_threshold == 1e-4 assert f.stopping_min_time == 80 + def test_energy_decay_mode(self): + f = FDTD(stopping="energy_decay", stopping_dt=100, stopping_threshold=1e-4) + assert f.stopping == "energy_decay" + assert f.stopping_dt == 100 + assert f.stopping_threshold == 1e-4 + def test_invalid_mode(self): with pytest.raises(ValidationError): FDTD(stopping="invalid") # ty: ignore[invalid-argument-type] @@ -117,8 +128,8 @@ class TestFDTD: def test_defaults(self): f = FDTD() assert f.resolution == 32 - assert f.stopping == "dft_decay" - assert f.max_time == 200.0 + assert f.stopping == "energy_decay" + assert f.max_time == 2000.0 assert f.subpixel is False assert f.simplify_tol == 0.0 @@ -211,12 +222,85 @@ def test_solver_stopping_replace(self): assert sim.solver.stopping == "decay" assert sim.solver.stopping_threshold == 1e-4 + def test_stop_when_energy_decayed(self): + f = FDTD() + result = f.stop_when_energy_decayed(dt=100, decay_by=1e-4) + assert result is f + assert f.stopping == "energy_decay" + assert f.stopping_dt == 100 + assert f.stopping_threshold == 1e-4 + + def test_stop_when_dft_decayed(self): + f = FDTD() + result = f.stop_when_dft_decayed(tol=1e-4, min_time=200) + assert result is f + assert f.stopping == "dft_decay" + assert f.stopping_threshold == 1e-4 + assert f.stopping_min_time == 200 + + def test_stop_when_fields_decayed(self): + f = FDTD() + result = f.stop_when_fields_decayed( + dt=25, component="Hz", decay_by=1e-4, monitor_port="o2" + ) + assert result is f + assert f.stopping == "decay" + assert f.stopping_dt == 25 + assert f.stopping_component == "Hz" + assert f.stopping_threshold == 1e-4 + assert f.stopping_monitor_port == "o2" + + def test_stop_after(self): + f = FDTD() + result = f.stop_after(time=500) + assert result is f + assert f.stopping == "fixed" + assert f.max_time == 500 + def test_geometry_component(self): sim = Simulation() sim.geometry.component = "placeholder" assert sim.geometry.component == "placeholder" +class TestCallableAPI: + def test_source_callable(self): + sim = Simulation() + result = sim.source(port="o1", wavelength=1.31, bandwidth=0.05) + assert result is sim.source + assert sim.source.port == "o1" + assert sim.source.wavelength == 1.31 + assert sim.source.bandwidth == 0.05 + + def test_domain_callable(self): + sim = Simulation() + sim.domain(pml=0.5, margin=0.2) + assert sim.domain.pml == 0.5 + assert sim.domain.margin == 0.2 + + def test_geometry_callable(self): + sim = Simulation() + sim.geometry(component="placeholder", z_crop="auto") + assert sim.geometry.component == "placeholder" + assert sim.geometry.z_crop == "auto" + + def test_solver_callable(self): + sim = Simulation() + sim.solver(resolution=64, simplify_tol=0.01) + assert sim.solver.resolution == 64 + assert sim.solver.simplify_tol == 0.01 + + def test_callable_validation(self): + sim = Simulation() + with pytest.raises(ValidationError): + sim.solver(resolution=1) # ge=4 + + def test_callable_invalid_field(self): + sim = Simulation() + with pytest.raises(ValidationError): + sim.source(nonexistent_field="value") + + class TestWholeObjectAssignment: def test_source(self): sim = Simulation() @@ -325,6 +409,14 @@ def test_stopping_dft_decay(self): assert cfg.threshold == 1e-4 assert cfg.dft_min_run_time == 80 + def test_stopping_energy_decay(self): + sim = Simulation() + sim.solver.stop_when_energy_decayed(dt=100, decay_by=1e-4) + cfg = sim._stopping_config() + assert cfg.mode == "energy_decay" + assert cfg.decay_dt == 100 + assert cfg.threshold == 1e-4 + def test_domain_translation(self): sim = Simulation() sim.domain = Domain(pml=0.5, margin=0.3, margin_z_above=1.0, margin_z_below=0.8) From c42d3dead04de4cf3dc751f6851015c9ccd832a7 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 11:51:47 +0100 Subject: [PATCH 02/10] fix: bump ty>=0.0.15 (allowed-unresolved-imports support) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d8359e0a..5776dfd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dev = [ "towncrier>=24.0.0", "vega-datasets>=0.9.0", "nbstripout>=0.8.1", - "ty>=0.0.13" + "ty>=0.0.15" ] docs = [ "ihp-gdsfactory>=0.1.4", From 15b43f0d61c75f6b94db0230a543cb77de6bacd7 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 12:02:47 +0100 Subject: [PATCH 03/10] docs: add notebook showing callable, attribute, and constructor API styles --- nbs/meep_api_styles.ipynb | 168 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 nbs/meep_api_styles.ipynb diff --git a/nbs/meep_api_styles.ipynb b/nbs/meep_api_styles.ipynb new file mode 100644 index 00000000..19535ab7 --- /dev/null +++ b/nbs/meep_api_styles.ipynb @@ -0,0 +1,168 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MEEP API Styles\n", + "\n", + "The `gsim.meep` API supports three equivalent styles for configuring simulations.\n", + "All three produce identical `Simulation` objects — pick whichever reads best for your use case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ubcpdk import PDK, cells\n", + "\n", + "PDK.activate()\n", + "c = cells.ebeam_y_1550()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Callable style (recommended)\n", + "\n", + "Updates fields in place — only the fields you pass change, others keep their current values. No extra imports needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Attribute style\n", + "\n", + "One field per line. Most explicit — good when you need to set fields conditionally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry.component = c\n", + "sim.geometry.z_crop = \"auto\"\n", + "\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "\n", + "sim.source.port = \"o1\"\n", + "sim.source.wavelength = 1.55\n", + "sim.source.bandwidth = 0.01\n", + "sim.source.num_freqs = 11\n", + "\n", + "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", + "\n", + "sim.domain.pml = 1.0\n", + "sim.domain.margin = 0.5\n", + "\n", + "sim.solver.resolution = 20\n", + "sim.solver.simplify_tol = 0.01\n", + "sim.solver.save_animation = True\n", + "sim.solver.verbose_interval = 5.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Constructor style\n", + "\n", + "Replaces the entire sub-object — fields you don't pass reset to defaults. Requires importing model classes, but useful when building configs programmatically or from saved presets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "from gsim.meep import Domain, FDTD, Geometry, ModeSource\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry = Geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source = ModeSource(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", + "sim.domain = Domain(pml=1.0, margin=0.5)\n", + "sim.solver = FDTD(\n", + " resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combining styles\n", + "\n", + "All three styles can be freely combined. For example, use callable for bulk setup, then attribute for conditional tweaks:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, simplify_tol=0.01)\n", + "\n", + "# Conditional tweaks with attribute style\n", + "debug = True\n", + "if debug:\n", + " sim.solver.save_animation = True\n", + " sim.solver.verbose_interval = 5.0" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 2479a5c1c1d59cb4748b0db78efe28da4e48ba83 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 12:04:58 +0100 Subject: [PATCH 04/10] docs: add meep_ring_coupler notebook to mkdocs nav --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index b3289192..b15f3fbd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - examples: - Simple Example: nbs/example.md - MEEP Y-Branch: nbs/meep_ybranch.md + - MEEP Ring Coupler: nbs/meep_ring_coupler.md - Palace CPW: nbs/palace_demo_cpw.md - Palace Microstrip: nbs/palace_demo_microstrip.md - api reference: From 5f2c7c4527f998e3facf3896345b42761355c9de Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 12:32:41 +0100 Subject: [PATCH 05/10] feat: default stopping to field_decay (stop_when_fields_decayed) with 95% threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename mode string "decay" → "field_decay" for clarity, switch default from energy_decay to field_decay to match MEEP tutorial conventions, and set decay_by default to 0.05 (95% decay). --- nbs/meep_ring_coupler.ipynb | 1 + nbs/meep_ybranch.ipynb | 1 + src/gsim/meep/models/api.py | 35 +++++++++++++++++++--------------- src/gsim/meep/models/config.py | 18 ++++++++--------- src/gsim/meep/script.py | 8 ++++---- tests/meep/test_meep_models.py | 8 ++++---- tests/meep/test_simulation.py | 28 +++++++++++++-------------- 7 files changed, 53 insertions(+), 46 deletions(-) diff --git a/nbs/meep_ring_coupler.ipynb b/nbs/meep_ring_coupler.ipynb index 8ee7e83e..023c6087 100644 --- a/nbs/meep_ring_coupler.ipynb +++ b/nbs/meep_ring_coupler.ipynb @@ -71,6 +71,7 @@ "sim.monitors = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", + "sim.solver.stop_when_fields_decayed(dt=50, decay_by=0.05)\n", "\n", "print(sim.validate_config())" ] diff --git a/nbs/meep_ybranch.ipynb b/nbs/meep_ybranch.ipynb index 49ce4731..b335e590 100644 --- a/nbs/meep_ybranch.ipynb +++ b/nbs/meep_ybranch.ipynb @@ -63,6 +63,7 @@ "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", + "sim.solver.stop_when_fields_decayed(dt=50, decay_by=0.05)\n", "\n", "print(sim.validate_config())" ] diff --git a/src/gsim/meep/models/api.py b/src/gsim/meep/models/api.py index 0a82e198..67fee5fa 100644 --- a/src/gsim/meep/models/api.py +++ b/src/gsim/meep/models/api.py @@ -155,19 +155,20 @@ class FDTD(BaseModel): resolution: int = Field(default=32, ge=4, description="Pixels per micrometer") # Stopping criteria (flat fields instead of variant classes) - stopping: Literal["fixed", "decay", "dft_decay", "energy_decay"] = Field( - default="energy_decay", + stopping: Literal["fixed", "field_decay", "dft_decay", "energy_decay"] = Field( + default="field_decay", description=( - "Stopping mode: 'energy_decay' (recommended) monitors total field " - "energy decay; 'dft_decay' waits for DFT convergence; 'decay' " - "monitors a field component at a point; 'fixed' runs for max_time." + "Stopping mode: 'field_decay' (recommended, matches MEEP tutorials) " + "monitors a field component at a point; 'energy_decay' monitors " + "total field energy; 'dft_decay' waits for DFT convergence; " + "'fixed' runs for max_time." ), ) max_time: float = Field( default=2000.0, gt=0, description="Max run time after sources (um/c)" ) stopping_threshold: float = Field( - default=1e-3, gt=0, lt=1, description="Decay/convergence threshold" + default=0.05, gt=0, lt=1, description="Decay/convergence threshold" ) stopping_min_time: float = Field( default=100.0, @@ -178,15 +179,15 @@ class FDTD(BaseModel): ), ) stopping_component: str = Field( - default="Ey", description="Field component for decay mode" + default="Ey", description="Field component for field_decay mode" ) stopping_dt: float = Field( default=50.0, gt=0, - description="Decay measurement window for decay/energy_decay modes", + description="Decay measurement window for field_decay/energy_decay modes", ) stopping_monitor_port: str | None = Field( - default=None, description="Port to monitor for decay mode" + default=None, description="Port to monitor for field_decay mode" ) subpixel: bool = Field(default=False, description="Toggle subpixel averaging") @@ -205,7 +206,7 @@ class FDTD(BaseModel): # -- Convenience methods for stopping configuration -- def stop_when_energy_decayed( - self, dt: float = 50.0, decay_by: float = 1e-3 + self, dt: float = 50.0, decay_by: float = 0.05 ) -> FDTD: """Stop when total field energy in the cell decays (recommended). @@ -215,7 +216,7 @@ def stop_when_energy_decayed( Args: dt: Time window between energy checks (MEEP time units). - decay_by: Fractional energy decay threshold (e.g. 1e-3 = 0.1%). + decay_by: Fractional energy decay threshold (e.g. 0.05 = 5%). Returns: self (for fluent chaining). @@ -244,21 +245,25 @@ def stop_when_fields_decayed( self, dt: float = 50.0, component: str = "Ey", - decay_by: float = 1e-3, + decay_by: float = 0.05, monitor_port: str | None = None, ) -> FDTD: - """Stop when a field component decays at a point. + """Stop when a field component decays at a point (recommended). + + Matches the standard MEEP tutorial stopping condition. Monitors + |component|² at a point and stops when it decays by ``decay_by`` + from its peak value. Args: dt: Decay measurement time window. component: Field component name (e.g. "Ey", "Hz"). - decay_by: Fractional decay threshold. + decay_by: Fractional decay threshold (e.g. 0.05 = 5%). monitor_port: Port to monitor (None = first non-source port). Returns: self (for fluent chaining). """ - self.stopping = "decay" + self.stopping = "field_decay" self.stopping_dt = dt self.stopping_component = component self.stopping_threshold = decay_by diff --git a/src/gsim/meep/models/config.py b/src/gsim/meep/models/config.py index 973191f8..dccf50ef 100644 --- a/src/gsim/meep/models/config.py +++ b/src/gsim/meep/models/config.py @@ -64,9 +64,13 @@ class DomainConfig(BaseModel): class StoppingConfig(BaseModel): """Controls when the MEEP simulation stops. - ``energy_decay`` mode (recommended) monitors total electromagnetic - energy in the cell and stops when it decays by ``threshold`` from - its peak. Robust against false early convergence. + ``field_decay`` mode (recommended, matches MEEP tutorials) monitors + a field component at a point and stops when |component|² decays by + ``threshold`` from its peak, with ``max_time`` as a numeric time cap + (whichever fires first). + + ``energy_decay`` mode monitors total electromagnetic energy in the + cell and stops when it decays by ``threshold`` from its peak. ``dft_decay`` mode monitors convergence of all DFT monitors and stops when they stabilize. ``dft_min_run_time`` is an *absolute* @@ -74,16 +78,12 @@ class StoppingConfig(BaseModel): off at ~t=78, a min_run_time=100 starts checking at t=100, only ~22 time units after the source ends. - ``decay`` mode monitors field decay at a point and stops when the - fields have decayed by ``threshold``, with ``max_time`` as - a numeric time cap (whichever fires first). - ``fixed`` mode runs for a fixed time after sources turn off. """ model_config = ConfigDict(validate_assignment=True) - mode: Literal["fixed", "decay", "dft_decay", "energy_decay"] = Field( + mode: Literal["fixed", "field_decay", "dft_decay", "energy_decay"] = Field( default="fixed" ) max_time: float = Field( @@ -91,7 +91,7 @@ class StoppingConfig(BaseModel): ) decay_dt: float = Field(default=50.0, gt=0) decay_component: str = Field(default="Ey") - threshold: float = Field(default=1e-3, gt=0, lt=1, serialization_alias="decay_by") + threshold: float = Field(default=0.05, gt=0, lt=1, serialization_alias="decay_by") decay_monitor_port: str | None = Field(default=None) dft_min_run_time: float = Field( default=100, diff --git a/src/gsim/meep/script.py b/src/gsim/meep/script.py index 8f001f7b..a8cb90dc 100644 --- a/src/gsim/meep/script.py +++ b/src/gsim/meep/script.py @@ -1100,7 +1100,7 @@ def _capture_frame(sim_obj): ) elif stop_mode == "energy_decay": dt = stopping.get("decay_dt", 50.0) - decay_by = stopping.get("decay_by", 1e-3) + decay_by = stopping.get("decay_by", 0.05) logger.info( "Running simulation (energy_decay mode: dt=%s, " "decay_by=%s, cap=%.1f)...", @@ -1108,13 +1108,13 @@ def _capture_frame(sim_obj): ) energy_fn = mp.stop_when_energy_decayed(dt=dt, decay_by=decay_by) sim.run(*step_funcs, until_after_sources=[energy_fn, _make_time_cap(run_after)]) - elif stop_mode == "decay": + elif stop_mode == "field_decay": dt = stopping.get("decay_dt", 50.0) comp_name = stopping.get("decay_component", "Ey") comp = _COMPONENT_MAP.get(comp_name, mp.Ey) - decay_by = stopping.get("decay_by", 1e-3) + decay_by = stopping.get("decay_by", 0.05) monitor_pt = resolve_decay_monitor_point(config) - logger.info("Running simulation (decay mode: component=%s, dt=%s, " + logger.info("Running simulation (field_decay mode: component=%s, dt=%s, " "decay_by=%s, cap=%.1f)...", comp_name, dt, decay_by, run_after) # Decay condition + numeric time cap (list = OR logic, first wins) diff --git a/tests/meep/test_meep_models.py b/tests/meep/test_meep_models.py index f0237d56..9b15fcc8 100644 --- a/tests/meep/test_meep_models.py +++ b/tests/meep/test_meep_models.py @@ -650,12 +650,12 @@ def test_default_is_fixed(self): assert cfg.max_time == 100.0 assert cfg.decay_dt == 50.0 assert cfg.decay_component == "Ey" - assert cfg.threshold == 1e-3 + assert cfg.threshold == 0.05 assert cfg.decay_monitor_port is None - def test_decay_mode(self): - cfg = StoppingConfig(mode="decay", threshold=1e-4, decay_dt=25.0) - assert cfg.mode == "decay" + def test_field_decay_mode(self): + cfg = StoppingConfig(mode="field_decay", threshold=1e-4, decay_dt=25.0) + assert cfg.mode == "field_decay" assert cfg.threshold == 1e-4 assert cfg.decay_dt == 25.0 diff --git a/tests/meep/test_simulation.py b/tests/meep/test_simulation.py index fa827aa3..24f06f2e 100644 --- a/tests/meep/test_simulation.py +++ b/tests/meep/test_simulation.py @@ -82,9 +82,9 @@ def test_symmetries(self): class TestStoppingFields: def test_defaults(self): f = FDTD() - assert f.stopping == "energy_decay" + assert f.stopping == "field_decay" assert f.max_time == 2000.0 - assert f.stopping_threshold == 1e-3 + assert f.stopping_threshold == 0.05 assert f.stopping_min_time == 100.0 assert f.stopping_component == "Ey" assert f.stopping_dt == 50.0 @@ -95,9 +95,9 @@ def test_fixed_mode(self): assert f.stopping == "fixed" assert f.max_time == 100 - def test_decay_mode(self): - f = FDTD(stopping="decay", stopping_threshold=1e-4, stopping_dt=25) - assert f.stopping == "decay" + def test_field_decay_mode(self): + f = FDTD(stopping="field_decay", stopping_threshold=1e-4, stopping_dt=25) + assert f.stopping == "field_decay" assert f.stopping_threshold == 1e-4 assert f.stopping_dt == 25 @@ -128,14 +128,14 @@ class TestFDTD: def test_defaults(self): f = FDTD() assert f.resolution == 32 - assert f.stopping == "energy_decay" + assert f.stopping == "field_decay" assert f.max_time == 2000.0 assert f.subpixel is False assert f.simplify_tol == 0.0 def test_with_custom_stopping(self): - f = FDTD(stopping="decay", stopping_threshold=1e-4) - assert f.stopping == "decay" + f = FDTD(stopping="field_decay", stopping_threshold=1e-4) + assert f.stopping == "field_decay" assert f.stopping_threshold == 1e-4 def test_resolution_custom(self): @@ -217,9 +217,9 @@ def test_solver_resolution(self): def test_solver_stopping_replace(self): sim = Simulation() - sim.solver.stopping = "decay" + sim.solver.stopping = "field_decay" sim.solver.stopping_threshold = 1e-4 - assert sim.solver.stopping == "decay" + assert sim.solver.stopping == "field_decay" assert sim.solver.stopping_threshold == 1e-4 def test_stop_when_energy_decayed(self): @@ -244,7 +244,7 @@ def test_stop_when_fields_decayed(self): dt=25, component="Hz", decay_by=1e-4, monitor_port="o2" ) assert result is f - assert f.stopping == "decay" + assert f.stopping == "field_decay" assert f.stopping_dt == 25 assert f.stopping_component == "Hz" assert f.stopping_threshold == 1e-4 @@ -385,14 +385,14 @@ def test_stopping_fixed(self): assert cfg.mode == "fixed" assert cfg.max_time == 200 - def test_stopping_decay(self): + def test_stopping_field_decay(self): sim = Simulation() - sim.solver.stopping = "decay" + sim.solver.stopping = "field_decay" sim.solver.stopping_threshold = 1e-4 sim.solver.stopping_dt = 25 sim.solver.stopping_monitor_port = "o2" cfg = sim._stopping_config() - assert cfg.mode == "decay" + assert cfg.mode == "field_decay" assert cfg.threshold == 1e-4 assert cfg.decay_dt == 25 assert cfg.decay_monitor_port == "o2" From f76bcc4b4ffcd6e6d51954d07fd8ad461d551622 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 13:38:13 +0100 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20rename=20stop=5Fafter=20=E2=86=92?= =?UTF-8?q?=20stop=5Fafter=5Fsources,=20add=20wall-clock=20time=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stop_after(time) was ambiguous — unclear it meant "after sources die." Renamed to stop_after_sources(time) for clarity. Added wall_time_max field + stop_after_walltime(seconds) method as an orthogonal wall-clock safety net. The runner appends _make_wall_time_cap() to until_after_sources in all 4 stopping mode branches when enabled. Logs a warning when the wall-clock limit likely triggered. --- nbs/meep_ring_coupler.ipynb | 9 +- nbs/meep_ybranch.ipynb | 2 +- src/gsim/meep/meep-overview.md | 8 +- src/gsim/meep/models/api.py | 26 ++- src/gsim/meep/models/config.py | 102 ++++------ src/gsim/meep/script.py | 154 ++++++++------ src/gsim/meep/simulation.py | 1 + tests/meep/test_meep_models.py | 355 ++++++++++++++++++++++++++++----- tests/meep/test_simulation.py | 22 +- 9 files changed, 491 insertions(+), 188 deletions(-) diff --git a/nbs/meep_ring_coupler.ipynb b/nbs/meep_ring_coupler.ipynb index 023c6087..f5b75b28 100644 --- a/nbs/meep_ring_coupler.ipynb +++ b/nbs/meep_ring_coupler.ipynb @@ -35,13 +35,6 @@ "PDK.activate()\n", "\n", "c = cells.coupler_ring()\n", - "# ebeam_DC_te895\n", - "# ebeam_MMI_2x2_5050_te1310\n", - "# ebeam_YBranch_te1310\n", - "# ebeam_adiabatic_te1550\n", - "# ebeam_crossing4\n", - "# ebeam_y_1310\n", - "# ebeam_y_adiabatic\n", "\n", "c" ] @@ -71,7 +64,7 @@ "sim.monitors = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", - "sim.solver.stop_when_fields_decayed(dt=50, decay_by=0.05)\n", + "sim.solver.stop_after_sources(time=160)\n", "\n", "print(sim.validate_config())" ] diff --git a/nbs/meep_ybranch.ipynb b/nbs/meep_ybranch.ipynb index b335e590..ad7269ea 100644 --- a/nbs/meep_ybranch.ipynb +++ b/nbs/meep_ybranch.ipynb @@ -63,7 +63,7 @@ "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", - "sim.solver.stop_when_fields_decayed(dt=50, decay_by=0.05)\n", + "sim.solver.stop_after_sources(time=80)\n", "\n", "print(sim.validate_config())" ] diff --git a/src/gsim/meep/meep-overview.md b/src/gsim/meep/meep-overview.md index 01d513ba..3f3f3172 100644 --- a/src/gsim/meep/meep-overview.md +++ b/src/gsim/meep/meep-overview.md @@ -34,7 +34,7 @@ result = sim.run() - **6 typed physics objects** -- `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD` + `monitors: list[str]` -- assigned to a `Simulation` container. No ordering dependencies. - **Callable, attribute, or constructor style** -- `sim.source(port="o1")` (callable), `sim.source.port = "o1"` (attribute), or `sim.source = ModeSource(port="o1")` (constructor). All work via Pydantic `validate_assignment=True`. - **Float shorthand for materials** -- `{"si": 3.47}` auto-normalizes to `Material(n=3.47)` via validator. -- **Flat stopping fields + method API** -- `stopping` mode string (`energy_decay`/`dft_decay`/`decay`/`fixed`) + flat fields on `FDTD`, plus convenience methods (`stop_when_energy_decayed()`, `stop_when_dft_decayed()`, `stop_when_fields_decayed()`, `stop_after()`). `StoppingConfig.mode` now includes `energy_decay`. +- **5 stopping methods** -- Flat `stopping` mode string on `FDTD` (`field_decay`/`energy_decay`/`dft_decay`/`fixed`) plus convenience methods: `stop_when_fields_decayed()`, `stop_when_energy_decayed()`, `stop_when_dft_decayed()`, `stop_after_sources()`. A 5th method `stop_after_walltime(seconds)` sets an orthogonal wall-clock safety net (field `wall_time_max`) that can be combined with any mode. - **Source defines spectral window** -- `WavelengthConfig` derived from `ModeSource.wavelength/bandwidth/num_freqs`. Monitors are just port name strings. - **JSON contract** -- `write_config()` translates new API -> existing `SimConfig` -> JSON. `StoppingConfig.mode` extended with `"energy_decay"` (runner updated to handle it). @@ -50,7 +50,7 @@ result = sim.run() | `models/api.py` | Declarative models: `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD`, `Symmetry`. All except `Material`/`Symmetry` have `__call__(**kwargs)` for fluent updates | | `simulation.py` | `Simulation` container -- `write_config()`, `run()`, `validate_config()`, `plot_2d()`/`plot_3d()`, `estimate_meep_np()` | | `viz.py` | Standalone viz helpers -- `build_geometry_model()`, `plot_2d()`, `plot_3d()` (calls `common/viz`) | -| `models/config.py` | `SimConfig` + all sub-configs (JSON serialization layer) | +| `models/config.py` | `SimConfig` + all sub-configs (JSON serialization layer, strict — no defaults on fields that `build_config` always populates) | | `models/results.py` | `SParameterResult` -- CSV + debug JSON + diagnostic PNGs | | `ports.py` | `extract_port_info()` -- port center/direction/normal from gdsfactory | | `materials.py` | `resolve_materials()` -- material names -> (n, k) via common DB | @@ -93,8 +93,10 @@ Simulation.plot_2d() - **Z-crop** -- Auto-crops stack around core layer. Full UBC stack is 15um; cropped to ~1.5um (massive compute savings). - **Port extension into PML** -- `gf.components.extend_ports()` at `write_config()` time. Original bbox stored in `SimConfig.component_bbox` for correct cell sizing. - **Symmetries disabled for S-params** -- `add_mode_monitor` uses `use_symmetry=false` internally; `get_eigenmode_coefficients` doesn't apply `S.multiplicity()`. Source port coefficients underestimated ~2x. gplugins also never uses `mp.Mirror`. -- **`energy_decay` default stopping** -- `dft_decay` suffers from false early convergence (DFTs can converge on near-zero fields). `energy_decay` monitors total field energy and waits for it to peak then decay — more robust for directional couplers and similar devices. Runner uses `mp.stop_when_energy_decayed(dt, decay_by)` with a `_make_time_cap(max_time)` as a secondary callable in the `until_after_sources` list (OR logic). The `decay` mode also uses `_make_time_cap` instead of passing a raw float to `until_after_sources`. +- **`field_decay` default stopping** -- Monitors |component|² at a point and stops when it decays by `decay_by` from peak. Runner uses `mp.stop_when_fields_decayed()` + `_make_time_cap(max_time)` in an `until_after_sources` list (OR logic — first condition wins). `energy_decay` is similar but monitors total cell energy (more robust for ring couplers where DFTs can falsely converge). `stop_after_sources(time)` runs for a fixed sim-time after sources turn off. +- **Wall-clock safety net** -- `stop_after_walltime(seconds)` sets `wall_time_max` on `FDTD`, an orthogonal real-time cap. Runner's `_make_wall_time_cap()` uses `time.time()` deadline and is appended to the `until_after_sources` list in all 4 mode branches when `wall_time_max > 0`. A warning is logged when the wall-clock limit likely triggered (elapsed >= 95% of limit). - **`dft_min_run_time` is absolute sim time** -- Not time-after-sources. With a broadband source turning off at ~t=78 and min_run_time=100, DFT checking starts at t=100 (only ~22 units after source ends). Must exceed `device_length * n_group`. +- **API models are the single source of truth for defaults** -- Config models (`models/config.py`) and the cloud runner (`script.py`) have no default values for fields that `build_config()` always populates. `model_dump(by_alias=True)` serializes all fields explicitly, so config defaults and script.py `.get()` fallbacks were dead code that could silently drift. Config sub-models are strict (missing fields = `ValidationError`); `script.py` uses direct `dict["key"]` access. - **Stack resolved lazily** -- `_ensure_stack()` falls back to `get_stack()` (active PDK defaults) when no explicit stack kwargs are set. Same path for `write_config()` and viz. --- diff --git a/src/gsim/meep/models/api.py b/src/gsim/meep/models/api.py index 67fee5fa..7a0c79db 100644 --- a/src/gsim/meep/models/api.py +++ b/src/gsim/meep/models/api.py @@ -189,6 +189,12 @@ class FDTD(BaseModel): stopping_monitor_port: str | None = Field( default=None, description="Port to monitor for field_decay mode" ) + wall_time_max: float = Field( + default=0.0, + ge=0, + description="Wall-clock time limit in seconds (0=unlimited). " + "Orthogonal safety net — stops the sim if real time exceeds this.", + ) subpixel: bool = Field(default=False, description="Toggle subpixel averaging") subpixel_maxeval: int = Field( @@ -270,8 +276,8 @@ def stop_when_fields_decayed( self.stopping_monitor_port = monitor_port return self - def stop_after(self, time: float) -> FDTD: - """Run for a fixed time after sources turn off. + def stop_after_sources(self, time: float) -> FDTD: + """Run for a fixed sim-time after sources turn off. Args: time: Run time after sources in MEEP time units (um/c). @@ -283,6 +289,22 @@ def stop_after(self, time: float) -> FDTD: self.max_time = time return self + def stop_after_walltime(self, seconds: float) -> FDTD: + """Set a wall-clock time limit (safety net). + + This is orthogonal to the sim-time stopping mode — it caps + how long the FDTD run is allowed to take in real (wall) seconds. + Combine with any other stopping method. + + Args: + seconds: Maximum wall-clock seconds for the FDTD run. + + Returns: + self (for fluent chaining). + """ + self.wall_time_max = seconds + return self + def __call__(self, **kwargs: Any) -> FDTD: """Update fields in place. Returns self for chaining.""" for k, v in kwargs.items(): diff --git a/src/gsim/meep/models/config.py b/src/gsim/meep/models/config.py index dccf50ef..40825311 100644 --- a/src/gsim/meep/models/config.py +++ b/src/gsim/meep/models/config.py @@ -19,7 +19,7 @@ class SymmetryEntry(BaseModel): model_config = ConfigDict(validate_assignment=True) direction: Literal["X", "Y", "Z"] - phase: Literal[1, -1] = Field(default=1) + phase: Literal[1, -1] class DomainConfig(BaseModel): @@ -38,23 +38,21 @@ class DomainConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - dpml: float = Field(default=1.0, ge=0, description="PML thickness in um") + dpml: float = Field(ge=0, description="PML thickness in um") margin_xy: float = Field( - default=0.5, ge=0, description="XY margin between geometry and PML in um" + ge=0, description="XY margin between geometry and PML in um" ) margin_z_above: float = Field( - default=0.5, ge=0, description="Z margin above core kept by set_z_crop in um" + ge=0, description="Z margin above core kept by set_z_crop in um" ) margin_z_below: float = Field( - default=0.5, ge=0, description="Z margin below core kept by set_z_crop in um" + ge=0, description="Z margin below core kept by set_z_crop in um" ) port_margin: float = Field( - default=0.5, ge=0, description="Margin on each side of port width for monitors (um)", ) extend_ports: float = Field( - default=0.0, ge=0, description="Length to extend waveguide ports into PML in um. " "0 = auto (margin_xy + dpml).", @@ -83,23 +81,24 @@ class StoppingConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - mode: Literal["fixed", "field_decay", "dft_decay", "energy_decay"] = Field( - default="fixed" - ) - max_time: float = Field( - default=100.0, gt=0, serialization_alias="run_after_sources" - ) - decay_dt: float = Field(default=50.0, gt=0) - decay_component: str = Field(default="Ey") - threshold: float = Field(default=0.05, gt=0, lt=1, serialization_alias="decay_by") + mode: Literal["fixed", "field_decay", "dft_decay", "energy_decay"] + max_time: float = Field(gt=0, serialization_alias="run_after_sources") + decay_dt: float = Field(gt=0) + decay_component: str + threshold: float = Field(gt=0, lt=1, serialization_alias="decay_by") decay_monitor_port: str | None = Field(default=None) dft_min_run_time: float = Field( - default=100, ge=0, description="Minimum absolute sim time for dft_decay mode (not " "time-after-sources). Must exceed pulse transit time through the " "device to avoid false convergence on near-zero fields.", ) + wall_time_max: float = Field( + default=0.0, + ge=0, + description="Wall-clock time limit in seconds (0=unlimited). " + "Orthogonal safety net for all stopping modes.", + ) class SourceConfig(BaseModel): @@ -162,11 +161,9 @@ class WavelengthConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - wavelength: float = Field(default=1.55, gt=0, description="Center wavelength in um") - bandwidth: float = Field( - default=0.1, ge=0, description="Wavelength bandwidth in um" - ) - num_freqs: int = Field(default=11, ge=1, description="Number of frequency points") + wavelength: float = Field(gt=0, description="Center wavelength in um") + bandwidth: float = Field(ge=0, description="Wavelength bandwidth in um") + num_freqs: int = Field(ge=1, description="Number of frequency points") @computed_field # type: ignore[prop-decorator] @property @@ -190,9 +187,7 @@ class ResolutionConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - pixels_per_um: int = Field( - default=32, ge=4, description="Grid pixels per micrometer" - ) + pixels_per_um: int = Field(ge=4, description="Grid pixels per micrometer") @classmethod def coarse(cls) -> ResolutionConfig: @@ -222,15 +217,13 @@ class AccuracyConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - eps_averaging: bool = Field(default=False, description="Toggle subpixel averaging") + eps_averaging: bool = Field(description="Toggle subpixel averaging") subpixel_maxeval: int = Field( - default=0, ge=0, description="Cap on integration evaluations (0=unlimited)" - ) - subpixel_tol: float = Field( - default=1e-4, gt=0, description="Subpixel integration tolerance" + ge=0, description="Cap on integration evaluations (0=unlimited)" ) + subpixel_tol: float = Field(gt=0, description="Subpixel integration tolerance") simplify_tol: float = Field( - default=0.0, ge=0, description="Shapely simplification tolerance in um (0=off)" + ge=0, description="Shapely simplification tolerance in um (0=off)" ) @@ -239,27 +232,20 @@ class DiagnosticsConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - save_geometry: bool = Field( - default=True, description="Pre-run geometry cross-section plots" - ) - save_fields: bool = Field(default=True, description="Post-run field snapshot") - save_epsilon_raw: bool = Field( - default=False, description="Raw epsilon .npy (advanced)" - ) + save_geometry: bool = Field(description="Pre-run geometry cross-section plots") + save_fields: bool = Field(description="Post-run field snapshot") + save_epsilon_raw: bool = Field(description="Raw epsilon .npy (advanced)") save_animation: bool = Field( - default=False, description="Field animation MP4 (heavy, needs ffmpeg)" + description="Field animation MP4 (heavy, needs ffmpeg)" ) animation_interval: float = Field( - default=0.5, gt=0, description="MEEP time units between animation frames", ) preview_only: bool = Field( - default=False, description="Init sim and save geometry diagnostics, skip FDTD run", ) verbose_interval: float = Field( - default=0, ge=0, description="MEEP time units between progress prints (0=off)", ) @@ -315,32 +301,28 @@ class SimConfig(BaseModel): model_config = ConfigDict(validate_assignment=True) - gds_filename: str = Field( - default="layout.gds", description="GDS file with 2D layout" - ) + gds_filename: str = Field(description="GDS file with 2D layout") component_bbox: list[float] | None = Field( default=None, description=( "Original component bbox [xmin, ymin, xmax, ymax] before port extension." ), ) - layer_stack: list[LayerStackEntry] = Field(default_factory=list) - dielectrics: list[dict[str, Any]] = Field(default_factory=list) - ports: list[PortData] = Field(default_factory=list) - materials: dict[str, MaterialData] = Field(default_factory=dict) - wavelength: WavelengthConfig = Field( - default_factory=WavelengthConfig, serialization_alias="fdtd" - ) - source: SourceConfig = Field(default_factory=SourceConfig) - stopping: StoppingConfig = Field(default_factory=StoppingConfig) - resolution: ResolutionConfig = Field(default_factory=ResolutionConfig) - domain: DomainConfig = Field(default_factory=DomainConfig) - accuracy: AccuracyConfig = Field(default_factory=AccuracyConfig) + layer_stack: list[LayerStackEntry] + dielectrics: list[dict[str, Any]] + ports: list[PortData] + materials: dict[str, MaterialData] + wavelength: WavelengthConfig = Field(serialization_alias="fdtd") + source: SourceConfig + stopping: StoppingConfig + resolution: ResolutionConfig + domain: DomainConfig + accuracy: AccuracyConfig verbose_interval: float = Field( - default=0, ge=0, description="MEEP time units between progress prints (0=off)" + ge=0, description="MEEP time units between progress prints (0=off)" ) - diagnostics: DiagnosticsConfig = Field(default_factory=DiagnosticsConfig) - symmetries: list[SymmetryEntry] = Field(default_factory=list) + diagnostics: DiagnosticsConfig + symmetries: list[SymmetryEntry] split_chunks_evenly: bool = Field(default=False) meep_np: int = Field(default=1, ge=1, description="Recommended MPI process count") diff --git a/src/gsim/meep/script.py b/src/gsim/meep/script.py index a8cb90dc..31c14dc0 100644 --- a/src/gsim/meep/script.py +++ b/src/gsim/meep/script.py @@ -59,7 +59,7 @@ def build_materials(config): materials = {} for name, props in config["materials"].items(): n = props["refractive_index"] - k = props.get("extinction_coeff", 0.0) + k = props["extinction_coeff"] if k > 0: materials[name] = mp.Medium(index=n, D_conductivity=2 * cmath.pi * k / n) else: @@ -202,7 +202,7 @@ def build_background_slabs(config, materials): geometry list so that patterned prisms (added later) take precedence. """ slabs = [] - for diel in sorted(config.get("dielectrics", []), key=lambda d: d["zmin"]): + for diel in sorted(config["dielectrics"], key=lambda d: d["zmin"]): mat = materials.get(diel["material"]) if mat is None: continue @@ -224,9 +224,9 @@ def build_symmetries(config): """Build MEEP symmetry objects from config.""" direction_map = {"X": mp.X, "Y": mp.Y, "Z": mp.Z} symmetries = [] - for sym in config.get("symmetries", []): + for sym in config["symmetries"]: symmetries.append( - mp.Mirror(direction_map[sym["direction"]], phase=sym.get("phase", 1)) + mp.Mirror(direction_map[sym["direction"]], phase=sym["phase"]) ) return symmetries @@ -254,6 +254,21 @@ def _check(sim_obj): return _check +def _make_wall_time_cap(wall_seconds): + """Wrap a wall-clock time limit as a callable for until_after_sources lists. + + Returns True once ``wall_seconds`` of real (wall) time have elapsed + since the first call. This provides a safety net orthogonal to any + sim-time stopping condition. + """ + _deadline = [None] + def _check(sim_obj): + if _deadline[0] is None: + _deadline[0] = time.time() + wall_seconds + return time.time() >= _deadline[0] + return _check + + def resolve_decay_monitor_point(config): """Return center mp.Vector3 for decay monitoring. @@ -261,9 +276,9 @@ def resolve_decay_monitor_point(config): otherwise picks the first non-source port. Falls back to the first port if all are sources. """ - stopping = config.get("stopping", {}) + stopping = config["stopping"] target_name = stopping.get("decay_monitor_port") - ports = config.get("ports", []) + ports = config["ports"] if target_name: for p in ports: @@ -272,7 +287,7 @@ def resolve_decay_monitor_point(config): # Fallback: first non-source port, or first port for p in ports: - if not p.get("is_source", False): + if not p["is_source"]: return mp.Vector3(*p["center"]) if ports: return mp.Vector3(*ports[0]["center"]) @@ -288,11 +303,11 @@ def build_geometry(config, materials): 2. Extrude to 3D as mp.Prism with correct z-range and material 3. Handle polygon holes via Delaunay triangulation """ - gds_filename = config.get("gds_filename", "layout.gds") + gds_filename = config["gds_filename"] component = load_gds_component(gds_filename) - accuracy = config.get("accuracy", {}) - simplify_tol = accuracy.get("simplify_tol", 0.0) + accuracy = config["accuracy"] + simplify_tol = accuracy["simplify_tol"] geometry = [] total_vertices = 0 @@ -304,7 +319,7 @@ def build_geometry(config, materials): zmax = layer_entry["zmax"] height = zmax - zmin gds_layer = layer_entry["gds_layer"] - sidewall_angle_deg = layer_entry.get("sidewall_angle", 0.0) + sidewall_angle_deg = layer_entry["sidewall_angle"] sw_rad = math.radians(sidewall_angle_deg) if sidewall_angle_deg else 0 if height <= 0: @@ -366,7 +381,7 @@ def build_sources(config): df = fdtd["df"] fwidth = config["source"]["fwidth"] z_span = get_port_z_span(config) - port_margin = config.get("domain", {}).get("port_margin", 0.5) + port_margin = config["domain"]["port_margin"] sources = [] for port in config["ports"]: @@ -412,7 +427,7 @@ def build_monitors(config, sim): df = fdtd["df"] nfreq = fdtd["num_freqs"] z_span = get_port_z_span(config) - port_margin = config.get("domain", {}).get("port_margin", 0.5) + port_margin = config["domain"]["port_margin"] monitors = {} for port in config["ports"]: @@ -618,9 +633,9 @@ def save_debug_log(config, s_params, debug_data, wall_seconds=0.0, if not mp.am_master(): return fdtd = config["fdtd"] - domain = config.get("domain", {}) - resolution = config.get("resolution", {}).get("pixels_per_um", 0) - stopping = config.get("stopping", {}) + domain = config["domain"] + resolution = config["resolution"]["pixels_per_um"] + stopping = config["stopping"] meep_time = 0.0 timesteps = 0 @@ -638,7 +653,7 @@ def save_debug_log(config, s_params, debug_data, wall_seconds=0.0, "meep_time": meep_time, "timesteps": timesteps, "wall_seconds": wall_seconds, - "stopping_mode": stopping.get("mode", "fixed"), + "stopping_mode": stopping["mode"], }, "eigenmode_info": debug_data.get("eigenmode_info", {}), "incident_coefficients": debug_data.get("incident_coefficients", {}), @@ -664,8 +679,8 @@ def save_geometry_diagnostics(sim, config, cell_center): # plot2D is collective — all ranks call it, only master saves pass - domain = config.get("domain", {}) - dpml = domain.get("dpml", 1.0) + domain = config["domain"] + dpml = domain["dpml"] z_min = min(l["zmin"] for l in config["layer_stack"]) z_max = max(l["zmax"] for l in config["layer_stack"]) z_core = (z_min + z_max) / 2 @@ -945,7 +960,7 @@ def main(): # Compute simulation cell from component bounds + layer z-range # Use original component bbox if available (port extension changes GDS bbox) - component_bbox = config.get("component_bbox") + component_bbox = config["component_bbox"] if component_bbox is not None: bbox_left, bbox_bottom, bbox_right, bbox_top = component_bbox else: @@ -956,9 +971,9 @@ def main(): z_min = min(l["zmin"] for l in config["layer_stack"]) z_max = max(l["zmax"] for l in config["layer_stack"]) - domain = config.get("domain", {}) - dpml = domain.get("dpml", 1.0) - margin_xy = domain.get("margin_xy", 0.5) + domain = config["domain"] + dpml = domain["dpml"] + margin_xy = domain["margin_xy"] # XY: margin_xy is gap between geometry bbox and PML cell_x = (bbox_right - bbox_left) + 2 * (margin_xy + dpml) @@ -976,12 +991,12 @@ def main(): logger.info("PML: %.2f um, margin_xy: %.2f", dpml, margin_xy) logger.info("Resolution: %s pixels/um", resolution) - accuracy = config.get("accuracy", {}) - diagnostics = config.get("diagnostics", {}) - preview_only = diagnostics.get("preview_only", False) + accuracy = config["accuracy"] + diagnostics = config["diagnostics"] + preview_only = diagnostics["preview_only"] # In preview mode, skip expensive subpixel averaging - eps_avg = False if preview_only else accuracy.get("eps_averaging", True) + eps_avg = False if preview_only else accuracy["eps_averaging"] # Symmetries are NOT used for S-parameter extraction runs. # MEEP's get_eigenmode_coefficients with add_mode_monitor produces @@ -990,7 +1005,7 @@ def main(): # consistent with gplugins which also never uses mp.Mirror for # S-parameter extraction. Symmetries are only applied in preview-only # mode (geometry validation, no FDTD/S-params). - cfg_symmetries = config.get("symmetries", []) + cfg_symmetries = config["symmetries"] if cfg_symmetries and not preview_only: logger.info("Symmetries present in config but IGNORED for S-parameter " "extraction (causes incorrect eigenmode normalization). " @@ -1005,21 +1020,21 @@ def main(): resolution=resolution, boundary_layers=[mp.PML(dpml)], symmetries=use_symmetries, - split_chunks_evenly=config.get("split_chunks_evenly", False), + split_chunks_evenly=config["split_chunks_evenly"], eps_averaging=eps_avg, ) - spx_maxeval = accuracy.get("subpixel_maxeval", 0) + spx_maxeval = accuracy["subpixel_maxeval"] if spx_maxeval > 0: sim_kwargs["subpixel_maxeval"] = spx_maxeval - spx_tol = accuracy.get("subpixel_tol", 1e-4) + spx_tol = accuracy["subpixel_tol"] if spx_tol != 1e-4: sim_kwargs["subpixel_tol"] = spx_tol sim = mp.Simulation(**sim_kwargs) # --- Diagnostics & preview mode --- - diag_geometry = diagnostics.get("save_geometry", True) - diag_fields = diagnostics.get("save_fields", True) - diag_epsilon = diagnostics.get("save_epsilon_raw", False) + diag_geometry = diagnostics["save_geometry"] + diag_fields = diagnostics["save_fields"] + diag_epsilon = diagnostics["save_epsilon_raw"] if diag_geometry or diag_epsilon or preview_only: logger.info("Initializing simulation for diagnostics...") @@ -1039,12 +1054,12 @@ def main(): logger.info("Building monitors...") monitors = build_monitors(config, sim) - stopping = config.get("stopping", {}) - run_after = stopping.get("run_after_sources", 100) + stopping = config["stopping"] + run_after = stopping["run_after_sources"] # Build verbose step functions step_funcs = [] - verbose_interval = config.get("verbose_interval", 0) + verbose_interval = config["verbose_interval"] if verbose_interval > 0: _wall_start = time.time() def _verbose_print(sim_obj): @@ -1053,8 +1068,8 @@ def _verbose_print(sim_obj): step_funcs.append(mp.at_every(verbose_interval, _verbose_print)) # Animation field capture step function (raw data, no plotting yet) - diag_animation = diagnostics.get("save_animation", False) - animation_interval = diagnostics.get("animation_interval", 0.5) + diag_animation = diagnostics["save_animation"] + animation_interval = diagnostics["animation_interval"] _frame_counter = [0] # mutable container for closure _anim_plane = None @@ -1078,53 +1093,78 @@ def _capture_frame(sim_obj): animation_interval, ) - stop_mode = stopping.get("mode", "fixed") + stop_mode = stopping["mode"] + wall_time_max = stopping.get("wall_time_max", 0) + if wall_time_max > 0: + logger.info("Wall-clock safety net: %.0f seconds", wall_time_max) wall_start = time.time() if stop_mode == "dft_decay": - decay_by = stopping.get("decay_by", 1e-3) - min_time = stopping.get("dft_min_run_time", 100) + decay_by = stopping["decay_by"] + min_time = stopping["dft_min_run_time"] logger.info( "Running simulation (dft_decay mode: tol=%s, " "min=%.1f, max=%.1f)...", decay_by, min_time, run_after, ) - sim.run( - *step_funcs, - until_after_sources=mp.stop_when_dft_decayed( - tol=decay_by, - minimum_run_time=min_time, - maximum_run_time=run_after, - ), + dft_fn = mp.stop_when_dft_decayed( + tol=decay_by, + minimum_run_time=min_time, + maximum_run_time=run_after, ) + if wall_time_max > 0: + conds = [dft_fn, _make_wall_time_cap(wall_time_max)] + sim.run(*step_funcs, until_after_sources=conds) + else: + sim.run(*step_funcs, until_after_sources=dft_fn) elif stop_mode == "energy_decay": - dt = stopping.get("decay_dt", 50.0) - decay_by = stopping.get("decay_by", 0.05) + dt = stopping["decay_dt"] + decay_by = stopping["decay_by"] logger.info( "Running simulation (energy_decay mode: dt=%s, " "decay_by=%s, cap=%.1f)...", dt, decay_by, run_after, ) energy_fn = mp.stop_when_energy_decayed(dt=dt, decay_by=decay_by) - sim.run(*step_funcs, until_after_sources=[energy_fn, _make_time_cap(run_after)]) + conds = [energy_fn, _make_time_cap(run_after)] + if wall_time_max > 0: + conds.append(_make_wall_time_cap(wall_time_max)) + sim.run(*step_funcs, until_after_sources=conds) elif stop_mode == "field_decay": - dt = stopping.get("decay_dt", 50.0) - comp_name = stopping.get("decay_component", "Ey") + dt = stopping["decay_dt"] + comp_name = stopping["decay_component"] comp = _COMPONENT_MAP.get(comp_name, mp.Ey) - decay_by = stopping.get("decay_by", 0.05) + decay_by = stopping["decay_by"] monitor_pt = resolve_decay_monitor_point(config) logger.info("Running simulation (field_decay mode: component=%s, dt=%s, " "decay_by=%s, cap=%.1f)...", comp_name, dt, decay_by, run_after) # Decay condition + numeric time cap (list = OR logic, first wins) decay_fn = mp.stop_when_fields_decayed(dt, comp, monitor_pt, decay_by) - sim.run(*step_funcs, until_after_sources=[decay_fn, _make_time_cap(run_after)]) + conds = [decay_fn, _make_time_cap(run_after)] + if wall_time_max > 0: + conds.append(_make_wall_time_cap(wall_time_max)) + sim.run(*step_funcs, until_after_sources=conds) else: logger.info("Running simulation (until_after_sources=%.1f)...", run_after) - sim.run(*step_funcs, until_after_sources=run_after) + if wall_time_max > 0: + conds = [ + _make_time_cap(run_after), + _make_wall_time_cap(wall_time_max), + ] + sim.run(*step_funcs, until_after_sources=conds) + else: + sim.run(*step_funcs, until_after_sources=run_after) wall_seconds = time.time() - wall_start + if wall_time_max > 0 and wall_seconds >= wall_time_max * 0.95: + logger.warning( + "Wall-clock limit likely triggered (%.1fs elapsed, limit %.0fs). " + "Results may be incomplete — consider increasing wall_time_max or " + "using a coarser resolution.", + wall_seconds, wall_time_max, + ) if diag_fields: save_field_snapshot(sim, config, cell_center) diff --git a/src/gsim/meep/simulation.py b/src/gsim/meep/simulation.py index ea938ae8..7edc4221 100644 --- a/src/gsim/meep/simulation.py +++ b/src/gsim/meep/simulation.py @@ -334,6 +334,7 @@ def _stopping_config(self) -> Any: decay_component=s.stopping_component, decay_dt=s.stopping_dt, decay_monitor_port=s.stopping_monitor_port, + wall_time_max=s.wall_time_max, ) def _domain_config(self) -> Any: diff --git a/tests/meep/test_meep_models.py b/tests/meep/test_meep_models.py index 9b15fcc8..09bae0a7 100644 --- a/tests/meep/test_meep_models.py +++ b/tests/meep/test_meep_models.py @@ -32,25 +32,23 @@ class TestWavelengthConfig: """Test WavelengthConfig frequency/wavelength conversion.""" - def test_defaults(self): - cfg = WavelengthConfig() - assert cfg.wavelength == 1.55 - assert cfg.bandwidth == 0.1 - assert cfg.num_freqs == 11 + def test_requires_all_fields(self): + with pytest.raises(ValidationError): + WavelengthConfig() def test_fcen(self): - cfg = WavelengthConfig(wavelength=1.55) + cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) assert abs(cfg.fcen - 1.0 / 1.55) < 1e-10 def test_df(self): - cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1) + cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) wl_min = 1.55 - 0.05 wl_max = 1.55 + 0.05 expected_df = 1.0 / wl_min - 1.0 / wl_max assert abs(cfg.df - expected_df) < 1e-10 def test_model_dump(self): - cfg = WavelengthConfig() + cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) d = cfg.model_dump() assert "wavelength" in d assert "fcen" in d @@ -81,7 +79,7 @@ def test_custom(self): assert cfg.pixels_per_um == 48 def test_model_dump(self): - cfg = ResolutionConfig() + cfg = ResolutionConfig(pixels_per_um=32) d = cfg.model_dump() assert d["pixels_per_um"] == 32 @@ -90,12 +88,25 @@ class TestSimConfig: """Test SimConfig serialization.""" def test_to_json(self, tmp_path): - wl_cfg = WavelengthConfig() + wl_cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) fwidth = SourceConfig().compute_fwidth(wl_cfg.fcen, wl_cfg.df) source_cfg = SourceConfig(fwidth=fwidth) - stopping_cfg = StoppingConfig(mode="dft_decay", max_time=200.0) + stopping_cfg = StoppingConfig( + mode="dft_decay", + max_time=200.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + ) + from gsim.meep.models.config import ( + AccuracyConfig, + DiagnosticsConfig, + ) + cfg = SimConfig( gds_filename="layout.gds", + verbose_interval=0, layer_stack=[ # ty: ignore[invalid-argument-type] { "layer_name": "core", @@ -121,7 +132,32 @@ def test_to_json(self, tmp_path): wavelength=wl_cfg, source=source_cfg, stopping=stopping_cfg, - resolution=ResolutionConfig(), + resolution=ResolutionConfig(pixels_per_um=32), + domain=DomainConfig( + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=0.0, + ), + accuracy=AccuracyConfig( + eps_averaging=False, + subpixel_maxeval=0, + subpixel_tol=1e-4, + simplify_tol=0.0, + ), + diagnostics=DiagnosticsConfig( + save_geometry=True, + save_fields=True, + save_epsilon_raw=False, + save_animation=False, + animation_interval=0.5, + preview_only=False, + verbose_interval=0, + ), + dielectrics=[], + symmetries=[], ) path = tmp_path / "config.json" cfg.to_json(path) @@ -519,18 +555,18 @@ def test_import_all_public_api(self): class TestDomainConfig: """Test DomainConfig model.""" - def test_defaults(self): - cfg = DomainConfig() - assert cfg.dpml == 1.0 - assert cfg.margin_xy == 0.5 - assert cfg.margin_z_above == 0.5 - assert cfg.margin_z_below == 0.5 - assert cfg.port_margin == 0.5 - assert cfg.extend_ports == 0.0 + def test_requires_all_fields(self): + with pytest.raises(ValidationError): + DomainConfig() def test_custom(self): cfg = DomainConfig( - dpml=0.5, margin_xy=0.2, margin_z_above=0.3, margin_z_below=0.4 + dpml=0.5, + margin_xy=0.2, + margin_z_above=0.3, + margin_z_below=0.4, + port_margin=0.5, + extend_ports=0.0, ) assert cfg.dpml == 0.5 assert cfg.margin_xy == 0.2 @@ -538,17 +574,36 @@ def test_custom(self): assert cfg.margin_z_below == 0.4 def test_extend_ports_custom(self): - cfg = DomainConfig(extend_ports=2.5) + cfg = DomainConfig( + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=2.5, + ) assert cfg.extend_ports == 2.5 def test_extend_ports_serialization(self): - cfg = DomainConfig(extend_ports=3.0) + cfg = DomainConfig( + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=3.0, + ) d = cfg.model_dump() assert d["extend_ports"] == 3.0 def test_model_dump(self): cfg = DomainConfig( - dpml=2.0, margin_xy=0.5, margin_z_above=1.0, margin_z_below=1.5 + dpml=2.0, + margin_xy=0.5, + margin_z_above=1.0, + margin_z_below=1.5, + port_margin=0.5, + extend_ports=0.0, ) d = cfg.model_dump() assert d["dpml"] == 2.0 @@ -560,23 +615,71 @@ def test_model_dump(self): class TestSimConfigComponentBbox: """Test SimConfig.component_bbox field.""" - def test_default_none(self): - cfg = SimConfig() + @pytest.fixture + def _sim_kwargs(self): + from gsim.meep.models.config import AccuracyConfig, DiagnosticsConfig + + return dict( + gds_filename="layout.gds", + verbose_interval=0, + layer_stack=[], + dielectrics=[], + ports=[], + materials={}, + wavelength=WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11), + source=SourceConfig(), + stopping=StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + ), + resolution=ResolutionConfig(pixels_per_um=32), + domain=DomainConfig( + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=0.0, + ), + accuracy=AccuracyConfig( + eps_averaging=False, + subpixel_maxeval=0, + subpixel_tol=1e-4, + simplify_tol=0.0, + ), + diagnostics=DiagnosticsConfig( + save_geometry=True, + save_fields=True, + save_epsilon_raw=False, + save_animation=False, + animation_interval=0.5, + preview_only=False, + verbose_interval=0, + ), + symmetries=[], + ) + + def test_default_none(self, _sim_kwargs): + cfg = SimConfig(**_sim_kwargs) assert cfg.component_bbox is None - def test_with_bbox(self): - cfg = SimConfig(component_bbox=[-5.0, -2.0, 5.0, 2.0]) + def test_with_bbox(self, _sim_kwargs): + cfg = SimConfig(**_sim_kwargs, component_bbox=[-5.0, -2.0, 5.0, 2.0]) assert cfg.component_bbox == [-5.0, -2.0, 5.0, 2.0] - def test_json_roundtrip(self, tmp_path): - cfg = SimConfig(component_bbox=[-1.0, -0.5, 1.0, 0.5]) + def test_json_roundtrip(self, tmp_path, _sim_kwargs): + cfg = SimConfig(**_sim_kwargs, component_bbox=[-1.0, -0.5, 1.0, 0.5]) path = tmp_path / "config.json" cfg.to_json(path) data = json.loads(path.read_text()) assert data["component_bbox"] == [-1.0, -0.5, 1.0, 0.5] - def test_json_roundtrip_none(self, tmp_path): - cfg = SimConfig() + def test_json_roundtrip_none(self, tmp_path, _sim_kwargs): + cfg = SimConfig(**_sim_kwargs) path = tmp_path / "config.json" cfg.to_json(path) data = json.loads(path.read_text()) @@ -587,14 +690,50 @@ class TestDomainInSimConfig: """Test domain serialization in SimConfig JSON.""" def test_domain_in_sim_config_json(self, tmp_path): + from gsim.meep.models.config import AccuracyConfig, DiagnosticsConfig + cfg = SimConfig( gds_filename="layout.gds", + verbose_interval=0, layer_stack=[], + dielectrics=[], ports=[], materials={}, - wavelength=WavelengthConfig(), - resolution=ResolutionConfig(), - domain=DomainConfig(dpml=0.5, margin_xy=0.2), + wavelength=WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11), + source=SourceConfig(), + stopping=StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + ), + resolution=ResolutionConfig(pixels_per_um=32), + domain=DomainConfig( + dpml=0.5, + margin_xy=0.2, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=0.0, + ), + accuracy=AccuracyConfig( + eps_averaging=False, + subpixel_maxeval=0, + subpixel_tol=1e-4, + simplify_tol=0.0, + ), + diagnostics=DiagnosticsConfig( + save_geometry=True, + save_fields=True, + save_epsilon_raw=False, + save_animation=False, + animation_interval=0.5, + preview_only=False, + verbose_interval=0, + ), + symmetries=[], ) path = tmp_path / "config.json" cfg.to_json(path) @@ -618,9 +757,9 @@ def test_creation(self): assert s.direction == "X" assert s.phase == -1 - def test_default_phase(self): - s = SymmetryEntry(direction="Y") - assert s.phase == 1 + def test_requires_phase(self): + with pytest.raises(ValidationError): + SymmetryEntry(direction="Y") def test_model_dump(self): s = SymmetryEntry(direction="Z", phase=-1) @@ -644,43 +783,121 @@ def test_invalid_phase(self): class TestStoppingConfig: """Test StoppingConfig model.""" - def test_default_is_fixed(self): - cfg = StoppingConfig() - assert cfg.mode == "fixed" - assert cfg.max_time == 100.0 - assert cfg.decay_dt == 50.0 - assert cfg.decay_component == "Ey" - assert cfg.threshold == 0.05 - assert cfg.decay_monitor_port is None + def test_requires_all_fields(self): + with pytest.raises(ValidationError): + StoppingConfig() def test_field_decay_mode(self): - cfg = StoppingConfig(mode="field_decay", threshold=1e-4, decay_dt=25.0) + cfg = StoppingConfig( + mode="field_decay", + threshold=1e-4, + decay_dt=25.0, + max_time=100.0, + decay_component="Ey", + dft_min_run_time=100, + ) assert cfg.mode == "field_decay" assert cfg.threshold == 1e-4 assert cfg.decay_dt == 25.0 def test_energy_decay_mode(self): - cfg = StoppingConfig(mode="energy_decay", decay_dt=100, threshold=1e-4) + cfg = StoppingConfig( + mode="energy_decay", + decay_dt=100, + threshold=1e-4, + max_time=100.0, + decay_component="Ey", + dft_min_run_time=100, + ) assert cfg.mode == "energy_decay" assert cfg.decay_dt == 100 assert cfg.threshold == 1e-4 def test_invalid_mode(self): with pytest.raises(ValidationError): - StoppingConfig(mode="invalid") # ty: ignore[invalid-argument-type] + StoppingConfig( # ty: ignore[invalid-argument-type] + mode="invalid", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + ) def test_threshold_bounds(self): with pytest.raises(ValidationError): - StoppingConfig(threshold=0) + StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0, + dft_min_run_time=100, + ) + with pytest.raises(ValidationError): + StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=1.0, + dft_min_run_time=100, + ) + + def test_wall_time_max_default(self): + cfg = StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + ) + assert cfg.wall_time_max == 0.0 + + def test_wall_time_max_set(self): + cfg = StoppingConfig( + mode="field_decay", + max_time=200.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + wall_time_max=3600, + ) + assert cfg.wall_time_max == 3600 + + def test_wall_time_max_serialized(self): + cfg = StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + wall_time_max=1800, + ) + data = cfg.model_dump() + assert data["wall_time_max"] == 1800 + + def test_wall_time_max_negative_rejected(self): with pytest.raises(ValidationError): - StoppingConfig(threshold=1.0) + StoppingConfig( + mode="fixed", + max_time=100.0, + decay_dt=50.0, + decay_component="Ey", + threshold=0.05, + dft_min_run_time=100, + wall_time_max=-1, + ) class TestWavelengthConfigStopping: """Test that WavelengthConfig no longer embeds StoppingConfig.""" def test_model_dump_excludes_stopping(self): - cfg = WavelengthConfig() + cfg = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) d = cfg.model_dump() assert "stopping" not in d assert "run_after_sources" not in d @@ -735,7 +952,7 @@ def test_auto_fwidth_wider_than_monitor(self): """Auto source fwidth should always be wider than monitor df.""" cfg = SourceConfig() fcen = 1.0 / 1.55 - wl = WavelengthConfig(wavelength=1.55, bandwidth=0.1) + wl = WavelengthConfig(wavelength=1.55, bandwidth=0.1, num_freqs=11) fwidth = cfg.compute_fwidth(fcen, wl.df) assert fwidth > wl.df @@ -873,7 +1090,12 @@ def test_build_sim_overlay(self): ) domain_cfg = DomainConfig( - dpml=1.0, margin_xy=0.5, margin_z_above=0.0, margin_z_below=0.0 + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.0, + margin_z_below=0.0, + port_margin=0.5, + extend_ports=0.0, ) port_data = [ @@ -932,7 +1154,14 @@ def test_build_sim_overlay_with_dielectrics(self): bbox=((-2.0, -1.0, -1.0), (2.0, 1.0, 1.22)), ) - domain_cfg = DomainConfig(dpml=1.0, margin_xy=0.5) + domain_cfg = DomainConfig( + dpml=1.0, + margin_xy=0.5, + margin_z_above=0.5, + margin_z_below=0.5, + port_margin=0.5, + extend_ports=0.0, + ) dielectrics = [ {"name": "substrate", "material": "silicon", "zmin": -1.0, "zmax": 0.0}, {"name": "oxide", "material": "SiO2", "zmin": 0.0, "zmax": 1.22}, @@ -990,6 +1219,22 @@ def test_script_handles_component_bbox(self): assert "bbox_top" in script assert "bbox_bottom" in script + def test_script_contains_wall_time_cap(self): + """Verify runner script has wall-clock time cap logic.""" + from gsim.meep.script import generate_meep_script + + script = generate_meep_script() + assert "_make_wall_time_cap" in script + assert "wall_time_max" in script + + def test_script_wall_time_all_modes(self): + """Wall-clock cap should be wired into all 4 stopping modes.""" + from gsim.meep.script import generate_meep_script + + script = generate_meep_script() + # Each mode branch should reference _make_wall_time_cap + assert script.count("_make_wall_time_cap") >= 4 + # --------------------------------------------------------------------------- # Render2d overlay tests diff --git a/tests/meep/test_simulation.py b/tests/meep/test_simulation.py index 24f06f2e..f62cb6aa 100644 --- a/tests/meep/test_simulation.py +++ b/tests/meep/test_simulation.py @@ -250,13 +250,31 @@ def test_stop_when_fields_decayed(self): assert f.stopping_threshold == 1e-4 assert f.stopping_monitor_port == "o2" - def test_stop_after(self): + def test_stop_after_sources(self): f = FDTD() - result = f.stop_after(time=500) + result = f.stop_after_sources(time=500) assert result is f assert f.stopping == "fixed" assert f.max_time == 500 + def test_wall_time_max_default(self): + f = FDTD() + assert f.wall_time_max == 0.0 + + def test_stop_after_walltime(self): + f = FDTD() + result = f.stop_after_walltime(seconds=3600) + assert result is f + assert f.wall_time_max == 3600 + + def test_wall_time_max_orthogonal(self): + """wall_time_max can be combined with any stopping mode.""" + f = FDTD() + f.stop_when_fields_decayed(dt=50, decay_by=0.05) + f.stop_after_walltime(seconds=1800) + assert f.stopping == "field_decay" + assert f.wall_time_max == 1800 + def test_geometry_component(self): sim = Simulation() sim.geometry.component = "placeholder" From 1b20e3f11625c206962ddc01c14a5622bf949506 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Tue, 17 Feb 2026 13:38:42 +0100 Subject: [PATCH 07/10] ci: replace pull_request trigger with workflow_dispatch for docs and release --- .github/workflows/docs.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 09e27191..0b6e0710 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ on: - main tags: - "[0-9]+.[0-9]+.[0-9]+" - pull_request: + workflow_dispatch: env: GFP_API_KEY: ${{ secrets.GFP_API_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e81294d5..6847dafe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: push: tags: - "[0-9]*.[0-9]*.[0-9]*" - pull_request: + workflow_dispatch: jobs: build: From 20dd2776a19ac335e87a863861e9a038b8a2873d Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Wed, 18 Feb 2026 12:05:46 +0100 Subject: [PATCH 08/10] fix: separate source and monitor positions to fix 2x S-parameter error MEEP eigenmode coefficients are wrong when source and monitor overlap at the same position (standard FDTD soft-source behavior). Match the gplugins approach: offset source inward by 0.1um, source-port monitor by 0.3um (0.2um past source), non-source monitors by 0.1um. Also includes: refactor gcloud.py to use RunResult dataclass with structured sim-data-{job_name}/ directories, update palace module run() signatures, add meep_straight and meep_sbend notebooks, remove stale meep-overview.md. --- nbs/meep_api_styles.ipynb | 76 +--------------- nbs/meep_ring_coupler.ipynb | 120 ++++++++++++++++++++---- nbs/meep_sbend.ipynb | 151 +++++++++++++++++++++++++++++++ nbs/meep_straight.ipynb | 151 +++++++++++++++++++++++++++++++ nbs/meep_ybranch.ipynb | 21 +---- nbs/palace_demo_cpw.ipynb | 16 +--- nbs/palace_demo_microstrip.ipynb | 16 +--- src/gsim/gcloud.py | 129 ++++++++++++++++---------- src/gsim/meep/__init__.py | 2 +- src/gsim/meep/meep-overview.md | 135 --------------------------- src/gsim/meep/models/api.py | 18 +++- src/gsim/meep/models/config.py | 9 ++ src/gsim/meep/script.py | 48 +++++++++- src/gsim/meep/simulation.py | 46 +++++++--- src/gsim/palace/__init__.py | 2 +- src/gsim/palace/driven.py | 19 ++-- src/gsim/palace/eigenmode.py | 4 +- src/gsim/palace/electrostatic.py | 4 +- tests/meep/test_meep_models.py | 18 ++++ tests/meep/test_simulation.py | 18 ++-- 20 files changed, 648 insertions(+), 355 deletions(-) create mode 100644 nbs/meep_sbend.ipynb create mode 100644 nbs/meep_straight.ipynb delete mode 100644 src/gsim/meep/meep-overview.md diff --git a/nbs/meep_api_styles.ipynb b/nbs/meep_api_styles.ipynb index 19535ab7..e0bee08d 100644 --- a/nbs/meep_api_styles.ipynb +++ b/nbs/meep_api_styles.ipynb @@ -36,18 +36,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from gsim import meep\n", - "\n", - "sim = meep.Simulation()\n", - "\n", - "sim.geometry(component=c, z_crop=\"auto\")\n", - "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", - "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "sim.domain(pml=1.0, margin=0.5)\n", - "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)" - ] + "source": "from gsim import meep\n\nsim = meep.Simulation()\n\nsim.geometry(component=c, z_crop=\"auto\")\nsim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\nsim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\nsim.monitors = [\"o1\", \"o2\", \"o3\"]\nsim.domain(pml=1.0, margin=0.5)\nsim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)" }, { "cell_type": "markdown", @@ -63,31 +52,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from gsim import meep\n", - "\n", - "sim = meep.Simulation()\n", - "\n", - "sim.geometry.component = c\n", - "sim.geometry.z_crop = \"auto\"\n", - "\n", - "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "\n", - "sim.source.port = \"o1\"\n", - "sim.source.wavelength = 1.55\n", - "sim.source.bandwidth = 0.01\n", - "sim.source.num_freqs = 11\n", - "\n", - "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "\n", - "sim.domain.pml = 1.0\n", - "sim.domain.margin = 0.5\n", - "\n", - "sim.solver.resolution = 20\n", - "sim.solver.simplify_tol = 0.01\n", - "sim.solver.save_animation = True\n", - "sim.solver.verbose_interval = 5.0" - ] + "source": "from gsim import meep\n\nsim = meep.Simulation()\n\nsim.geometry.component = c\nsim.geometry.z_crop = \"auto\"\n\nsim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n\nsim.source.port = \"o1\"\nsim.source.wavelength = 1.55\nsim.source.wavelength_span = 0.01\nsim.source.num_freqs = 11\n\nsim.monitors = [\"o1\", \"o2\", \"o3\"]\n\nsim.domain.pml = 1.0\nsim.domain.margin = 0.5\n\nsim.solver.resolution = 20\nsim.solver.simplify_tol = 0.01\nsim.solver.save_animation = True\nsim.solver.verbose_interval = 5.0" }, { "cell_type": "markdown", @@ -103,21 +68,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from gsim import meep\n", - "from gsim.meep import Domain, FDTD, Geometry, ModeSource\n", - "\n", - "sim = meep.Simulation()\n", - "\n", - "sim.geometry = Geometry(component=c, z_crop=\"auto\")\n", - "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "sim.source = ModeSource(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", - "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "sim.domain = Domain(pml=1.0, margin=0.5)\n", - "sim.solver = FDTD(\n", - " resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0\n", - ")" - ] + "source": "from gsim import meep\nfrom gsim.meep import Domain, FDTD, Geometry, ModeSource\n\nsim = meep.Simulation()\n\nsim.geometry = Geometry(component=c, z_crop=\"auto\")\nsim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\nsim.source = ModeSource(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\nsim.monitors = [\"o1\", \"o2\", \"o3\"]\nsim.domain = Domain(pml=1.0, margin=0.5)\nsim.solver = FDTD(\n resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0\n)" }, { "cell_type": "markdown", @@ -133,24 +84,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from gsim import meep\n", - "\n", - "sim = meep.Simulation()\n", - "\n", - "sim.geometry(component=c, z_crop=\"auto\")\n", - "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", - "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "sim.domain(pml=1.0, margin=0.5)\n", - "sim.solver(resolution=20, simplify_tol=0.01)\n", - "\n", - "# Conditional tweaks with attribute style\n", - "debug = True\n", - "if debug:\n", - " sim.solver.save_animation = True\n", - " sim.solver.verbose_interval = 5.0" - ] + "source": "from gsim import meep\n\nsim = meep.Simulation()\n\nsim.geometry(component=c, z_crop=\"auto\")\nsim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\nsim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\nsim.monitors = [\"o1\", \"o2\", \"o3\"]\nsim.domain(pml=1.0, margin=0.5)\nsim.solver(resolution=20, simplify_tol=0.01)\n\n# Conditional tweaks with attribute style\ndebug = True\nif debug:\n sim.solver.save_animation = True\n sim.solver.verbose_interval = 5.0" } ], "metadata": { @@ -165,4 +99,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/nbs/meep_ring_coupler.ipynb b/nbs/meep_ring_coupler.ipynb index f5b75b28..c40da89c 100644 --- a/nbs/meep_ring_coupler.ipynb +++ b/nbs/meep_ring_coupler.ipynb @@ -25,16 +25,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[32m2026-02-18 06:12:13.722\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m25\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.728\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m48\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.729\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m110\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.730\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m149\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.731\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m168\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.731\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m210\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.732\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m244\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.732\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m269\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", + "\u001b[32m2026-02-18 06:12:13.733\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m294\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from ubcpdk import PDK, cells\n", "\n", "PDK.activate()\n", "\n", - "c = cells.coupler_ring()\n", + "c = cells.coupler_ring(gap=0.2)\n", "\n", "c" ] @@ -52,7 +78,17 @@ "execution_count": null, "id": "4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Stack validation: PASSED\n", + "Warnings:\n", + " - No stack configured. Will use active PDK with defaults.\n" + ] + } + ], "source": [ "from gsim import meep\n", "\n", @@ -60,21 +96,32 @@ "\n", "sim.geometry(component=c, z_crop=\"auto\")\n", "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", + "sim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\n", "sim.monitors = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", - "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", - "sim.solver.stop_after_sources(time=160)\n", + "sim.solver(resolution=25, save_animation=True, verbose_interval=5.0)\n", + "sim.solver.stop_after_sources(time=100)\n", "\n", "print(sim.validate_config())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "sim.plot_2d(slices=\"xyz\")" ] @@ -89,10 +136,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Uploading simulation... done\n", + "Job started: meep-57e3d203\n", + "Created: 05:12:16 | Now: 05:31:54 | Status: completed\n", + "Extracting results.tar.gz...\n", + "Downloaded 362 files to /Users/vahid/doplaydo/gsim/nbs/sim-data-meep-57e3d203/results\n" + ] + } + ], "source": [ "# Run on GDSFactory+ cloud\n", "result = sim.run()" @@ -100,20 +159,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXopJREFUeJzt3Qd8FGX+x/HfJqTQEkoKLXQBQbqCoBQVQfTsYj/BAuqpqNhAlKKnCHqC+ldRUVFPT2xgP0GxcFRFqWIBKQqk0BJa+v5fvyfZZVtCNiTZ2cnn7WtNdnZ2dmae7PLdp43D6XQ6BQAAAGEvItQ7AAAAgIpBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AS1i7dq1ccskl0qJFC4mNjZWmTZvKmWeeKc8888xRn+twONy3iIgIadKkiQwePFi++eYbsbNDhw7JpEmTbH+cAMrOwbViAYTakiVL5LTTTpPmzZvL8OHDpVGjRvLnn3/KsmXLZNOmTbJx48ZSn6+BTkPgNddcI3r5682bN8tzzz0n6enp8umnn8rQoUPFjnbt2iWJiYkyceJEE/AAoAanAECoPfLIIxIfHy/ff/+91KtXz+sxDWdl0a5dO7n66qvd9y+88ELp0qWLzJgx45iD3cGDB6V27dpSXVS34wXshKZYACGntXKdOnXyC3UqKSmpXNvs3LmzJCQkmNo7tWjRIhk2bJipFYyJiZGUlBS588475fDhw17PGzFihNSpU8fs09lnny1169aVq666qlzb2LZtm/ztb38zv2vT8rPPPutudj799NNNeNKm57feestv//ft2yd33HGHeQ19rbZt28rUqVOlsLDQPL5lyxZTW6cmT57sbor2rLn75ZdfTPN2gwYNTPP2iSeeKB999JHX68yePds879tvv5V//OMf5nw3a9bMPLZ//36zDy1btjT7oI9pzeiPP/5YrjIBUPmosQMQchpuli5dKuvWrZMTTjihQra5d+9ec9NApN59913TJ+3mm2+Whg0byooVK0z/vb/++ss85ik/P1+GDBkip556qjzxxBNSq1atoLdRUFBgagr79+8v06ZNkzfffFNuvfVWE+bGjx9vwuJFF10kM2fONE3Iffr0kVatWpnn6msMGDBAtm/fLjfeeKMJktpcPW7cONm5c6ephdRQ9/zzz5t90dpJ3ZbSWkq1fv16OeWUU0ygHDt2rHndd955Ry644AJ5//33zXM8aajTbU6YMMHU2KmbbrpJ3nvvPbPfHTt2lN27d8v//vc/2bBhg/To0aNCyglABdM+dgAQSvPnz3dGRkaaW58+fZz33nuv84svvnDm5uaW6fn6UXb99dc7MzIynOnp6c7ly5c7zzjjDLP8X//6l1nn0KFDfs+bMmWK0+FwOLdu3epeNnz4cPO8sWPH+q0f7DYeffRR97K9e/c6a9asadZ9++233ct/+eUXs+7EiRPdyx5++GFn7dq1nb/99pvXa+k+6Tnatm2bua/H6/tcFz3+zp07O7Ozs93LCgsLnX379nUed9xx7mWvvvqq2capp57qzM/P99pGfHy885ZbbvHbNgDroikWQMhp857W2J133nmyevVqU8OlNWZa2+TbdFiSl19+2dQ4aXNh7969ZfHixTJmzBjTlKhq1qzpXldrpHTgQd++fc1gi59++slve1oT5ivYbdxwww3u37WZuX379qbm7NJLL3Uv12X62B9//OFeprV//fr1k/r165vXcN0GDRpkagK/++67Us/Fnj17ZOHCheZ1tDnV9XytcdPz+vvvv5vaQE8jR46UyMhIr2W6X8uXL5cdO3aU+noArIOmWACWcNJJJ8kHH3wgubm5JtzNnTtXpk+fbvqIrVq1yoyU1cc8Q5YOuHA5//zzTZOh9hfTfnHaZ89zAID2d9NmRg2K2kTrKTMz0+t+jRo13P3MPAWzDe3T5uoD56L7q9vVffRd7rk9DV5r1qzxe35ZB5ToKGINmw8++KC5lbQNDc4urmZgTxqwdZSy9vPr2bOn6XOozcatW7cu9fUBhA7BDoClREdHm5CnNx3peu2115oarK+//tp08HfRwKEd/100MGmNViBay6W1glqTdd9990mHDh1M6NNaKx3o4BqQ4KIDBXQ+vGPZhm/t19GWF7UoF9Ft6Wvde++9AdfV81Ia177cfffdpoYuEFffw0C1kS5a46c1hxqy58+fL48//rgZwKEB3K5TyADhjmAHwLJ0FKfSAQP/+te/vGq1dBListJRqL/99pu89tprpsbJZcGCBVW6jbJq06aNHDhwoMSg6uJb8+fiqlGLioo66jaOpnHjxmZghd60lk8HTej0NAQ7wJroYwcg5LQ2zrPGyuWzzz5z90PTpkANKa6bjtIsK1ctmedr6O9PPfVUlW6jrLSmTPscfvHFFwGnQdFRu8o1WleXedJ+hgMHDpQXXnjBhGJfGRkZR90HraH0bV7W7WqgzsnJCfqYAFQNauwAhNxtt91mpvjQKTi0iVP70un0HnPmzDFzqGlz7LHQbWotmDZNatNpXFycmfLDt59cZW+jrO655x7Tj0/nwNNmXg21OlhDaw11+hGdw07n6NPmUw24ep60eVbnq9PpYvSmc+bpdC06n58OjNBavLS0NBMYdXoW7cdYGh10oc3b2sexa9euZi6+L7/80kwirbWnAKyJYAcg5HSuOO1HpzV0L774ogl2OnebNv898MADAScuDoY2SX788ccyevRomTJlihnYoCFSB1toaKmqbZSV1sRpf8JHH33UnJfXX3/dBEkNbzoZseegkVmzZplgrBMl63nTy4tpsNPA98MPP5j1tS+ijojVGrfu3bubASBl2Qc9/9q3TvvUab897Zenl2oLNGIYgDVwrVgAAACboI8dAACATRDsAAAAbIJgBwAAYBMEOwAAAJsg2AEAANgEwQ4AAMAmmMfOh87VtGPHDnMR8ZIu1wMAAFBV9Co3Omm4XvnF9zrWvgh2PjTUpaSkVGb5AAAABO3PP/80V4QpDcHOh9bUuU6ezvSOkms29XqTiYmJR/32gKpBmVgL5WEtlIe1UB7BycrKMpVOroxSGoKdD1fzq4Y6gl3pb8rs7Gxzjgh21kCZWAvlYS2Uh7VQHuVTli5iVLUAAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCXQjkpabKwWXLzc9wVpieIYeWh/9x2KU8dP/zfvrJFsdBeVgH5WEtdikPxb8hlaNGJW0XJdj33nuy88EJIk6niMMhDa6/TuoOHOi/osNR+jk82uNFKx3Tw0UvE3ilrIULJfOlWZJZfBwNR42UuqefHtw+lvq4o3zPK/Vp/g/u//JL2fX8THd5JPzjZqk7aFAVHUdw+1ra62XNny+7nvk/cxwH9Dhuu03ihgyumP2s4H0t7bGsz/8rGU89JVJYKBIRIYl33C5xQ4ceZXuBXyPwqiXsTzDLSzymI8uzPv1U0p94whzHgYgISbr7bon72zkemwhiu8e6bkl7W4ZtZH70kaRNecxdHsnjxkn8+eeVcz8qqZzKsO6+D+ZK2sMPu8sjecKDUu+ii0rfs7K+1jEsK/W9U9K/HxMmusuj8UOTpd4ll0g42vf++5I5cZJkhvmx7LNgmTicTv0XDS5ZWVkSHx8vmZmZEhcXV6EnRr9hbTz9jKI/AAAAykIDYEn/VOtjroB4LCGzpG1XwjKn/huYne3/cGysOCIi/J9b3td0lPM4y/h6ehyF+/Z5L4yIkLYLv5KoRo0kVNmEGrsqlLtla8BQVyM5WSJq1jyy4ChZ2yllyOJHW6Useb6EdQqzs6Vg1y6/5ZEJCRIRExPU6xz1WMryteNox1LSceTmSuHevX7LI+rVk4jo6GPf/9KeWtp2g3zMmZcnhQcO+C2PqF1bHFFRpW6vxEdKek6wy4N4jrOgQJw5Of7rxcSIIzKyQl6j3MuDOVf6Hg/05c31D1ag1yjPeUX1cbS//Qr4O7HCX5ozO9sS+3FMCgsld+u2Cg92wSDYVaHoli2KPtw9P/QjIqTlnLdD+kdQITWPERHS6r13bXEcrefNtcdxfPqJLY6j7Rf/tcdxVMK3eE8lNr6UM0jqcWwaPMTvONq4yqOEbQRcWsH7drR1PRfnpaXJ5nPP9f+8+vgjiUpOLvt2y7iszI1gQb5GXnq6bLlkmP+/H+++IzUSEwM876gLyrYPZVgn8CGXvJ38jAzZevXVIoUe60RESIs3XpcaCQnlO8eVeLzOEl4vf9cu+fOGG7yfExEh0S2aSygR7KqQfhhq+7tve3w4/aOldH+TJ0+StImTwv447FIeHId1hKo8ytJnLhjRzZoFPI7olJTS90OsJbJO64DHEdumjYQTDTyBjqNmp04SbvRvKHnyZL9/Q2r17ClhpX07afzwQ5b7N4Q+dlXYx87zm7BW1WqqD/UfQHkVFhZK6rr1EnfooMS0bBm2x2GX8lA5O3ZI+uo1ktS1i8Q0aSLhivKwFsrDWuxSHvwbUnnZhGB3DCevOtM3ZXp6uiQlJUmENi8j5CgTa6E8rIXysBbKo/KyCf8iAwAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAm7BlsHv22WelZcuWEhsbK71795YVK1aEepcAAAAqne2C3Zw5c2TMmDEyceJE+fHHH6Vr164yZMgQSU9PD/WuAQAAVCrbBbsnn3xSRo4cKddee6107NhRZs6cKbVq1ZJXXnkl1LsGAABQqWqIjeTm5srKlStl3Lhx7mUREREyaNAgWbp0acDn5OTkmJtLVlaW+VlYWGhuCEzPjdPp5BxZCGViLZSHtVAe1kJ5BCeYPGKrYLdr1y4pKCiQ5ORkr+V6/5dffgn4nClTpsjkyZP9lmdkZEh2dnal7asd/sgyMzNNuNPwjNCjTKyF8rAWysNaKI/g7N+/v3oGu/LQ2j3tk+dZY5eSkiKJiYkSFxcX0n2z+pvS4XCY80SwswbKxFooD2uhPKyF8giODgatlsEuISFBIiMjJS0tzWu53m/UqFHA58TExJibLw0rBJbSabDjPFkLZWItlIe1UB7WQnmUXTB5xFZtaNHR0dKzZ0/56quvvL4V6P0+ffqEdN8AAAAqm61q7JQ2qw4fPlxOPPFE6dWrl8yYMUMOHjxoRskCAADYme2C3WWXXWYGPkyYMEFSU1OlW7du8t///tdvQAUAAIDd2C7YqVtvvdXcAAAAqhNb9bEDAACozgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AFAJ0g6myardq8zPcJZ6MFVW7FxhfoYzysN6MrIzZEVq+P9tpVrsPVIj1DtQHWnhb8vaJs3jmkuj2o0knN+UW1K3SMv4lmF9HHYpD/2Ha/Xu1dK1dldpXLexhCs7lMcHv38gk5dMlkIplIgfImRi34ly0XEXSVgex9LJUugslAhHhEzsE8bHESbl4XQ6i37qf56/i1Pm/T5P/rnsn0XHIRFy/8n3y/ltzvda17W+a1uu/zy3XdJjrt9975f6WPFrux93SomPmZ/Fq32x5Qt5fvXzZplDHHJT15vkjOZnHPUYvLYfYD9c/M6fx+/mp8+5dS3zfZ3SzoFasmOJ/OeX/5jHrPIecTg9zwQkKytL4uPjJTMzU+Li4mz9QVnaB8jR3qBzf5srj33/WNEfs0TIfb3uk/PanOd+vu8HiO+2j/VDxPX7kV31ft2jfRC47n+59UuZuXqm+8Plxi43yunNTz+yjuf2SzgO974c43GU+JjP8wPtx7Idy2TOr3PcxzGs3TDp1bhX4PNUjg/Dks5BaeVc6gdpCY+t2bXGlInrOLQsTkg4ocRzW5YP4VLLyPfcl+HcHK18D+Udkk83fyqe9FiGtBwiNWvU9NtOSa9blvfhMZ2XEo7ddT8nP0dWZawSX50TOktURFTQ59S3PAKe0wB/U8H+rfq+Tn5hvqQfTvc7joaxDc1n8NHKs7z/6Jfp3JTwXkf4inBEyBcXf1HhX0qDySYEu2M4eeWpiRjy/hAT6jzFRceJw+Ewvx/tA931u2vdQI+V9R8EAAB8v4SYnw79rei/4geOPKb/FT/uWjfQc93LPJ/rcJiwfTDvoN+Jj4uKk+ga0Udew+e1PV/H97VL2i+Haxs+++m5vUCPlfR8z+PSY9ictdnvOF4Z8oqc1OikkGUTmmKrkDYv+YY6lZWbJdVZaR8Grt/93tQ+b+ajfhD5bFvv5xXkyf68/X77Uy+6nveHS2kfBr6vHeADzvO5XsuC+CAJ+Hjx8R3IPSCbMjf5HUe7+u3Ml4aSzm1JH9KlfoAHWS5+x+go+bHd2btNs4avU5ucKkm1k7zOa0nnJlCZlHZegz3+0rbrOr6DuQfl9Z9f9/oSpY9de8K1Uje67jH9XZV4Xsv5d1XaY5k5mfLYiqJaec/XHn/yeKkfU7/C/vH3fW6pxxbk+03v7z68W25beJvXcWitynNnPCcNazYs2/vN52/+qMdewt9maecl4GMex5h+KF0u+egS0wzreRzzzp8nybWS/Y+/tPdeKX9fVcFUcrw3xO9Y3j///bDqfpEaoLJGjyOlbkpI94tgV4W0z5AWuu8fwYtnviiJNROD/0e1jN9efH93b6uEf+Dc65Swff2AuWDeBX5vyo8v+FiSayeX6YPEd1+t9KZ897x3bfHh8uwZz9riOLQ/VDgdh2pdr7Vlulwci9gasbY4jkl9J/kdxylNT5FwEh8Tb94LvsfRKr6VhBt9Pz948oPy8LKHi/oLFh9LuL3PG9VuZPbbt0xCfRw0xVbjPnbH4r1f3/N7U4bjcdilPDgO69m5f6es+XONdEnpEvaDWf7c/6ephQj1P1jHgvKwlsLCQlm/bb0cij4kLeJbhPXfVmoVvEfoY1dFJ686f1DyprQe/uGy3nskPT1dkpKSJCKCmaVCjfKwFsojOPSxszgNc+Ea6Dwlxiba4h8tu5SHNoN3bdDV3R8tXNmlPAAgFML7X2QAAAC4EewAAABsIqyC3SOPPCJ9+/aVWrVqSb169QKus23bNjnnnHPMOtpMeM8990h+fn6V7ysAAEBVC6vpTnJzc2XYsGHSp08fefnll/0eLygoMKGuUaNGsmTJEtm5c6dcc801EhUVJY8++mhI9hkAAKCqhFWN3eTJk+XOO++Uzp07B3x8/vz58vPPP8u///1v6datmwwdOlQefvhhefbZZ00oBAAAsLOwqrE7mqVLl5rQl5xcNAu3GjJkiNx8882yfv166d69u99zcnJyzM1zSLFrKLbeEJieG71EGefIOigTa6E8rIXysBbKIzjB/Ftrq2CXmprqFeqU674+FsiUKVNMTaCvjIwMyc7OrqQ9tccfmc71p+Eu3Kc7sQvKxFooD2uhPKyF8gjO/v3+l7+0bLAbO3asTJ06tdR1NmzYIB06dKiU1x83bpyMGTPGq8YuJSVFEhMTK22CYru8KfWSYHqeCHbWQJlYC+VhLZSHtVAewYmNjQ2fYHfXXXfJiBEjSl2ndevWZdqWDppYsWKF17K0tDT3Y4HExMSYmy8NKwSW0mmw4zxZC2ViLZSHtVAe1kJ5lF0weSTkwU5rfPRWEXS0rE6J4rqMj1qwYIGpeevYsWOFvAYAAIBVhTzYBUPnqNuzZ4/5qVObrFq1yixv27at1KlTRwYPHmwC3N///neZNm2a6Vf3wAMPyC233BKwVg4AAMBOwirYTZgwQV577TX3fdco16+//loGDhwokZGR8sknn5hRsFp7V7t2bRk+fLg89NBDIdxrAACAqhFWwW727NnmVpoWLVrIZ599VmX7BAAAYBXMUwEAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJsIm2G3ZskWuv/56adWqldSsWVPatGkjEydOlNzcXK/11qxZI/369ZPY2FhJSUmRadOmhWyfAQAAqlINCRO//PKLFBYWygsvvCBt27aVdevWyciRI+XgwYPyxBNPmHWysrJk8ODBMmjQIJk5c6asXbtWrrvuOqlXr56MGjUq1IcAAABQqcIm2J111lnm5tK6dWv59ddf5fnnn3cHuzfffNPU4L3yyisSHR0tnTp1klWrVsmTTz5JsAMAALYXNsEukMzMTGnQoIH7/tKlS6V///4m1LkMGTJEpk6dKnv37pX69ev7bSMnJ8fcXLTWT2ntoN4QmJ4bp9PJObIQysRaKA9roTyshfII/nzZPtht3LhRnnnmGXdtnUpNTTV98DwlJye7HwsU7KZMmSKTJ0/2W56RkSHZ2dmVsu92+SPTYK3hLiIibLpq2hplYi2Uh7VQHtZCeQRn//794RPsxo4da2rUSrNhwwbp0KGD+/727dtNs+ywYcNMP7tjMW7cOBkzZoxXjZ0OukhMTJS4uLhj2rbd35QOh8OcJ4KdNVAm1kJ5WAvlYS2UR3B0QGjYBLu77rpLRowYUeo62p/OZceOHXLaaadJ37595cUXX/Rar1GjRpKWlua1zHVfHwskJibG3HxpWCGwlE6DHefJWigTa6E8rIXysBbKo+yCySMhD3Za46O3stCaOg11PXv2lFdffdXvQPv06SPjx4+XvLw8iYqKMssWLFgg7du3D9gMCwAAYCdh0zlKQ93AgQOlefPmpl+d9oHTfnN6c7nyyivNwAmd7279+vUyZ84ceeqpp7yaWgEAAOwq5DV2ZaU1bzpgQm/NmjXzekw78Kv4+HiZP3++3HLLLaZWLyEhQSZMmMBUJwAAoFoIm2Cn/fCO1hdPdenSRRYtWlQl+wQAAGAlYdMUCwAAgNIR7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYBMEOAADAJgh2AAAANkGwAwAAsAmCHQAAgE0Q7EJgZ+ZhWbJpl/kZztL358rSTbvD/jjsUh66/yv/3G+L47BDeQCoHnZa7DOrRqh3oLqZ8/02GffBWil0ikQ4RMac2U7O7tzYPOZwOIp+eqxfvEgcHktdyyTQeh4POgKsH2g7XptzlG29uT9tl0c+2yDO4uN48G8d5cLuTYue594Xz304cmyex+R7LEXPcfi9bsDnBzoRx1geUy7qLJed1FzCjfdx/GaT4wjf8lD6Ib/qz/3SLaauNK1fW8KVHsfmXQelVUJtaRxfU8IV5WHNyoFN+3dL66Q6Yfu3NceCn1kOp1P/aYZLVlaWxMfHS2ZmpsTFxVX4B8spjy00fwCoWCWFQNdj7qDqETZFnJKT718YtaIjJbI4lXoGU1dQNb97ve6Rxzxfz3N9z/30fNxzn4s35bcsYNj12If8wkL5Le2A33Ec3zhOYmpE+O2315cF92M+++R33/uY/bd3ZP98txf49Xz3RSQ7r1D+uz7Vu1xF5OzOjaR2TA2/7Xme95LOqW/5e63vcUxF++W9Pdc+u8s2wPnz3Z7L2u375It1aaJ/Xbp06AmNpFvzel7P8zrOAOfX8+/O40/DvR8lHZvX34nnF60SvyB5H4/nPmmN/NsrtrmP44rezeWUNgke63iXVaDlno+UvL7/F1K/9Uv4gipl2OZ3v2XIK4s3my+iuvi6U1rJgHaJEqyK+Og+ln9yv/0tQ2Yv2eI+jhF9Wkq/dkXl4dqs/nT6vNaR+0fuudf3eMxZ4mNH9vnIY06v1/Rd3/0Mj/U99+/7zbtl7k873H9b53drIj1b1Df3dZ1C3Ubxuu7jKN7HI8s97nutL17r6S+ey/XfYPfxerxGidst4bUP5ebJh6t2epWR/tvxv7GnVXhQDSabEOyO4eQFS6tqr3xpud/yWlEREhlZ3Cru8Z4P+KZyL/Ncz/vN6LWZAOt5ruv5EeP7IeC7TQAAULr/jDxZ+rRpKKHKJjTFViFtytCqWs8aO033X909MKyqoXfsOySnTv3a6zj0uL69Z6A0Kj6OQN/+Slru9Y3Mc32fdQN9C/X89njkm2EJ3yo9nq/L0vdny7CZS/2O4+1RJ0tCnRi/b7ne++D5jS7Aa/ksC3yM3vvm+e3Q61yV9LrFy3YfyJG73l3tdZ71OB67qIvUrx3t9Y0z4GsG2kff/ff8VutxMkv89uxR4L7H5vtartfLPJwrz369yeuLhX6Tv3FAa6kbG+Uut0D77t4ln2/Xnn87nssCfUv3Pb9H9s3/G3uJ5egUScs6LN/8tkt89TsuQRK9/q58y9p/ud97oITajyNlFvj9UtLreJ5/39fJPJwnP+/M8juODo3qSlzNqKN+aTyyNwFez2t54PWlTOuX8Loevx/IyZdtew75HUfzBrVMTfDRlKfDR7C9RMqyvh7Hll3+x9E6obbUjS0+jgCtFaW1ZBTd92/N8H3MtyY/0D77t2SU3BVnz8Fc+XHbPr9jOallfUmsG1Ni7b73/cA19g6PVpSIElpPPNc363i0mPjVxIvDrONZY+9a50B2nry0aLPX36f+m94yoZaEEsGuCml40/b3+z9YJwVOp/kDePSiE8Iq1Kkm9WrJIxeeIOPnrjOhyHUcKQ3Cqx9RSoNaAcujV6uK/aZVFfIKCuX+D9ZKgSkPkUcv6iyXnpQi4UbLxLc8Qt1fpSK6XOixTLukS1i910s6jlevPckWxzHnxpNtcRxvjuwdVsdR2rE8fUX3sDuWNkl1LPdvOk2xVdgU6/lHrd+8NNWH+g+gvAoLC2Xdpr/kgKOmtEoM346vdikPtX3vQVm1cYd0a9sk7Dvrh3t5aIdq36AdbgH1yHGEd9BWlIf1/GfFVr/KgXD826qqz6xK72O3efNmWbRokWzdulUOHTokiYmJ0r17d+nTp4/ExsZKOKuKYGcHGuzS09MlKSlJIiKYNccKKBNrIWhbC+VhLXaqHKgKldbH7s0335SnnnpKfvjhB0lOTpYmTZpIzZo1Zc+ePbJp0yYT6q666iq57777pEWLFsd6HAAQtvQfqsiUupIU5v9g6XHY4R9dysN6kupGywlJDakcqGBlDnZaIxcdHS0jRoyQ999/X1JSvPvv5OTkyNKlS+Xtt9+WE088UZ577jkZNmxYRe8vAAAAjjXYPfbYYzJkyJASH4+JiZGBAwea2yOPPCJbtmwp66YBAABQlcGutFDnq2HDhuYGAACAMJruRMdefP3113L48GHp27ev1K9fv2L2DAAAAEEJajjjvn37ZPjw4dK5c2cZOXKkGaXRr18/GTRokJx77rly/PHHy5o1a4LbAwAAAFR9sLv77rvNAInLL79c1q5dK2eddZYUFBSYZcuXLzfBbvz48RWzZwAAAKi8ptjPP/9c3nrrLRkwYIAZHasjYxcuXCi9e/c2j0+dOlXOO++84PYAAAAAVV9jl5aWJu3atTO/N23a1Mxb5zntSfPmzSUjI6Ni9gwAAACVF+x0pujIyEj3ff3ddVFd5fk7AAAALD4qdtasWVKnTh3ze35+vsyePVsSEhLM/f3790tl0mbeVatWmUtZ6ehbHbShzb96BQwXHbxxyy23yPfff28udXbbbbfJvffeW6n7BQAAEHbBTptaX3rpJff9Ro0ayRtvvOG3TmU57bTT5P7775fGjRvL9u3bzWCOSy65RJYsWWIe11G6gwcPNoFv5syZZoDHddddJ/Xq1ZNRo0ZV2n4BAACEXbAL9dUk7rzzTvfvei3asWPHygUXXCB5eXkSFRVlrmWbm5srr7zyirn8WadOnUwN35NPPkmwAwAAthdUHzsr2bNnjwlyOimyhjql067079/fhDrPK2b8+uuvsnfv3hDuLQAAgIVq7J5++ukyb3T06NFSWe677z75v//7Pzl06JCcfPLJ8sknn7gfS01NlVatWnmtn5yc7H4s0FUxcnJyzM1Fm3NdA0X0hsD03OhVRzhH1kGZWAvlYS2Uh7VQHsEJ5t/aMge76dOne93XaU00XGn/NddVKWrVqiVJSUlBBTttTtUBEKXZsGGDdOjQwfx+zz33yPXXXy9bt26VyZMnyzXXXGPCXXlH5E6ZMsVsx5ceX3Z2drm2WV3+yDIzM024i4gI24pfW6FMrIXysBbKw1ooj+AEMzjV4dR/mYOkkxQ/99xz8vLLL0v79u3NMm3u1MuM3XjjjXLVVVeVeVsaoHbv3l3qOq1bt/ZqXnX566+/zDx6OniiT58+JuRpjdu8efPc6+h1bE8//XTTdFvWGjvdpjbdxsXFlfk4quObUstORx4T7KyBMrEWysNaKA9roTyCo9lEM4xWqBwtmwQ93Yl68MEH5b333nOHOqW/a62ejlINJthpMNDbsVRNuoKZhju9pJlrMIVasGCB2bdAoU7FxMSYmy8NKwSW0mktKefJWigTa6E8rIXysBbKo+yCySPlakPbuXOnmcPOl143Vq9OURn0WrTat05HuWozrF7K7IorrpA2bdqYQKeuvPJKU7OnTbXr16+XOXPmyFNPPSVjxoyplH0CAACwknIFuzPOOMM0uf7444/uZStXrpSbb77ZzCFXGbT/3gcffGBeW2vgNLx16dJFvv32W3eNW3x8vMyfP182b94sPXv2lLvuuksmTJjAVCcAAKBaKFdTrM4TN3z4cDnxxBPdTZ5ag6dTi+iVKSpD586dTS3d0WjYW7RoUaXsAwAAgO2CnfaJ++yzz+T33383I1aVjlpt165dRe8fAAAAKjPYuRx33HHmBgAAgDDqY/fYY4/J4cOHyzzQ4dNPPz2W/QIAAEBlBbuff/5ZmjdvLv/4xz/k888/N3OYuWj/ujVr1pi57fQSX5dddpnUrVs32H0BAABAVTTFvv7667J69Woz5YhOK6KT5UVGRpoRqXoFCtW9e3e54YYbZMSIERIbG3ss+wUAAIDK7GPXtWtXeemll+SFF14wNXQ6n5w2zyYkJEi3bt3MTwAAAITR4AmdAVmDnN4AAABgDVy9HQAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAACq26jY7777rlwv0LJlSzOxMQAAACwS7IYPHx70xh0Oh9xxxx0yevTooJ8LAACASgp2mzdvDnLTAAAAqEr0sQMAAKiOV57Iz8+XgoICc31Yl7S0NJk5c6YcPHhQzj33XOnXr19l7CcAAAAqMtiNHDlSoqOjzbVi1f79++Wkk06S7Oxsady4sUyfPl0+/PBDOfvss4PZLAAAAKq6KXbx4sVy8cUXu++//vrrpgbv999/l9WrV8uYMWPk8ccfr4j9AgAAQGUGu+3bt8txxx3nvv/VV1+ZoBcfH+8eObt+/fpg9wEAAABVHexiY2Pl8OHD7vvLli2T3r17ez1+4MCBitgvAAAAVGaw69atm7zxxhvm90WLFpmBE6effrr78U2bNkmTJk2C3QcAAABU9eCJCRMmyNChQ+Wdd96RnTt3yogRI8ygCZe5c+fKKaecUhH7BQAAgMoMdgMGDJCVK1fK/PnzpVGjRjJs2DC/Gr1evXoFuw8AAACo6mCnjj/+eHMLZNSoURWxTwAAACgHrjwBAABQ3WrsWrVqJQ6HI+gXuOOOO2T06NFBPw8AAACVFOxmz54t5dGyZctyPQ8AAACVFOx04AQAAACsiz52AAAANkGwAwAAsAmCHQAAgE0Q7AAAAGyCYAcAAGATBDsAAACbINgBAADYRFgGu5ycHOnWrZu5EsaqVau8HluzZo3069dPYmNjJSUlRaZNmxay/QQAAKhKYRns7r33XmnSpInf8qysLBk8eLC0aNFCVq5cKY8//rhMmjRJXnzxxZDsJwAAgCWvPGEVn3/+ucyfP1/ef/9987unN998U3Jzc+WVV16R6Oho6dSpk6nRe/LJJ2XUqFEh22cAAICqEFY1dmlpaTJy5Eh54403pFatWn6PL126VPr3729CncuQIUPk119/lb1791bx3gIAAFStsKmxczqdMmLECLnpppvkxBNPlC1btvitk5qaKq1atfJalpyc7H6sfv36Afvr6c2zOVcVFhaaGwLTc6NlwjmyDsrEWigPa6E8rIXyCE4w/9aGPNiNHTtWpk6dWuo6GzZsMM2v+/fvl3HjxlXo60+ZMkUmT57stzwjI0Oys7Mr9LXs9keWmZlpwl1ERFhV/NoWZWItlIe1UB7WQnkER/NP2AS7u+66y9TElaZ169aycOFC09QaExPj9ZjW3l111VXy2muvSaNGjUxzrSfXfX0sEA2KY8aM8aqx09G0iYmJEhcXdwxHZv83pY5K1vNEsLMGysRaKA9roTyshfIIjs70ETbBToOB3o7m6aefln/+85/u+zt27DD95+bMmSO9e/c2y/r06SPjx4+XvLw8iYqKMssWLFgg7du3D9gMqzQo+oZFpWGFwFI6DXacJ2uhTKyF8rAWysNaKI+yCyaPhDzYlVXz5s297tepU8f8bNOmjTRr1sz8fuWVV5pm1euvv17uu+8+WbdunTz11FMyffr0kOwzAABAVQqbYFcW8fHxpi/eLbfcIj179pSEhASZMGECU50AAIBqIWyDXcuWLU3HfV9dunSRRYsWhWSfAAAAQonhjAAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsIqyCXcuWLcXhcHjdHnvsMa911qxZI/369ZPY2FhJSUmRadOmhWx/AQAAqlINCTMPPfSQjBw50n2/bt267t+zsrJk8ODBMmjQIJk5c6asXbtWrrvuOqlXr56MGjUqRHsMAABQNcIu2GmQa9SoUcDH3nzzTcnNzZVXXnlFoqOjpVOnTrJq1Sp58sknCXYAAMD2wqopVmnTa8OGDaV79+7y+OOPS35+vvuxpUuXSv/+/U2ocxkyZIj8+uuvsnfv3hDtMQAAQNUIqxq70aNHS48ePaRBgwayZMkSGTdunOzcudPUyKnU1FRp1aqV13OSk5Pdj9WvX99vmzk5Oebm2ZyrCgsLzQ2B6blxOp2cIwuhTKyF8rAWysNaKI/gBJNHQh7sxo4dK1OnTi11nQ0bNkiHDh1kzJgx7mVdunQxNXM33nijTJkyRWJiYsr1+vrcyZMn+y3PyMiQ7Ozscm2zuvyRZWZmmnAXERF2Fb+2RJlYC+VhLZSHtVAewdm/f3/4BLu77rpLRowYUeo6rVu3Dri8d+/epil2y5Yt0r59e9P3Li0tzWsd1/2S+uVprZ9nYNQaOx1Nm5iYKHFxceU4ourzptRRyXqeCHbWQJlYC+VhLZSHtVAewdGZPsIm2Gkw0Ft56MAIDRVJSUnmfp8+fWT8+PGSl5cnUVFRZtmCBQtM6AvUDKu0pi9QbZ9ul8BSOg12nCdroUyshfKwFsrDWiiPsgsmj4RNG5oOjJgxY4asXr1a/vjjDzMC9s4775Srr77aHdquvPJK0zx7/fXXy/r162XOnDny1FNPedXIAQAA2FXIa+zKSmvV3n77bZk0aZIZ7KCDJDTYeYa2+Ph4mT9/vtxyyy3Ss2dPSUhIkAkTJjDVCQAAqBbCJtjpaNhly5YddT0dVLFo0aIq2ScAAAArCZumWAAAAJSOYAcAAGATBDsAAACbINgBAADYBMEOAADAJsJmVCwAHE1BQYGZoNwqM+vrvuilCa002blO3h4ZGRnq3QBQSQh2AMKeXrM4NTVV9u3bJ1baJw13eo1HnWHfSurVq2cus2i1/QJw7Ah2AMKeK9Tp5QVr1aplicCiwU6vZV2jRg1L7I9rnw4dOiTp6enmfuPGjUO9SwAqGMEOQNg3v7pCXcOGDcUqrBjsVM2aNc1PDXd6zmiWBezFOh0/AKAcXH3qtKYOZeM6V1bpjwig4hDsANiClWrFrI5zBdgXwQ4AAMAmCHYAEEIZGRly8803S/PmzSUmJsaMVh0yZIgsXrzYPP7iiy/KwIEDJS4uztS0BRr5+8gjj0jfvn1NE6uOeAVQhTK3i2z+ruinBTB4AgBC6OKLL5bc3Fx57bXXpHXr1pKWliZfffWV7N692zyuo1jPOusscxs3blzAbejzhw0bJn369JGXX365io8AqEbyc0Ry9otkZ4rkZImse19kyf/pcCkRR4TIuU+J9LgmpLtIsAOAENHat0WLFsk333wjAwYMMMtatGghvXr1cq9zxx13mJ+6TkkmT55sfs6ePbvS9xkIS4WFIrkHikKZBrLsrOLfMz1+L/5p7hff3L8XLy/IKfk1nIUiH98h0uYMkfimEioEOwDwsDPzsGzedVBaJdSWxvFFU4NUljp16pjbvHnz5OSTTzZNsUB1EXEgVeTgryIJbUsPQvm5xSErs+wBLMfz9+LnaK1aRYmuIxIZLXJ4j/dyZ4HInj8IdgBQ0XPIHc4rCPp576/8SyZ+tF4KnSIRDpHJ53WSi3s2C2obNaMiyzzqVOe401q2kSNHysyZM6VHjx6m5u7yyy+XLl26BL3/gOU4nSJ5h0UO7xXJ3lf0U2+/fCaJq/8jDhO2HCLNeonUSfSpTcs6ei1ZsCJqiMTEicTGicTUFYmJL/pp7sf5/O65ns/vEZFFfepmnFBUU+fiiBRp0FpCiRo7ALajoa7jhC+OaRsa7h78cL25BePnh4ZIregaQfWxO+ecc0yT7LJly+Tzzz+XadOmyaxZs2TEiBHl2HOgEhTkF9WY+Qa0w/t8lnk85lpWkHuUkZtOkb+Wl62WLFDI8gxiMYFCmkd4qxGr8/1UzDnRWkbtU6fNr1pTp6Hu3Bkhra1TBDsACLHY2Fg588wzze3BBx+UG264QSZOnEiwgz+tJdqzSaRBm+ADhNaeaT8zvzBWWkDbV7TMNGUeY01ZzfoisfWKAtCuX/zXOfkfIk16lFB7VreolsxqelxT1KdOm1+1pi7EoU4R7ADYjjaHas1ZMFIzs2XQk9+amjoXbY79cswAaRQfG9RrH6uOHTuafneAlx9fF/n49qKmPx2BOfB+kVb9S68t811WmH9sJ1VDVs16RQFNg5q5Ff/uXuZ7v75IdO0jNWWZ28U54wRx+DZh9rnVEsEoaLrPFtpvgh0A29E+bsE0h6rWiXVkykWd5f4P1kmB0ymRDoc8etEJZnll0SlNdJqS6667zvSpq1u3rvzwww+mKfb8888366Smpprbxo0bzf21a9ea9XTeuwYNGphl27Ztkz179pifeu3cVatWmeVt27Y1gzNgcbmHRA7tEjm4S+TQ7uKfuzx+7hbJ2iGSuvrIczQUff1Pka/L8Xra6d8dykoLZB7BTJfFxotEVkBsiG8qzr/NEPnkjqJwZ5EmTLsg2AFAsctOai792yXKll2HpGVCrSoZFdu7d2+ZPn26bNq0yVy7NSUlxQymuP/++806OqjCNZ2J6t+/v/n56quvuptqJ0yYYObBc+nevbv5+fXXX5vJjVGFXM2dpYU08zPjyO95h8r/enUaicQ1PnqNmeeyqJoV18+svLr/XTLiu0pCRJZENCxHszJK5HDq8DG4ZWVlSXx8vGRmZpqZ3hFYYWGhpKenS1JSkkREcAETK6iuZZKdnS2bN2+WVq1amb5qVqEfrfn5+Wbkq9WuzWrVc1aZCvf9Kfs2rZR6bXpKRL2Usj9R/4nUQQMmpGX4BLXdge+XZxSn1qLVShCp3VCkdmLx7wkitfR+QtHI0U+0k75P8+Uda8MyFFXXz6uqyCbU2AEA7O3H18Xx8e3SwFkoTu2bNvgRkbZnlC2k6c/CvOBfM6rWkaDmG9Lc9z0e18EBR/sCoI9bbAQmrIdgBwAIfzpX2v7UoptOfLs/rejn7s0iG+ZpfZdh+nR9MU4k2Nlwouv6hLSE0u/rYIFqMAIT1kOwAwBYl05UqyFt/06RA2newc29LK3o0lDBiKotUrdRKSHN43etaYuySJO1xUZgwnoIdgCAqmX6re0LENiKf3qGt7yDZd9ujZoidZOLBhToz7qNi5YtnuF9OSltxrz1ewISbIlgBwComAlx9ULreu1Mr3BWQmALZoCBNoN6Bjbzs/hWJ/nIT52OI1A/tYatxfmxTq1RIE5HpDjomwYbI9gBAMo2IW6/u0Sanujdh82z1k1vwUyAq0FMa9U8w5k7sHkEt5hjnIuvxzXibH2a7N30o9Rr00McwYyKBcIMwQ4AqnuzqF6VIPOv4tufRbddv4v89l+P9QpFvnu8bNvUPmkl1appkDO1bslF86lVlbimkts0SiQuqepeEwgBgh0A2JkGsoK84ltu0e3wIZEDGSJv3ieStrJoQt2yqt9aJOG4AE2jxYGtdpJIjejKPCIApSDYAUA417bpnGaeoU1v+R73A83Blu8UyT8ssvePI6FOR3/GNxPRZsr4lKJ51b6d5j/oYMQnDDoALIxgBwBWDm6+oc33vueVCErkEImMKrq6gd4KHCK1CkTOe1akQfH0GYGaRTXoMSEuEFYIdgBQGQryxJF7UMRRu+SmycICyUjdLhMmTZZPP/9C0tIzpH69OOnasYNMuHOUHN8mRSb+a6bM/3aZbNuRKokN6ssFZw2Uh++5WeLj6ro3M3rCE7L4+1Wy7peNcny7trJq6cIjIU5vETW8R4tmZ4tEHxRp3lmktEuKMSEuEHYIdgBQ0fRSVJl/mg9Yp2swQWSMf82bs0Auvuh6yc3Nk9eenCCtWzSVtIw98tX/VsjuPbtlR51Y2ZGWIU9Mukc6duggW3eky013PSA7dh+Q995+szi4RZntXzfqH7J8+XJZs2ZN0cCEisKEuEBYIdgBQHnnbTNNpdqnLafopnOz6aWtcg8cuYSV/k+vNxrAvsz9smj5T/LN3NdkQL9TTc1ei45R0uv0v7lr294/7RJ3bVsbEXkkt4ZcffXVkl+jltSoUfQR/vTTT5ufGRkZRcEOQLUVdsHu008/lYceesh8eMXGxsqAAQNk3rx57se3bdsmN998s3z99ddSp04dGT58uEyZMsX9AQigGtDAlXco+Oetekvk83uPzNs2dJpI1yuKw1txgCvMOfK7LvccXKBqxAaeJFcn2Y2u5dVEWifBYT6n5n37k5w89DKJiYk56i5mZmZKXFwcn2kAAgqrtPP+++/LyJEj5dFHH5XTTz9d8vPzZd26de7HCwoK5JxzzpFGjRrJkiVLZOfOnXLNNddIVFSUeQ6AakJD3aNNjm0bGu4+u7voFozRP4lkB7huab3mfn3t9AN49uzZ5nNt5syZ0qNHD/Nl9fLLL5cuXbr4bWLXrl3y8MMPy6hRo4I+HADVQ4SECQ1xt99+uzz++ONy0003Sbt27aRjx45y6aWXuteZP3++/Pzzz/Lvf/9bunXrJkOHDjUfgs8++6zk5uo3awCoZNq/LT7FXY9nfur0ISUMoLj44otlx44d8tFHH8lZZ50l33zzjQl4Gvg8ZWVlmS+u+rk3adIkihFAeNfY/fjjj7J9+3aJiIiQ7t27S2pqqglvGvROOOEEs87SpUulc+fOkpx8pOPwkCFDTNPs+vXrzfMA2Jjp86ZNpPkio9ccvdnUd8DDu8O9pw/R5tibFhfVtgVqXg0kqpZIdG2RmDjJzzkkkTG1jjphr3YrOfPMM83twQcflBtuuEEmTpwoI0aMMI/v37/fhL66devK3LlzTSsEAIR1sPvjjz/MT/2m+uSTT0rLli3lX//6lwwcOFB+++03adCggQl7nqFOue7rY4Hk5OSYm+e3YlVYWGhuCEzPjdPp5BxZiG3KJGu7yO4/zIXb9TJQfnQ0qV7yavcmkb2bxZmZLtJgoDh3FYgzIl8cgcJbRIRIRKw4dShDjZiiEar60/17cb+3v80Q+eRO98Xi5W/TRZKOL1/AjIySwqhaEhkZZcolGMcff7zpO6zP088kDXXa/+7DDz80P0vanmv50V5PH3f9rYT930t1e3/YBOURnGD+bkMe7MaOHStTp04tdZ0NGza4D2r8+PGm6UK9+uqr0qxZM3n33XflxhtvLNfr68CKyZMn+y3X0WXZOtcTAtLy0E7c+kGptagIPTuUSc0N70rcdxPE4SwUp0TIwc5XS2FcikRmbpMamVvMz8gD28XhcaF5R50UkVN6i6MwVxwRGusc4jSDE2LMz6Lf9WeM/3xuLpqD8vNFulwp0nKgOPb+IU69dFZck6Ll5aDloP1+zT6WUNu3e/duueKKK8wgL21t0Bq5lStXmpaIc889V/bs2SNnn322HDp0yHze6X29qcTERImMjDS/b9y4UQ4cOGD6Feu6P/zwg1muzbbR0dEBu7bo34u+fnWp/bPD+8NOKI/gaK192AS7u+66y93cUJLWrVubDyzXB5WLfnPVx3QkrNJBEytWrPB6blpamvuxQMaNGydjxoxx39dvxykpKeZDU0eeoeQ3pf5jpeeJD0lrCMsy0do3nVokfYM4/lwhsmKmxzQhhVJn7esBn+bUkacNWpkpSZwJncVZs4E441uIs3ZdU1PmNdVIsBo0L7pVkNKCU7169aR3797yzDPPyKZNmyQvL898/mhT7P3332/mpXN9pmktnm8rhrZcKO1u8u2337of69Wrl986nnSWAP0badiwoWkGrg7C8v1hY5RHcIJ5n4Y82OmbTG9H07NnTxPkfv31Vzn11FPNMv0Q3LJli7Ro0cLc79OnjzzyyCOSnp4uSUlJZtmCBQtMQPMMhJ50m4GmGNA3Pm/+0umHJOfJWixbJto0qM2naT+LpOttQ9HPXb8V930rRcrJIs17izRoXTS3XIPW4tALzhcfoyM7WxybN4sjprY4LHTxea0ZctXUlVRjpx/Wjz32mLkFctppp5WpGVcHXARD98eyfyuVqDoes5VRHmUXzN9syINdWWk409Gw2qFYv9FqmNPmCjVs2DDzc/DgwSbA/f3vf5dp06aZfnUPPPCA3HLLLWWaHwpABdBBCBravELcBpHcEpoSomoX9WOr30Jk3Qf+F52/5BUuOg8Adgt2SoOcNiFocDt8+LBpwli4cKHUr1/fPK79TT755BPTLKG1d7Vr1zZ9V3RCYwAVLOeASMYv/iHuYHrg9SOiRBLaFYW45I4iScU3nQrE9W209UAuOg8A1SXYaV+VJ554wtxKojV5n332WZXuF2BrOl3I7o3Fwc0jxO3bWsITHCL1WxYHN48Q17Bt0XVNS8NF5wGg+gQ7AJVIR55rWEv3qH3TELf7dxGPUah+k/G6at5cIS6xQ9E8buXFRecBoNwIdkB1o53xD6R7BDhXP7hfRPIOBn5OTFxRcDO3TsU/O4rUbljVew8AKAXBDrCLrO0SvX2lSGxPkXopRcuys46MQPUcjXpod+Bt6Jxvie2P1MC5Qlx8s7JfeQEAEDIEO8AOfpgtjk/vlAZmYl9HURjL2V80xUhAjqLpQ9yDGIpDnC6L5GMBAMIVn+BAODalal84rZ3b/qPI1iUiO370mJTXWVQr51K3ic9I1ONFEtqLRNcK1REAACoJwQ6wuoO7TXArCnLFt5KaUj2dNVWky6UitRpUxV4CACyAYAdYSe4hkdQ13iFu75bAc8I16izStGdR8+n88SLOQu+JfY8/l1AHANUMwQ4IlcKCogl+PUOcTi/iLLpwvJeGxxWFONet0QkiNTyuphJTR5wf3yEOZ4E4HZHiOHcGV2sIExkZGTJhwgT59NNPzbWtdcL1rl27mmWnnHKK3HjjjfLll1/Kjh07pE6dOtK3b1+ZOnWqdOjQwb2N0aNHy+LFi2XdunXmmrKrVq0K6TEBCB2CHVCV10p1h7gfRXasCjy9iM4N1/REkaY9ikJck+4iNeuVvv0e14iz9Wmyd9OPUq9ND3G4RsXC8i6++GLJzc2V1157TVq3bm3C3VdffSW7d+92Xyf7qquukubNm8uePXtk0qRJ5vKJmzdvNlfbcbnuuutk+fLlsmbNmhAeDYBQI9gBleHQnuJ+cR594w5m+K8XXacouHnWxsU1Kd/UInFNJbdplEhcUoUcQnWVejBVtmVtk+ZxzaVR7UaV+lr79u2TRYsWyTfffCMDBgxwXz2nV69e7nVGjRrl/r1ly5byz3/+09TobdmyRdq0aWOWP/300+7aP4IdUL0R7IBjlXdYJHWtd5Pqnj/814uoIZJ8gneISzhOJOJIrQsqhtPplMP5h4N+3kebPpIpy6dIoRRKhETIuN7j5Lw25wW1jZo1aoqjjMFcm1b1Nm/ePDn55JMlJsajeT2AgwcPyquvviqtWrWSlBRqZQH4I9gBQfeL+7UovLlGqqatD3zJrQZtfPrFdRaJiuV8VwENdb3f6n1M29Bw98jyR8wtGMuvXC61oso2lUyNGjVk9uzZMnLkSJk5c6b06NHD1Nxdfvnl0qVLF/d6zz33nNx7770m2LVv314WLFgg0dHRQR8TAPsj2AGZ20X2bCoKYnqdUq9+cX9594vbuUok94D/OaudWNwvTkNcj6LmVaYZQRn72J1zzjmmSXbZsmXy+eefy7Rp02TWrFkyYsQIs472sTvzzDNl586d8sQTT8ill15qBkvExvJFAYA3gh2qtx9fF/n49qKpQhwRIr1vEqnZwKNfXLr/c6JqF/eLKx7coDcuuWUp2hyqNWfBSDuUJhfMu8DU1LlEOCJk3vnzJLlWclCvHSwNaBrc9Pbggw/KDTfcIBMnTnQHu/j4eHM77rjjTJOtjpydO3euXHHFFUG/FgB7I9ihetLauC2LRD4arXeKlxWKLHvOez2dDy65k3eTql5LlX5xlqZ93MraHOrSKr6VTOw7USYvnSyFzkIT6ib2mWiWV7WOHTuafncl9R/UW05OTpXvFwDrI9iheijIL5r4d9vSoktw6c+Srt7Qsr9I+6FH+sVx6a1q46LjLpK+TfrKn/v/lJS6KZU+KlanNBk2bJiZqkT71NWtW1d++OEH0xR7/vnnyx9//CFz5swx05skJibKX3/9JY899pjUrFlTzj77bPd2Nm7cKAcOHJDU1FQ5fPiwex47DYj0xQOqF4Id7DtSVZtSty4V2bZE5M8V/n3jImNECnL8a+gunMnkvtWYhrnKDnQuOiK2d+/eMn36dNm0aZPk5eWZ0a46mOL++++XvXv3mr53M2bMML8nJydL//79ZcmSJZKUdGRaG226/fbbb933u3fvbn7qXHc6RQqA6oNgB3vIziwKb1sXF4U5HbFakOu9Tky8SPOTRVr0EWneV6RJN5E1c0Q+vqPoag8a6rhiA6qQTm8yZcoUcwtEa+Y+++yzo25H58EDAEWwQ3g6kH6kSVXDnE454nmtVNcVHJr3EWlxSlGYS+ro3zeuxzUibc4omndOr7nqOSoWAIAwQ7BDeAx02LvlSP84ven0JL7qtxJp0bc4zPUtCmplmShWwxyBDgBgAwQ7WE9hoUjGL0U1cSbMLRXZv8NnJUfRaFUT4oqbVuMah2iHAQCwBoIdQq8gT2Tn6iO1cX8uEzm81/9yXDp3nKmR01tvkZr1Q7XHAABYEsEOVS/3kMhf3x9pWtXf8w55r6NzkDU7qSjI6U2v6sC0IwAAlIpgh8qntW/blh0Z7LDjJ/9rq2rtmzarugY7NO4iEhlF6QAAEASCHcot4kCqyMFfRRLaeg8+yNpZNHecaVpdKpL+85GrO7jUbVJcG1cc5BL0ag4RlAYAAMeAYIfy+ekNSfzkDnG4rrHa5fKiEag64EFHsPpq2PZI/zgNc/ValG3EKgAAKDOCHYIbrbpvq8jm78Tx8e3i8LzG6uq3jqynQS/5hCPzx2nzap0js+QDAIDKQbBDYIf3FTWh6sS/aetE0n4uul98Wa6AdW0nDBPperlIykkisfGcWQAAqhidmqq7gnyRjF9F1r0v8tVDIm9dJjL9BJGpLUReHSry2d0iK2eL/FV8rdXIaJHEDr495ooux3XmZJHjBhHqgCBkZGTIzTffLM2bNzeXGGvUqJEMGTJEFi9e7LWe0+mUoUOHisPhkHnz5nk9Nnr0aOnZs6d5frdu3Tj/QDVGjV11ciCjuPZt/ZGaOA11BTmB149PKZoE2HVL6lTUVy6yhjhXvibi7mPHNVaB8rr44oslNzdXXnvtNWndurWkpaXJV199Jbt37/Zab8aMGSbUleS6666T5cuXy5o1aygMoBoj2NlRfk5RYHM3oxYHuYPpgdePqi2S3LE4wJ1QHOI6itSsV/JrdP+7ZMR3lYSILIlo2IZLcsE28lJTJXfLVolu2UKiGjWq1Nfat2+fLFq0SL755hsZMGCAWdaiRQvp1auX13qrVq2Sf/3rX/LDDz9I48b+V1h5+umn3bV/BDugeiPYhfs1VLO2+we4Xb+LOAsCPMFRdP1UzwCnNx2hWo6pRgrrNBJJ6sI0JbAcbbZ0Hj4c9PP2zZsnaf98pGigUESEJD8wXupdcEFQ23DUrFlqzZqnOnXqmJs2rZ588smmKdXXoUOH5Morr5Rnn33WNNMCQGkIduEi50DR9VN9m1KzMwOvH1vPO7zp70kdRKJrV/WeA1VOQ92vPXoe20YKCyXtoYfNLRjtf1wpjlq1yrRujRo1ZPbs2TJy5EiZOXOm9OjRw9TcXX755dKlSxezzp133il9+/aV888/v1yHAaB6IdiFQuZ2kT2bRBoEaMLUmoK9m73Dm/7UZYHoNVQT2nkHOP1ZtzHzxAFh0sfunHPOMU2yy5Ytk88//1ymTZsms2bNkgYNGsjChQvlp59+CvVuAggTBLuq9uPrIh/fXjT3m8731ufWokEKrgCnU4r4XjfVRZs+TXjreCTAaair4d98A1Rn2hyqNWfByEtLkz/O+VvRlyuXiAhp/eknEpWcHNRrBys2NlbOPPNMc3vwwQflhhtukIkTJ8qFF14omzZtknr16vmFwX79+pm+eQDgiWBX1TV1rlCn9OeSpwOUSqyZUsS7KbWTSO2EKt1dIFxpH7eyNoe6xLRqJY0fmiw7J0x097HT+7q8qnXs2NH0uxs7dqwJeZ46d+4s06dPl3PPPbfK9wuA9YVNsNNvpqeddlrAx1asWCEnnXSS+V1HhN1yyy3y/fffS2Jiotx2221y7733iiVo86sr1HlK6S3Sst+RplQd4BAZNkUD2Ea9Sy6R2qeeKrlbt0l0i+aVPipWpzQZNmyYmapE+9TVrVvXjHzVpljtU6eDJQINmNA571p5BM6NGzfKgQMHJDU1VQ4fPmxG0boCYnR0dKUeAwBrCZv0oJ2Hd+7c6bVMmyx0vqcTTzzR3M/KypLBgwfLoEGDTEfktWvXmg9MbcYYNWqUhJz2qdPmV89wp3PAXfIq04UAFqFhrrIDnYuOiO3du7epgdMm17y8PElJSTGDKe6///4yb0dr9b799lv3/e7du5ufmzdvlpYtW1bKvgOwprAJdvqt0/Obq34Afvjhh6ZGzjW1wJtvvmkm+nzllVfM+p06dTLfXJ988klrBDsdKHHuUyIf31E0HQkT+wLVmk5vMmXKFHMLZioXX/S1AxB2wc7XRx99ZJoxrr32WveypUuXSv/+/b2aHvTSPFOnTpW9e/dK/fr1/baTk5Njbi5a66cKCwvNrcJ1u1qk9WkiezaLNGglEtfUu7N2mNBzo//AVMo5QrlU1zJxHbfrZiWu/bHifrn+VqrL30t1fX9YFeURnGD+bsM22L388ssmtDVr1sy9TPuXePY7UcnFo9n0sUDBTr8pT5482W+5zuCenZ1dKfsuEiVSq52Ibj67hKtBhMEfWWZmpvmgjCjH5MaoeNW1TLT2Xo89Pz/f3KxCy6GgoGii8LJOWFxV9DzpOdMvx1FRUVIdVNf3h1VRHsHZv39/+AQ7HfWlNWql2bBhg3To0MF9/6+//pIvvvhC3nnnnWN+/XHjxsmYMWO8auy0j4sOvIiLizvm7dv5Tan/WOl54kPSGqprmegXMP3Q08l+9WY1VgxOep70b6Rhw4ZmqpXqoLq+P6yK8ghOMO/TkH8K3nXXXTJixIhS19ELY3t69dVXzQfSeeed57Vc++DpBbQ9ue6XdCke7eMS6DI++sbnzV86/ZDkPFlLdSwTPVYzvUnxzSq0Zsi1P1baL+U6V9Xtb6U6HrOVUR5lF8zfbMiDnX570lswH5Ya7K655hq/b8J9+vSR8ePHm6YZ12MLFiyQ9u3bB2yGBQAAsJOw+9qil9fRIfy+k3YqvVC2Dpy4/vrrZf369TJnzhx56qmnvJpaAdiT1QYoWBnnCrCvkNfYlWfQhM5p59nnziU+Pl7mz59vJiju2bOnJCQkyIQJE6wx1QmASuGqnT906JDULMflvKojPVdW7f8HoJoFu7feeqvUx3X2dr2YNoDqITIy0kxCnp5eNMK8Vq1alujTprViOvpUBypYYX9c+6ShTs+VnjM9dwDsJeyCHQD4cg2OcoU7K3DNmeYa3GElGupKGlAGILwR7ACEPQ1OjRs3lqSkJDN4ygpc88TpCH4rjcLU5ldq6gD7ItgBsA0NLFYJLRrsNETp/FNWCnYA7I1PGwAAAJsg2AEAANgEwQ4AAMAm6GNXwsSdes1YlN5/SK/PSf8h66BMrIXysBbKw1ooj+C4MklZJhcn2PnQsKJSUlKCPO0AAACVm1H0YgylcTi5tozft4gdO3ZI3bp1LTf3lNW+PWj4/fPPPyUuLi7UuwPKxHJ4j1gL5WEtlEdwNKppqGvSpMlRR9lTY+dDT1izZs2CPOXVl4Y6gp21UCbWQnlYC+VhLZRH2R2tps6FwRMAAAA2QbADAACwCYIdyiUmJkYmTpxofsIaKBNroTyshfKwFsqj8jB4AgAAwCaosQMAALAJgh0AAIBNEOwAAABsgmBXDX333Xdy7rnnmokOdRLmefPmlbr+N998Y9bzvaWmpgZc/7HHHjOP33HHHX6PLV26VE4//XSpXbu2mb+of//+cvjwYanOQlUeuv7f//53adSokSmPHj16yPvvv1+hxxauKqNMJk2a5Pd4hw4dvLaTnZ0tt9xyizRs2FDq1KkjF198saSlpUl1F4ry2LNnj9x2223Svn17qVmzpjRv3lxGjx4tmZmZUt2F6v3hOVnv0KFDy/Ta1RETFFdDBw8elK5du8p1110nF110UZmf9+uvv3pNRpyUlOS3zvfffy8vvPCCdOnSJWCoO+uss2TcuHHyzDPPSI0aNWT16tVHnUXb7kJVHtdcc43s27dPPvroI0lISJC33npLLr30Uvnhhx+ke/fuUp1VVpl06tRJvvzyS/d9fQ94uvPOO+XTTz+Vd99910xGeuutt5rXX7x4sVRnoSgPvQKR3p544gnp2LGjbN26VW666Saz7L333pPqLFTvD5cZM2ZwZajS6CXFUH3pn8DcuXNLXefrr7826+3du7fU9fbv3+887rjjnAsWLHAOGDDAefvtt3s93rt3b+cDDzxQIfttV1VZHrVr13a+/vrrXssaNGjgfOmll47hCOynospk4sSJzq5du5b4+L59+5xRUVHOd999171sw4YNZrtLly4t597bT1WVRyDvvPOOMzo62pmXlxfU8+ysqsvjp59+cjZt2tS5c+fOMr12dVS9q0oQlG7duknjxo3lzDPPDFiDoE1I55xzjgwaNMjvsfT0dFm+fLn5hta3b19JTk6WAQMGyP/+9z9KIQTlobQc5syZY5qc9BrJb7/9tmkKHDhwIGVSSWXy+++/m+ar1q1by1VXXSXbtm1zP7Zy5UrJy8vzKi9titImQK3tRtWWRyDaDKs1TiXVJKFyy+PQoUNy5ZVXyrPPPmu6kCAwgh2OSt+IM2fONP2v9JaSkmL+8f/xxx/d62go0PtTpkwJuI0//vjD3Y9i5MiR8t///tf06TrjjDPMmxlVWx7qnXfeMUFC+3PpZKE33nijzJ07V9q2bUtxVEKZ9O7dW2bPnm3+9p9//nnZvHmz9OvXz1zYW2l/o+joaKlXr57XtvVLUEn9J1F55eFr165d8vDDD8uoUaM47SEqD+2qoF9Izz//fMqgNKGuMkRolbcqu3///s6rr77a/L5t2zZnUlKSc/Xq1e7HfZv+Fi9ebF5r3LhxXtvp3Lmzc+zYscd0DHZSVeWhbr31VmevXr2cX375pXPVqlXOSZMmOePj451r1qypgCOxj4ook0C0WSouLs45a9Ysc//NN980zXy+TjrpJOe9994b9OvbVVWVh6fMzEzzXjnrrLOcubm5Qb+2nVVVeXz44YfOtm3bmi4mx/radkeNHcqlV69esnHjRncTkja1ag2cNlHo7dtvv5Wnn37a/F5QUGC+sSnthOzp+OOPP2rzByq+PDZt2iT/93//J6+88oqpNdWO0HqJuBNPPNE0c6BiyyQQrZlr166dex1tWsrNzTUDWjzpqFianaq+PFy0xkgHfdWtW9fUaEdFRVXA3iDY8li4cKH53NLlrs81pSPH6T7ijWCHclm1apU7rGkwWLt2rVnmumlA0D4S+ntkZKS0bNnS9J3QUVGefvvtN2nRogWlUMXloX1VlO+IZH1M+9uhYsskkAMHDph/qFzr9OzZ04SGr776yr2Ovl/0i0+fPn0okiouD5WVlSWDBw82TeQ6ejw2NpZyCFF5jB07VtasWeP1uaamT58ur776KuXigR6g1ZC+YTy/KWlfBn2TNGjQwHTU1ulItm/fLq+//rp7aHmrVq3MUHTtXD9r1izz7Wn+/Pnmcf0me8IJJ3i9hs6Lpn23XMt1vqF77rnH1App7ZB2on3ttdfkl19+qfZTB4SiPLRTvval0351Op2DPqbzQS1YsEA++eQTqe4qukzU3Xffbeb+0i8yOmWGvhc0SF9xxRXmcZ3e5Prrr5cxY8aY19FO+jqPmoa6k08+WaqzUJSHK9Tpl6B///vf5r7eVGJiolm3ugpFeWitdaCaa3093TY8hLotGFXPNfTc9zZ8+HDzuP7UPlkuU6dOdbZp08YZGxtrpsMYOHCgc+HChaW+RqA+XWrKlCnOZs2aOWvVquXs06ePc9GiRc7qLlTl8dtvvzkvuugi0x9Py6NLly5+059UV5VRJpdddpmzcePGph+dTteg9zdu3Oi1zuHDh53/+Mc/nPXr1zdlcuGFF5ppHaq7UJRHSa+pt82bNzurs1C9P3zRxy4wh/7PM+gBAAAgPNHHDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgCKTZo0yVzuzir0Unx6qbdg6TVm9fJLegH7yrRr1y5JSkqSv/76q1JfB0DZEewAVKmZM2ea69nm5+d7XXsyKipKBg4c6LXuN998Y8KNXgzczio6UOq1OvU6s3qeK1NCQoJcc8015rqeAKyBYAegSp122mkmyP3www/uZYsWLTI1TMuXLzcXCXf5+uuvzUW+27RpQymV0bZt2+STTz6RESNGVMk5u/baa+XNN9+UPXv2VMnrASgdwQ5AlWrfvr00btzY1Ma56O/nn3++tGrVSpYtW+a1XIOgeuONN+TEE080tVAaAq+88kpJT083jxUWFkqzZs3k+eef93qtn376SSIiImTr1q3m/r59++SGG26QxMREiYuLk9NPP11Wr15d6v7OmjVLjj/+eImNjZUOHTrIc889535sy5Ytpkbxgw8+MPtZq1Yt6dq1qyxdutRrGy+99JKkpKSYxy+88EJ58sknpV69euax2bNny+TJk81+6Lb0pss8mzv1Ofrc4447Tj766KNS9/edd94x+9C0adNSawRnzJghLVu2dN/XIHjBBRfIo48+KsnJyWb/HnroIVOzes8990iDBg3MOX711Ve9ttOpUydp0qSJzJ07t9T9AlA1CHYAqpyGIK2Nc9HftRl2wIAB7uWHDx82NXiuYJeXlycPP/ywCUDa70xDlatWSsPbFVdcIW+99ZbX62hN0imnnCItWrQw94cNG2bC4Oeffy4rV66UHj16yBlnnFFibZM+f8KECfLII4/Ihg0bTOh58MEH5bXXXvNab/z48XL33XfLqlWrpF27dmZfXE3Nixcvlptuukluv/128/iZZ55ptudy2WWXyV133WUC0s6dO81Nl7lo6Lv00ktlzZo1cvbZZ8tVV11Vau2Y1n5qAC6PhQsXyo4dO+S7774z4VObWP/2t79J/fr1TVnocdx4441+fep69eplXheABTgBoIq99NJLztq1azvz8vKcWVlZzho1ajjT09Odb731lrN///5mna+++sqpH1Fbt24NuI3vv//ePL5//35z/6effnI6HA73+gUFBc6mTZs6n3/+eXN/0aJFzri4OGd2drbXdtq0aeN84YUXzO8TJ050du3a1esx3SdPDz/8sLNPnz7m982bN5t9mDVrlvvx9evXm2UbNmww9y+77DLnOeec47WNq666yhkfH+++7/u6LrqdBx54wH3/wIEDZtnnn39e4rnV7Tz00ENeywJtf/r06c4WLVq47w8fPtzc1/Pm0r59e2e/fv3c9/Pz8025/ec///Ha1p133ukcOHBgifsEoOpQYwegymnt3MGDB+X77783NT1ay6XNo1pj5+pnp82wrVu3Nn3slNawnXvuuea+Nsfquq4+ZUqbGrXJ1FVr9+2335raOa2lU1rTp337GjZsKHXq1HHfNm/eHHBwhu6fLr/++uu91v/nP//pt36XLl3cv2szs3I1E+sIVa3R8uR7vzSe265du7ZpQnZtOxCt6dRm4/LQWkOt/XTRJtnOnTu770dGRprz5/v6NWvWlEOHDpXrNQFUrBoVvD0AOKq2bdua/lra7Lp37153SNO+WtoXbcmSJeYx7QPnCllDhgwxN20e1RCogU7v5+bmurerzZQa7MaOHWt+nnXWWSaIKA11vn37XFz93Tzp+q7+cb179/Z6TAOOJx3R66J95Fz9/iqC57Zd2y9t2zpSVc+pJw1rRRWAR2jTdlleqyyvr03DWiYAQo9gByAktO+chiwNIdo536V///6mD9yKFSvk5ptvNst++eUX2b17tzz22GMm+CnPUbUuOqDigQceMLV77733nplaxUX706WmpkqNGjW8Bg2URGurNGj+8ccfJjAey2ARrZn05Hs/OjpaCgoKpCJ0795dfv75Z69lGrr02DXcuYKn9verKOvWrfObqgZAaNAUCyBkwe5///ufCRiuGjulv7/wwgumJs41cEKbXzX8PPPMMyZo6chQHUjhSwNb3759TfOpBqXzzjvP/digQYOkT58+ZuTn/PnzzeALrRnUgQ+BQqJr4MKUKVPk6aeflt9++03Wrl1rRoXqwIKy0vnkPvvsM/Oc33//3RybBldXwHLttzYJ67nQUbA5OTlSXlqLqaNyPYOihq6MjAyZNm2aaUZ+9tlnzT5UBG2C1SA9ePDgCtkegGNDsAMQEhratD+YNstq7ZhnsNMrJrimRXHVOOkUIO+++6507NjR1Nw98cQTAbertWvan06nCNG+Xy4apDRgaY2gzr2m/fouv/xyMxWK5+t70qlRdLoTDXPa10z3TfdDp2UpKx2VqzWHGux0GpL//ve/cuedd3r1g7v44otNs7GeEz3W//znP1JeQ4cONbWSX375pXuZ9j3UaVo00Ok+aG2ojuKtCB9++KEJ3v369auQ7QE4Ng4dQXGM2wAABGHkyJGmebmypgjRAKe1ml988UWll8vJJ58so0ePNs3gAEKPPnYAUMm0dlHnr9NRrdoEqvPgeU50XNF0rjmdjFlrPivzsmLabHzRRReZefsAWAM1dgBQyXSCYR0ookFLp3DRfnc62S8AVDSCHQAAgE0weAIAAMAmCHYAAAA2QbADAACwCYIdAACATRDsAAAAbIJgBwAAYBMEOwAAAJsg2AEAANgEwQ4AAEDs4f8BuBVTU/w5FYMAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "result.plot(db=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "result.show_animation()" ] @@ -134,7 +221,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.12.10" } }, "nbformat": 4, diff --git a/nbs/meep_sbend.ipynb b/nbs/meep_sbend.ipynb new file mode 100644 index 00000000..462929c8 --- /dev/null +++ b/nbs/meep_sbend.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Running MEEP Simulations\n", + "\n", + "[MEEP](https://meep.readthedocs.io/) is an open-source FDTD electromagnetic simulator. This notebook demonstrates using the `gsim.meep` API to run an S-parameter simulation on a photonic Y-branch.\n", + "\n", + "**Requirements:**\n", + "\n", + "- UBC PDK: `uv pip install ubcpdk`\n", + "- [GDSFactory+](https://gdsfactory.com) account for cloud simulation" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "### Load a pcell from UBC PDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from ubcpdk import PDK, cells\n", + "\n", + "PDK.activate()\n", + "\n", + "c = cells.bend_s(size=(20.0, 5.0))\n", + "\n", + "c" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### Configure and run simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\"]\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, save_animation=True, verbose_interval=5.0)\n", + "sim.solver.stop_after_sources(time=100)\n", + "\n", + "print(sim.validate_config())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "sim.plot_2d(slices=\"xyz\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### Run simulation on cloud" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Run on GDSFactory+ cloud\n", + "result = sim.run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "result.plot(db=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "result.show_animation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1de30efa", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gsim", + "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.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbs/meep_straight.ipynb b/nbs/meep_straight.ipynb new file mode 100644 index 00000000..b26f5694 --- /dev/null +++ b/nbs/meep_straight.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Running MEEP Simulations\n", + "\n", + "[MEEP](https://meep.readthedocs.io/) is an open-source FDTD electromagnetic simulator. This notebook demonstrates using the `gsim.meep` API to run an S-parameter simulation on a photonic Y-branch.\n", + "\n", + "**Requirements:**\n", + "\n", + "- UBC PDK: `uv pip install ubcpdk`\n", + "- [GDSFactory+](https://gdsfactory.com) account for cloud simulation" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "### Load a pcell from UBC PDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from ubcpdk import PDK, cells\n", + "\n", + "PDK.activate()\n", + "\n", + "c = cells.straight(length=20.0)\n", + "\n", + "c" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### Configure and run simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from gsim import meep\n", + "\n", + "sim = meep.Simulation()\n", + "\n", + "sim.geometry(component=c, z_crop=\"auto\")\n", + "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", + "sim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\n", + "sim.monitors = [\"o1\", \"o2\"]\n", + "sim.domain(pml=1.0, margin=0.5)\n", + "sim.solver(resolution=20, save_animation=True, verbose_interval=5.0)\n", + "sim.solver.stop_after_sources(time=100)\n", + "\n", + "print(sim.validate_config())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "sim.plot_2d(slices=\"xyz\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### Run simulation on cloud" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# Run on GDSFactory+ cloud\n", + "result = sim.run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "result.plot(db=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "result.show_animation()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c03f0c9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gsim", + "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.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbs/meep_ybranch.ipynb b/nbs/meep_ybranch.ipynb index ad7269ea..a4e8adbe 100644 --- a/nbs/meep_ybranch.ipynb +++ b/nbs/meep_ybranch.ipynb @@ -52,21 +52,7 @@ "id": "4", "metadata": {}, "outputs": [], - "source": [ - "from gsim import meep\n", - "\n", - "sim = meep.Simulation()\n", - "\n", - "sim.geometry(component=c, z_crop=\"auto\")\n", - "sim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\n", - "sim.source(port=\"o1\", wavelength=1.55, bandwidth=0.01, num_freqs=11)\n", - "sim.monitors = [\"o1\", \"o2\", \"o3\"]\n", - "sim.domain(pml=1.0, margin=0.5)\n", - "sim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\n", - "sim.solver.stop_after_sources(time=80)\n", - "\n", - "print(sim.validate_config())" - ] + "source": "from gsim import meep\n\nsim = meep.Simulation()\n\nsim.geometry(component=c, z_crop=\"auto\")\nsim.materials = {\"si\": 3.47, \"SiO2\": 1.44}\nsim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\nsim.monitors = [\"o1\", \"o2\", \"o3\"]\nsim.domain(pml=1.0, margin=0.5)\nsim.solver(resolution=20, simplify_tol=0.01, save_animation=True, verbose_interval=5.0)\nsim.solver.stop_after_sources(time=60)\n\nprint(sim.validate_config())" }, { "cell_type": "code", @@ -133,9 +119,10 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.12.10" } }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/nbs/palace_demo_cpw.ipynb b/nbs/palace_demo_cpw.ipynb index 8c5769c9..72b1504e 100644 --- a/nbs/palace_demo_cpw.ipynb +++ b/nbs/palace_demo_cpw.ipynb @@ -188,17 +188,6 @@ "### Run simulation on cloud" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "8", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate Palace config file\n", - "sim.write_config()" - ] - }, { "cell_type": "code", "execution_count": null, @@ -207,7 +196,7 @@ "outputs": [], "source": [ "# Run simulation on GDSFactory+ cloud\n", - "results = sim.simulate()" + "results = sim.run()" ] }, { @@ -263,7 +252,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.12.10" } }, "nbformat": 4, diff --git a/nbs/palace_demo_microstrip.ipynb b/nbs/palace_demo_microstrip.ipynb index ad4670c4..f796189c 100644 --- a/nbs/palace_demo_microstrip.ipynb +++ b/nbs/palace_demo_microstrip.ipynb @@ -115,17 +115,6 @@ "# sim.plot_mesh(show_groups=[\"metal\", \"P\"], interactive=True)" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "7", - "metadata": {}, - "outputs": [], - "source": [ - "# Generate Palace config file\n", - "sim.write_config()" - ] - }, { "cell_type": "markdown", "id": "8", @@ -142,7 +131,7 @@ "outputs": [], "source": [ "# Run simulation on GDSFactory+ cloud\n", - "results = sim.simulate()" + "results = sim.run()" ] }, { @@ -198,7 +187,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.12.10" } }, "nbformat": 4, diff --git a/src/gsim/gcloud.py b/src/gsim/gcloud.py index 2b2cb508..a6bd8a02 100644 --- a/src/gsim/gcloud.py +++ b/src/gsim/gcloud.py @@ -7,15 +7,19 @@ from gsim import gcloud # Run simulation (uploads, starts, waits, downloads) - results = gcloud.run_simulation("./sim", job_type="palace") + result = gcloud.run_simulation("./sim", job_type="palace") + print(result.sim_dir) # palace_abc123/ + print(result.files) # {"port-S.csv": Path(...), ...} # Or use solver-specific wrappers: from gsim import palace as pa - results = pa.run_simulation("./sim") + result = pa.run_simulation("./sim") """ from __future__ import annotations +import shutil +from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING @@ -26,6 +30,38 @@ from typing import Literal +@dataclass +class RunResult: + """Result of a cloud simulation run. + + Attributes: + sim_dir: Root directory (``{job_type}_{job_name}/``). + files: Flat mapping of filename → Path inside ``output/``. + job_name: Cloud job identifier. + """ + + sim_dir: Path + files: dict[str, Path] = field(default_factory=dict) + job_name: str = "" + + +def _flatten_results(raw_results: dict) -> dict[str, Path]: + """Flatten gdsfactoryplus download results to a filename → Path dict. + + The SDK may return directories (extracted archives) or individual files. + This walks everything and returns a flat mapping. + """ + flat: dict[str, Path] = {} + for result_path in raw_results.values(): + if result_path.is_dir(): + for file_path in result_path.rglob("*"): + if file_path.is_file() and not file_path.name.startswith("."): + flat[file_path.name] = file_path + else: + flat[result_path.name] = result_path + return flat + + def _handle_failed_job(job, output_dir: Path, verbose: bool) -> None: """Handle a failed simulation job by downloading logs and raising informative error. @@ -54,16 +90,7 @@ def _handle_failed_job(job, output_dir: Path, verbose: bool) -> None: print("Downloading logs from failed job...") # noqa: T201 raw_results = sim.download_results(job, output_dir=output_dir) - - # Flatten results to find all files - all_files: dict[str, Path] = {} - for result_path in raw_results.values(): - if result_path.is_dir(): - for file_path in result_path.rglob("*"): - if file_path.is_file() and not file_path.name.startswith("."): - all_files[file_path.name] = file_path - else: - all_files[result_path.name] = result_path + all_files = _flatten_results(raw_results) # Look for log files and display them log_files = ["palace.log", "stdout.log", "stderr.log", "output.log"] @@ -109,48 +136,55 @@ def upload_simulation_dir(input_dir: str | Path, job_type: str): def run_simulation( - output_dir: str | Path, + config_dir: str | Path, job_type: Literal["palace", "meep"] = "palace", verbose: bool = True, on_started: Callable | None = None, -) -> dict[str, Path]: + parent_dir: str | Path | None = None, +) -> RunResult: """Run a simulation on GDSFactory+ cloud. This function handles the complete workflow: - 1. Uploads simulation files + 1. Uploads simulation files from *config_dir* 2. Starts the simulation job - 3. Waits for completion - 4. Downloads results + 3. Creates a structured directory ``sim-data-{job_name}/`` + with ``input/`` (config files) and ``output/`` (results) sub-dirs + 4. Waits for completion + 5. Downloads results into ``output/`` Args: - output_dir: Directory containing the simulation files - job_type: Type of simulation (default: "palace") - verbose: Print progress messages (default True) - on_started: Optional callback called with job object when simulation starts + config_dir: Directory containing the simulation config files. + job_type: Type of simulation (default: "palace"). + verbose: Print progress messages (default True). + on_started: Optional callback called with job object when simulation starts. + parent_dir: Where to create the sim directory. + Defaults to the current working directory. Returns: - Dict mapping result filename to local Path. + RunResult with sim_dir, files dict, and job_name. Raises: RuntimeError: If simulation fails Example: - >>> results = gcloud.run_simulation("./sim", job_type="palace") + >>> result = gcloud.run_simulation("./sim", job_type="palace") Uploading simulation... done Job started: palace-abc123 Waiting for completion... done (2m 34s) Downloading results... done + >>> print(result.sim_dir) + sim-data-palace-abc123/ """ - output_dir = Path(output_dir) + config_dir = Path(config_dir) - if not output_dir.exists(): - raise FileNotFoundError(f"Output directory not found: {output_dir}") + if not config_dir.exists(): + raise FileNotFoundError(f"Config directory not found: {config_dir}") # Upload if verbose: print("Uploading simulation... ", end="", flush=True) # noqa: T201 - pre_job = upload_simulation_dir(output_dir, job_type) + pre_job = upload_simulation_dir(config_dir, job_type) if verbose: print("done") # noqa: T201 @@ -164,6 +198,20 @@ def run_simulation( if on_started: on_started(job) + # Create structured directory + root = Path(parent_dir) if parent_dir else Path.cwd() + sim_dir = root / f"sim-data-{job.job_name}" + input_dir = sim_dir / "input" + output_dir = sim_dir / "output" + input_dir.mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) + + # Move config files into input/ + for item in list(config_dir.iterdir()): + shutil.move(str(item), str(input_dir / item.name)) + # Remove now-empty config_dir (may fail if it was CWD, etc.) + shutil.rmtree(config_dir, ignore_errors=True) + # Wait finished_job = sim.wait_for_simulation(job) @@ -171,29 +219,14 @@ def run_simulation( if finished_job.exit_code != 0: _handle_failed_job(finished_job, output_dir, verbose) - # Download - raw_results = sim.download_results( - finished_job, output_dir=f"sim-data-{finished_job.job_name}" - ) - - # Flatten results: gdsfactoryplus returns extracted directories, - # but we want a dict of filename -> Path for individual files - results: dict[str, Path] = {} - for result_path in raw_results.values(): - if result_path.is_dir(): - # Recursively find all files in the extracted directory - for file_path in result_path.rglob("*"): - if file_path.is_file() and not file_path.name.startswith("."): - results[file_path.name] = file_path - else: - results[result_path.name] = result_path + # Download into output/ + raw_results = sim.download_results(finished_job, output_dir=output_dir) + files = _flatten_results(raw_results) - if verbose and results: - # Find common parent directory for display - first_path = next(iter(results.values())) - print(f"Downloaded {len(results)} files to {first_path.parent}") # noqa: T201 + if verbose and files: + print(f"Downloaded {len(files)} files to {output_dir}") # noqa: T201 - return results + return RunResult(sim_dir=sim_dir, files=files, job_name=job.job_name) def print_job_summary(job) -> None: diff --git a/src/gsim/meep/__init__.py b/src/gsim/meep/__init__.py index d62f4365..3a841889 100644 --- a/src/gsim/meep/__init__.py +++ b/src/gsim/meep/__init__.py @@ -11,7 +11,7 @@ sim = meep.Simulation() sim.geometry(component=ybranch, z_crop="auto") sim.materials = {"si": 3.47, "SiO2": 1.44} - sim.source(port="o1", wavelength=1.55, bandwidth=0.01, num_freqs=11) + sim.source(port="o1", wavelength=1.55, wavelength_span=0.01, num_freqs=11) sim.monitors = ["o1", "o2"] sim.domain(pml=1.0, margin=0.5) sim.solver(resolution=32, simplify_tol=0.01) diff --git a/src/gsim/meep/meep-overview.md b/src/gsim/meep/meep-overview.md deleted file mode 100644 index 3f3f3172..00000000 --- a/src/gsim/meep/meep-overview.md +++ /dev/null @@ -1,135 +0,0 @@ -# gsim.meep — Overview & Roadmap - -## Architecture - -``` -GDS + PDK ──► 3D Geometry ──► Client Viz ──► SimConfig JSON ──► Cloud Runner -(common/) (GeometryModel) (no meep) (no meep) (meep in Docker) -``` - -**Key constraint:** gsim is the client SDK — no meep dependency. MEEP runs only in Docker/cloud. - -**Upload package:** `layout.gds` + `sim_config.json` + `run_meep.py` -> Docker -> `s_parameters.csv` + `meep_debug.json` + diagnostic PNGs. - ---- - -## API: Declarative `Simulation` - -```python -from gsim import meep - -sim = meep.Simulation() -sim.geometry(component=ybranch, z_crop="auto") -sim.materials = {"si": 3.47, "SiO2": 1.44} # float shorthand -sim.source(port="o1", wavelength=1.55, bandwidth=0.1, num_freqs=11) -sim.monitors = ["o1", "o2"] -sim.domain(pml=1.0, margin=0.5) -sim.solver(resolution=32, simplify_tol=0.01) -sim.plot_2d(slices="xyz") -result = sim.run() -``` - -### Design principles - -- **6 typed physics objects** -- `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD` + `monitors: list[str]` -- assigned to a `Simulation` container. No ordering dependencies. -- **Callable, attribute, or constructor style** -- `sim.source(port="o1")` (callable), `sim.source.port = "o1"` (attribute), or `sim.source = ModeSource(port="o1")` (constructor). All work via Pydantic `validate_assignment=True`. -- **Float shorthand for materials** -- `{"si": 3.47}` auto-normalizes to `Material(n=3.47)` via validator. -- **5 stopping methods** -- Flat `stopping` mode string on `FDTD` (`field_decay`/`energy_decay`/`dft_decay`/`fixed`) plus convenience methods: `stop_when_fields_decayed()`, `stop_when_energy_decayed()`, `stop_when_dft_decayed()`, `stop_after_sources()`. A 5th method `stop_after_walltime(seconds)` sets an orthogonal wall-clock safety net (field `wall_time_max`) that can be combined with any mode. -- **Source defines spectral window** -- `WavelengthConfig` derived from `ModeSource.wavelength/bandwidth/num_freqs`. Monitors are just port name strings. -- **JSON contract** -- `write_config()` translates new API -> existing `SimConfig` -> JSON. `StoppingConfig.mode` extended with `"energy_decay"` (runner updated to handle it). - ---- - -## Module Structure - -### `gsim.meep` -- Public API - -| Module | Purpose | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `__init__.py` | Exports: `Simulation`, all model classes | -| `models/api.py` | Declarative models: `Geometry`, `Material`, `ModeSource`, `Domain`, `FDTD`, `Symmetry`. All except `Material`/`Symmetry` have `__call__(**kwargs)` for fluent updates | -| `simulation.py` | `Simulation` container -- `write_config()`, `run()`, `validate_config()`, `plot_2d()`/`plot_3d()`, `estimate_meep_np()` | -| `viz.py` | Standalone viz helpers -- `build_geometry_model()`, `plot_2d()`, `plot_3d()` (calls `common/viz`) | -| `models/config.py` | `SimConfig` + all sub-configs (JSON serialization layer, strict — no defaults on fields that `build_config` always populates) | -| `models/results.py` | `SParameterResult` -- CSV + debug JSON + diagnostic PNGs | -| `ports.py` | `extract_port_info()` -- port center/direction/normal from gdsfactory | -| `materials.py` | `resolve_materials()` -- material names -> (n, k) via common DB | -| `script.py` | `generate_meep_script()` -- cloud runner template (string in Python) | -| `overlay.py` | `SimOverlay` + `PortOverlay` + `DielectricOverlay` -- viz metadata | - -### `gsim.common` -- Shared infrastructure - -Solver-agnostic: `LayeredComponentBase`, `GeometryModel`/`Prism`, `LayerStack`, `ValidationResult`, `viz/` (Matplotlib 2D, PyVista/Open3D 3D). - -Key sub-modules: - -- `stack/_layer_utils.py` — shared `get_gds_layer_tuple()` and `classify_layer_type()` (used by both `extractor` and `visualization`) -- `viz/_mesh_helpers.py` — shared Delaunay triangulation (`triangulate_polygon_with_holes`) and batch prism geometry (`collect_triangular_prism_geometry`), used by both PyVista and Open3D renderers - -### Visualization pipeline - -3D backends: PyVista (desktop) and Open3D+Plotly (Jupyter, with view buttons for Iso/Top/Front/Right). Three.js backend removed. - -``` -Simulation.plot_2d() - -> viz.build_geometry_model(component, stack, domain_config) - -> LayeredComponentBase + extract_geometry_model() - -> crop_geometry_model() if stack is z-cropped - -> viz.build_overlay(gm, component, stack, domain_config) - -> overlay.build_sim_overlay() - -> common/viz/render2d.plot_prism_slices(gm, overlay=overlay) -``` - -- **Z slices** -- draw actual prism polygon outlines at the slice plane (XY view) -- **X/Y slices** -- compute Shapely line-polygon intersections for accurate cross-sections (XZ/YZ views); each intersection segment becomes a rectangle from z_base to z_top -- **Overlay** -- sim cell boundary, PML regions, port markers, dielectric backgrounds - ---- - -## Key Design Decisions - -- **GDS-file approach** -- Send raw GDS to cloud (not polygon coords in JSON). Complex geometries have 1000s of nodes; DerivedLayers need gdsfactory to resolve. -- **Port z-center = highest refractive index** -- Photonic ports center on waveguide core (not conductor layer like RF). -- **Z-crop** -- Auto-crops stack around core layer. Full UBC stack is 15um; cropped to ~1.5um (massive compute savings). -- **Port extension into PML** -- `gf.components.extend_ports()` at `write_config()` time. Original bbox stored in `SimConfig.component_bbox` for correct cell sizing. -- **Symmetries disabled for S-params** -- `add_mode_monitor` uses `use_symmetry=false` internally; `get_eigenmode_coefficients` doesn't apply `S.multiplicity()`. Source port coefficients underestimated ~2x. gplugins also never uses `mp.Mirror`. -- **`field_decay` default stopping** -- Monitors |component|² at a point and stops when it decays by `decay_by` from peak. Runner uses `mp.stop_when_fields_decayed()` + `_make_time_cap(max_time)` in an `until_after_sources` list (OR logic — first condition wins). `energy_decay` is similar but monitors total cell energy (more robust for ring couplers where DFTs can falsely converge). `stop_after_sources(time)` runs for a fixed sim-time after sources turn off. -- **Wall-clock safety net** -- `stop_after_walltime(seconds)` sets `wall_time_max` on `FDTD`, an orthogonal real-time cap. Runner's `_make_wall_time_cap()` uses `time.time()` deadline and is appended to the `until_after_sources` list in all 4 mode branches when `wall_time_max > 0`. A warning is logged when the wall-clock limit likely triggered (elapsed >= 95% of limit). -- **`dft_min_run_time` is absolute sim time** -- Not time-after-sources. With a broadband source turning off at ~t=78 and min_run_time=100, DFT checking starts at t=100 (only ~22 units after source ends). Must exceed `device_length * n_group`. -- **API models are the single source of truth for defaults** -- Config models (`models/config.py`) and the cloud runner (`script.py`) have no default values for fields that `build_config()` always populates. `model_dump(by_alias=True)` serializes all fields explicitly, so config defaults and script.py `.get()` fallbacks were dead code that could silently drift. Config sub-models are strict (missing fields = `ValidationError`); `script.py` uses direct `dict["key"]` access. -- **Stack resolved lazily** -- `_ensure_stack()` falls back to `get_stack()` (active PDK defaults) when no explicit stack kwargs are set. Same path for `write_config()` and viz. - ---- - -## TODO - -### Near-term - -- [ ] **Cloud end-to-end test** -- Full round-trip (upload -> run meep -> download results) hasn't been tested with real cloud instance. -- [ ] **Monitor z-span touches PML** -- After z-crop, layer stack z-extent exactly matches PML inner boundary. At coarse resolution, outermost monitor pixels may straddle PML. Fix: shrink monitor z-span by ~`2/resolution` inset. -- [ ] **Empty `core2` layer in config** -- UBC PDK's ebeam_y_1550 includes a `core2` entry (GDS layer 31) with zero geometry. Harmless but clutters config. Consider filtering empty layers. - -### Medium-term - -- [ ] **Custom monitors** -- Monitors are port-name strings only. Add `FieldMonitor(center, size)` and `FluxMonitor` for custom measurement locations. -- [ ] **Per-port margin/mode_index** -- `port_margin` is global in `Domain`. Could add per-port control for margin and higher-order modes. -- [ ] **Typed boundaries** -- Only PML today. Add `Periodic`, `Bloch`, `PEC`, `PMC` per-axis boundary specs. -- [ ] **Mesh refinement regions** -- Single global `resolution`. Add local refinement for thin features. -- [ ] **Dispersive materials** -- `Material(n, k)` is non-dispersive. Add Sellmeier/Lorentz/Drude support. -- [ ] **`updated_copy()` for parameter sweeps** -- Pydantic `model_copy(update={...})` makes this nearly free. - -### Deferred - -- [ ] **JSON schema v2** -- Restructure JSON groups (`"solver.wavelength"`, per-port margins, etc.) with version field for runner backward compat. Requires atomic client + runner update. -- [ ] **Port symmetries** -- gplugins-style: run fewer source ports, copy S-params between symmetric port pairs (e.g. S31=S21 for Y-branch). -- [ ] **`add_flux` + `eig_parity` path** -- Alternative to `add_mode_monitor` that works correctly with `mp.Mirror` symmetry. Needs auto-detection of correct parity from symmetry config and waveguide polarization. - ---- - -## Docker / Cloud - -- **Base:** `continuumio/miniconda3` -> conda env with `pymeep=*=mpi_mpich_*`, `gdsfactory`, `nlopt` -- **Entrypoint:** downloads input -> `mpirun -np $NP python run_meep.py` -> uploads outputs -- **`meep_np` hardcoded to 2** -- auto-compute logic exists (`total_voxels // 200k`) but `lscpu` reports host cores inside containers, not Batch-allocated vCPUs. TODO: pass Batch vCPU allocation as `MEEP_NP` env var from job submission, then restore auto-compute with proper clamping -- **Instance recommendation:** `c7a.16xlarge` (AMD EPYC Genoa, DDR5, ~460 GB/s mem BW) for typical photonics; `hpc7a.96xlarge` for large problems -- **Local testing:** `./nbs/test_meep_local.sh` (regenerate -> Docker build -> run -> results) diff --git a/src/gsim/meep/models/api.py b/src/gsim/meep/models/api.py index 7a0c79db..25861b8f 100644 --- a/src/gsim/meep/models/api.py +++ b/src/gsim/meep/models/api.py @@ -71,10 +71,11 @@ class ModeSource(BaseModel): gt=0, description="Center wavelength in um", ) - bandwidth: float = Field( + wavelength_span: float = Field( default=0.1, ge=0, - description="Measurement wavelength bandwidth in um", + description="Wavelength span of the measurement frequency grid in um. " + "Together with num_freqs, sets the spacing between monitor frequency points.", ) num_freqs: int = Field( default=11, @@ -130,6 +131,19 @@ class Domain(BaseModel): ge=0, description="Extend ports into PML (um). 0 = auto (margin + pml).", ) + source_port_offset: float = Field( + default=0.1, + ge=0, + description="Distance to offset source from port center into device (um). " + "Matches gplugins port_source_offset.", + ) + distance_source_to_monitors: float = Field( + default=0.2, + ge=0, + description="Distance between source and its port monitor (um). " + "The source-port monitor is placed this far past the source, " + "deeper into the device. Matches gplugins distance_source_to_monitors.", + ) symmetries: list[Symmetry] = Field( default_factory=list, description="Mirror symmetry planes. Not yet used in production runs.", diff --git a/src/gsim/meep/models/config.py b/src/gsim/meep/models/config.py index 40825311..0b05576b 100644 --- a/src/gsim/meep/models/config.py +++ b/src/gsim/meep/models/config.py @@ -57,6 +57,15 @@ class DomainConfig(BaseModel): description="Length to extend waveguide ports into PML in um. " "0 = auto (margin_xy + dpml).", ) + source_port_offset: float = Field( + ge=0, + description="Distance to offset source from port center into device (um).", + ) + distance_source_to_monitors: float = Field( + ge=0, + description="Distance between source and its port monitor (um). " + "Source-port monitor is placed this far past the source into the device.", + ) class StoppingConfig(BaseModel): diff --git a/src/gsim/meep/script.py b/src/gsim/meep/script.py index 31c14dc0..0a9b99b8 100644 --- a/src/gsim/meep/script.py +++ b/src/gsim/meep/script.py @@ -375,22 +375,35 @@ def get_port_z_span(config): def build_sources(config): - """Build MEEP source from config port data.""" + """Build MEEP source from config port data. + + The source is offset from the port center by ``source_port_offset`` + along the propagation direction (into the device). This separates + the soft source from the port monitor so eigenmode coefficients + measure the true incident amplitude rather than half of it. + """ fdtd = config["fdtd"] fcen = fdtd["fcen"] df = fdtd["df"] fwidth = config["source"]["fwidth"] z_span = get_port_z_span(config) port_margin = config["domain"]["port_margin"] + source_port_offset = config["domain"].get("source_port_offset", 0.1) sources = [] for port in config["ports"]: if not port["is_source"]: continue - center = mp.Vector3(*port["center"]) + # Offset source center into the device along propagation direction + center_list = list(port["center"]) normal_axis = port["normal_axis"] direction = port["direction"] + if direction == "+": + center_list[normal_axis] += source_port_offset + else: + center_list[normal_axis] -= source_port_offset + center = mp.Vector3(*center_list) size = [0, 0, 0] transverse_axis = 1 - normal_axis @@ -421,18 +434,45 @@ def build_sources(config): def build_monitors(config, sim): - """Build mode monitors at all ports and return flux regions.""" + """Build mode monitors at all ports and return flux regions. + + The source-port monitor is offset further into the device (past + the source) by ``source_port_offset + distance_source_to_monitors`` + so the forward-going mode from the source passes through it at + full amplitude. This matches the gplugins approach. + """ fdtd = config["fdtd"] fcen = fdtd["fcen"] df = fdtd["df"] nfreq = fdtd["num_freqs"] z_span = get_port_z_span(config) port_margin = config["domain"]["port_margin"] + source_port_offset = config["domain"].get("source_port_offset", 0.1) + distance_source_to_monitors = config["domain"].get( + "distance_source_to_monitors", 0.2 + ) monitors = {} for port in config["ports"]: - center = mp.Vector3(*port["center"]) + center_list = list(port["center"]) normal_axis = port["normal_axis"] + direction = port["direction"] + + # All monitors shift inward from port center (matches gplugins): + # source-port monitor: source_port_offset + distance_source_to_monitors + # non-source monitors: source_port_offset + if port["is_source"]: + offset = source_port_offset + distance_source_to_monitors + else: + offset = source_port_offset + + if offset > 0: + if direction == "+": + center_list[normal_axis] += offset + else: + center_list[normal_axis] -= offset + + center = mp.Vector3(*center_list) size = [0, 0, 0] transverse_axis = 1 - normal_axis diff --git a/src/gsim/meep/simulation.py b/src/gsim/meep/simulation.py index 7edc4221..afb84126 100644 --- a/src/gsim/meep/simulation.py +++ b/src/gsim/meep/simulation.py @@ -98,7 +98,7 @@ class Simulation(BaseModel): sim.monitors = ["o1", "o2"] sim.solver.stopping = "dft_decay" sim.solver.max_time = 200 - result = sim.run("./meep-sim") + result = sim.run() # creates sim-data-{job_name}/ in CWD """ model_config = ConfigDict( @@ -308,7 +308,7 @@ def _wavelength_config(self) -> Any: return WavelengthConfig( wavelength=self.source.wavelength, - bandwidth=self.source.bandwidth, + bandwidth=self.source.wavelength_span, num_freqs=self.source.num_freqs, ) @@ -348,6 +348,8 @@ def _domain_config(self) -> Any: margin_z_below=self.domain.margin_z_below, port_margin=self.domain.port_margin, extend_ports=self.domain.extend_ports, + source_port_offset=self.domain.source_port_offset, + distance_source_to_monitors=self.domain.distance_source_to_monitors, ) def _resolution_config(self) -> Any: @@ -618,12 +620,21 @@ def write_config(self, output_dir: str | Path) -> Path: # run # ------------------------------------------------------------------------- - def run(self, output_dir: str | Path | None = None, *, verbose: bool = True) -> Any: + def run( + self, + parent_dir: str | Path | None = None, + *, + verbose: bool = True, + ) -> Any: """Run MEEP simulation on the cloud. + Config files are written to a temporary directory, uploaded, then + moved into a structured ``sim-data-{job_name}/input/`` directory. + Results are downloaded to ``sim-data-{job_name}/output/``. + Args: - output_dir: Directory to write config and download results. - If None, a temporary directory is created automatically. + parent_dir: Where to create the sim directory. + Defaults to the current working directory. verbose: Print progress info. Returns: @@ -634,18 +645,23 @@ def run(self, output_dir: str | Path | None = None, *, verbose: bool = True) -> from gsim.gcloud import run_simulation from gsim.meep.models.results import SParameterResult - if output_dir is None: - output_dir = Path(tempfile.mkdtemp(prefix="meep_")) - - output_dir = self.write_config(output_dir) + tmp = Path(tempfile.mkdtemp(prefix="meep_")) + try: + self.write_config(tmp) + result = run_simulation( + config_dir=tmp, + job_type="meep", + verbose=verbose, + parent_dir=parent_dir, + ) + except Exception: + # Clean up temp dir on failure (run_simulation moves it on success) + import shutil - results = run_simulation( - output_dir, - job_type="meep", - verbose=verbose, - ) + shutil.rmtree(tmp, ignore_errors=True) + raise - csv_path = results.get("s_parameters.csv") + csv_path = result.files.get("s_parameters.csv") if csv_path is not None: return SParameterResult.from_csv(csv_path) diff --git a/src/gsim/palace/__init__.py b/src/gsim/palace/__init__.py index 6b242127..8de7ce5f 100644 --- a/src/gsim/palace/__init__.py +++ b/src/gsim/palace/__init__.py @@ -22,7 +22,7 @@ # Generate mesh and run sim.mesh("./sim", preset="fine") - results = sim.simulate() + results = sim.run() """ from __future__ import annotations diff --git a/src/gsim/palace/driven.py b/src/gsim/palace/driven.py index 52c5860d..583f7e86 100644 --- a/src/gsim/palace/driven.py +++ b/src/gsim/palace/driven.py @@ -46,7 +46,7 @@ class DrivenSim(PalaceSimMixin, BaseModel): >>> sim.add_cpw_port("P3", "P4", layer="topmetal2", length=5.0) >>> sim.set_driven(fmin=1e9, fmax=100e9, num_points=40) >>> sim.mesh("./sim", preset="default") - >>> results = sim.simulate() + >>> results = sim.run() Attributes: geometry: Wrapped gdsfactory Component (from common) @@ -663,7 +663,7 @@ def write_config(self) -> Path: # Simulation # ------------------------------------------------------------------------- - def simulate( + def run( self, *, verbose: bool = True, @@ -672,6 +672,10 @@ def simulate( Requires mesh() and write_config() to be called first. + Config files are uploaded, then moved into a structured + ``sim-data-{job_name}/input/`` directory. Results are downloaded + to ``sim-data-{job_name}/output/``. + Args: verbose: Print progress messages @@ -684,7 +688,7 @@ def simulate( RuntimeError: If simulation fails Example: - >>> results = sim.simulate() + >>> results = sim.run() >>> print(f"S-params saved to: {results['port-S.csv']}") """ from gsim.gcloud import run_simulation @@ -692,15 +696,14 @@ def simulate( if self._output_dir is None: raise ValueError("Output directory not set. Call set_output_dir() first.") - output_dir = self._output_dir - - return run_simulation( - output_dir, + result = run_simulation( + config_dir=self._output_dir, job_type="palace", verbose=verbose, ) + return result.files - def simulate_local( + def run_local( self, *, verbose: bool = True, diff --git a/src/gsim/palace/eigenmode.py b/src/gsim/palace/eigenmode.py index aeb123fa..e75394dd 100644 --- a/src/gsim/palace/eigenmode.py +++ b/src/gsim/palace/eigenmode.py @@ -45,7 +45,7 @@ class EigenmodeSim(PalaceSimMixin, BaseModel): >>> sim.set_eigenmode(num_modes=10, target=50e9) >>> sim.set_output_dir("./sim") >>> sim.mesh(preset="default") - >>> results = sim.simulate() + >>> results = sim.run() Attributes: geometry: Wrapped gdsfactory Component (from common) @@ -530,7 +530,7 @@ def mesh( # Simulation # ------------------------------------------------------------------------- - def simulate( + def run( self, output_dir: str | Path | None = None, *, diff --git a/src/gsim/palace/electrostatic.py b/src/gsim/palace/electrostatic.py index 4e4f8ab0..0f1d0626 100644 --- a/src/gsim/palace/electrostatic.py +++ b/src/gsim/palace/electrostatic.py @@ -46,7 +46,7 @@ class ElectrostaticSim(PalaceSimMixin, BaseModel): >>> sim.set_electrostatic() >>> sim.set_output_dir("./sim") >>> sim.mesh(preset="default") - >>> results = sim.simulate() + >>> results = sim.run() Attributes: geometry: Wrapped gdsfactory Component (from common) @@ -371,7 +371,7 @@ def mesh( # Simulation # ------------------------------------------------------------------------- - def simulate( + def run( self, output_dir: str | Path | None = None, *, diff --git a/tests/meep/test_meep_models.py b/tests/meep/test_meep_models.py index 09bae0a7..61a5c8a6 100644 --- a/tests/meep/test_meep_models.py +++ b/tests/meep/test_meep_models.py @@ -140,6 +140,8 @@ def test_to_json(self, tmp_path): margin_z_below=0.5, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ), accuracy=AccuracyConfig( eps_averaging=False, @@ -567,6 +569,8 @@ def test_custom(self): margin_z_below=0.4, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) assert cfg.dpml == 0.5 assert cfg.margin_xy == 0.2 @@ -581,6 +585,8 @@ def test_extend_ports_custom(self): margin_z_below=0.5, port_margin=0.5, extend_ports=2.5, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) assert cfg.extend_ports == 2.5 @@ -592,6 +598,8 @@ def test_extend_ports_serialization(self): margin_z_below=0.5, port_margin=0.5, extend_ports=3.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) d = cfg.model_dump() assert d["extend_ports"] == 3.0 @@ -604,6 +612,8 @@ def test_model_dump(self): margin_z_below=1.5, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) d = cfg.model_dump() assert d["dpml"] == 2.0 @@ -644,6 +654,8 @@ def _sim_kwargs(self): margin_z_below=0.5, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ), accuracy=AccuracyConfig( eps_averaging=False, @@ -717,6 +729,8 @@ def test_domain_in_sim_config_json(self, tmp_path): margin_z_below=0.5, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ), accuracy=AccuracyConfig( eps_averaging=False, @@ -1096,6 +1110,8 @@ def test_build_sim_overlay(self): margin_z_below=0.0, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) port_data = [ @@ -1161,6 +1177,8 @@ def test_build_sim_overlay_with_dielectrics(self): margin_z_below=0.5, port_margin=0.5, extend_ports=0.0, + source_port_offset=0.1, + distance_source_to_monitors=0.2, ) dielectrics = [ {"name": "substrate", "material": "silicon", "zmin": -1.0, "zmax": 0.0}, diff --git a/tests/meep/test_simulation.py b/tests/meep/test_simulation.py index f62cb6aa..58741a95 100644 --- a/tests/meep/test_simulation.py +++ b/tests/meep/test_simulation.py @@ -44,14 +44,14 @@ def test_defaults(self): s = ModeSource() assert s.port is None assert s.wavelength == 1.55 - assert s.bandwidth == 0.1 + assert s.wavelength_span == 0.1 assert s.num_freqs == 11 def test_custom(self): - s = ModeSource(port="o1", wavelength=1.31, bandwidth=0.05, num_freqs=21) + s = ModeSource(port="o1", wavelength=1.31, wavelength_span=0.05, num_freqs=21) assert s.port == "o1" assert s.wavelength == 1.31 - assert s.bandwidth == 0.05 + assert s.wavelength_span == 0.05 assert s.num_freqs == 21 @@ -64,6 +64,8 @@ def test_defaults(self): assert d.margin_z_below == 0.5 assert d.port_margin == 0.5 assert d.extend_ports == 0.0 + assert d.source_port_offset == 0.1 + assert d.distance_source_to_monitors == 0.2 assert d.symmetries == [] def test_custom(self): @@ -284,11 +286,11 @@ def test_geometry_component(self): class TestCallableAPI: def test_source_callable(self): sim = Simulation() - result = sim.source(port="o1", wavelength=1.31, bandwidth=0.05) + result = sim.source(port="o1", wavelength=1.31, wavelength_span=0.05) assert result is sim.source assert sim.source.port == "o1" assert sim.source.wavelength == 1.31 - assert sim.source.bandwidth == 0.05 + assert sim.source.wavelength_span == 0.05 def test_domain_callable(self): sim = Simulation() @@ -382,7 +384,7 @@ def test_from_source_defaults(self): def test_from_source_custom(self): sim = Simulation() - sim.source = ModeSource(wavelength=1.31, bandwidth=0.05, num_freqs=21) + sim.source = ModeSource(wavelength=1.31, wavelength_span=0.05, num_freqs=21) wl = sim._wavelength_config() assert wl.wavelength == 1.31 assert wl.bandwidth == 0.05 @@ -443,6 +445,8 @@ def test_domain_translation(self): assert cfg.margin_xy == 0.3 assert cfg.margin_z_above == 1.0 assert cfg.margin_z_below == 0.8 + assert cfg.source_port_offset == 0.1 + assert cfg.distance_source_to_monitors == 0.2 def test_resolution_translation(self): sim = Simulation() @@ -460,7 +464,7 @@ def test_accuracy_translation(self): def test_source_translation(self): sim = Simulation() - sim.source = ModeSource(port="o1", bandwidth=0.05) + sim.source = ModeSource(port="o1") cfg = sim._source_config() assert cfg.port == "o1" assert cfg.bandwidth is None # always auto fwidth From a173d36df33b3db388e9e65c1f60308845fcc47d Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Wed, 18 Feb 2026 12:26:36 +0100 Subject: [PATCH 09/10] ci: parallelize notebook execution in docs workflow Use a matrix strategy to run each notebook on its own runner simultaneously instead of sequentially. A list-notebooks job discovers all .ipynb files, then run-notebook fans out in parallel with fail-fast: false. The build job collects all artifacts via merge-multiple before building docs. --- .github/workflows/docs.yml | 45 ++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0b6e0710..abf338d8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,8 +12,30 @@ env: GFP_API_KEY: ${{ secrets.GFP_API_KEY }} jobs: - run-notebooks: + # Discover all notebooks and output as JSON array for the matrix + list-notebooks: runs-on: ubuntu-latest + outputs: + notebooks: ${{ steps.find.outputs.notebooks }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Find notebooks + id: find + run: | + nbs=$(find nbs -name "*.ipynb" -not -path "*/.ipynb_checkpoints/*" | sort | jq -Rsc 'split("\n") | map(select(. != ""))') + echo "notebooks=$nbs" >> "$GITHUB_OUTPUT" + + # Run each notebook in parallel + run-notebook: + runs-on: ubuntu-latest + needs: list-notebooks + strategy: + matrix: + notebook: ${{ fromJson(needs.list-notebooks.outputs.notebooks) }} + fail-fast: false steps: - name: Checkout repo uses: actions/checkout@v4 @@ -29,24 +51,22 @@ jobs: run: just dev - name: Create ipykernel run: uv run --no-sync python -m ipykernel install --user --name gsim - - name: Run notebooks + - name: Run notebook run: | - for nb in $(find nbs -name "*.ipynb" -not -path "*/.ipynb_checkpoints/*" | sort); do - dir=$(dirname "$nb") - filename=$(basename "$nb") - (cd "$dir" && xvfb-run -a uv run --no-sync papermill "$filename" "$filename" -k gsim) - done - - name: Upload executed notebooks + dir=$(dirname "${{ matrix.notebook }}") + filename=$(basename "${{ matrix.notebook }}") + cd "$dir" && xvfb-run -a uv run --no-sync papermill "$filename" "$filename" -k gsim + - name: Upload executed notebook uses: actions/upload-artifact@v4 with: - name: nbs - path: nbs + name: nb-${{ hashFiles(matrix.notebook) }} + path: ${{ matrix.notebook }} retention-days: 1 build: name: Build Docs runs-on: ubuntu-latest - needs: run-notebooks + needs: run-notebook steps: - name: Checkout repo uses: actions/checkout@v4 @@ -68,7 +88,8 @@ jobs: - name: Download all notebook artifacts uses: actions/download-artifact@v4 with: - name: nbs + pattern: nb-* + merge-multiple: true path: nbs - name: Build docs run: | From 1405de90dee4f0a3bbc46a0f67442e597c0c4a84 Mon Sep 17 00:00:00 2001 From: vahid ansari Date: Wed, 18 Feb 2026 12:38:35 +0100 Subject: [PATCH 10/10] uodate notebook --- nbs/meep_ring_coupler.ipynb | 116 +++++------------------------------- 1 file changed, 14 insertions(+), 102 deletions(-) diff --git a/nbs/meep_ring_coupler.ipynb b/nbs/meep_ring_coupler.ipynb index c40da89c..42b0c632 100644 --- a/nbs/meep_ring_coupler.ipynb +++ b/nbs/meep_ring_coupler.ipynb @@ -25,42 +25,16 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "2", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m2026-02-18 06:12:13.722\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m25\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.728\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m48\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.729\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m110\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.730\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m149\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.731\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m168\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.731\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m210\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.732\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m244\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.732\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m269\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n", - "\u001b[32m2026-02-18 06:12:13.733\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mdoroutes.pcells\u001b[0m:\u001b[36m\u001b[0m:\u001b[36m294\u001b[0m - \u001b[33m\u001b[1mCannot determine output type ((D)KCell type)from annotation . Trying to continue but likely this will fail.\u001b[0m\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from ubcpdk import PDK, cells\n", "\n", "PDK.activate()\n", "\n", - "c = cells.coupler_ring(gap=0.2)\n", + "c = cells.coupler_ring(gap=0.15)\n", "\n", "c" ] @@ -78,17 +52,7 @@ "execution_count": null, "id": "4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Stack validation: PASSED\n", - "Warnings:\n", - " - No stack configured. Will use active PDK with defaults.\n" - ] - } - ], + "outputs": [], "source": [ "from gsim import meep\n", "\n", @@ -99,7 +63,7 @@ "sim.source(port=\"o1\", wavelength=1.55, wavelength_span=0.01, num_freqs=11)\n", "sim.monitors = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", "sim.domain(pml=1.0, margin=0.5)\n", - "sim.solver(resolution=25, save_animation=True, verbose_interval=5.0)\n", + "sim.solver(resolution=20, save_animation=True, verbose_interval=5.0)\n", "sim.solver.stop_after_sources(time=100)\n", "\n", "print(sim.validate_config())" @@ -107,21 +71,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "5", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sim.plot_2d(slices=\"xyz\")" ] @@ -136,22 +89,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "7", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Uploading simulation... done\n", - "Job started: meep-57e3d203\n", - "Created: 05:12:16 | Now: 05:31:54 | Status: completed\n", - "Extracting results.tar.gz...\n", - "Downloaded 362 files to /Users/vahid/doplaydo/gsim/nbs/sim-data-meep-57e3d203/results\n" - ] - } - ], + "outputs": [], "source": [ "# Run on GDSFactory+ cloud\n", "result = sim.run()" @@ -159,48 +100,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "8", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXopJREFUeJzt3Qd8FGX+x/HfJqTQEkoKLXQBQbqCoBQVQfTsYj/BAuqpqNhAlKKnCHqC+ldRUVFPT2xgP0GxcFRFqWIBKQqk0BJa+v5fvyfZZVtCNiTZ2cnn7WtNdnZ2dmae7PLdp43D6XQ6BQAAAGEvItQ7AAAAgIpBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AS1i7dq1ccskl0qJFC4mNjZWmTZvKmWeeKc8888xRn+twONy3iIgIadKkiQwePFi++eYbsbNDhw7JpEmTbH+cAMrOwbViAYTakiVL5LTTTpPmzZvL8OHDpVGjRvLnn3/KsmXLZNOmTbJx48ZSn6+BTkPgNddcI3r5682bN8tzzz0n6enp8umnn8rQoUPFjnbt2iWJiYkyceJEE/AAoAanAECoPfLIIxIfHy/ff/+91KtXz+sxDWdl0a5dO7n66qvd9y+88ELp0qWLzJgx45iD3cGDB6V27dpSXVS34wXshKZYACGntXKdOnXyC3UqKSmpXNvs3LmzJCQkmNo7tWjRIhk2bJipFYyJiZGUlBS588475fDhw17PGzFihNSpU8fs09lnny1169aVq666qlzb2LZtm/ztb38zv2vT8rPPPutudj799NNNeNKm57feestv//ft2yd33HGHeQ19rbZt28rUqVOlsLDQPL5lyxZTW6cmT57sbor2rLn75ZdfTPN2gwYNTPP2iSeeKB999JHX68yePds879tvv5V//OMf5nw3a9bMPLZ//36zDy1btjT7oI9pzeiPP/5YrjIBUPmosQMQchpuli5dKuvWrZMTTjihQra5d+9ec9NApN59913TJ+3mm2+Whg0byooVK0z/vb/++ss85ik/P1+GDBkip556qjzxxBNSq1atoLdRUFBgagr79+8v06ZNkzfffFNuvfVWE+bGjx9vwuJFF10kM2fONE3Iffr0kVatWpnn6msMGDBAtm/fLjfeeKMJktpcPW7cONm5c6ephdRQ9/zzz5t90dpJ3ZbSWkq1fv16OeWUU0ygHDt2rHndd955Ry644AJ5//33zXM8aajTbU6YMMHU2KmbbrpJ3nvvPbPfHTt2lN27d8v//vc/2bBhg/To0aNCyglABdM+dgAQSvPnz3dGRkaaW58+fZz33nuv84svvnDm5uaW6fn6UXb99dc7MzIynOnp6c7ly5c7zzjjDLP8X//6l1nn0KFDfs+bMmWK0+FwOLdu3epeNnz4cPO8sWPH+q0f7DYeffRR97K9e/c6a9asadZ9++233ct/+eUXs+7EiRPdyx5++GFn7dq1nb/99pvXa+k+6Tnatm2bua/H6/tcFz3+zp07O7Ozs93LCgsLnX379nUed9xx7mWvvvqq2capp57qzM/P99pGfHy885ZbbvHbNgDroikWQMhp857W2J133nmyevVqU8OlNWZa2+TbdFiSl19+2dQ4aXNh7969ZfHixTJmzBjTlKhq1qzpXldrpHTgQd++fc1gi59++slve1oT5ivYbdxwww3u37WZuX379qbm7NJLL3Uv12X62B9//OFeprV//fr1k/r165vXcN0GDRpkagK/++67Us/Fnj17ZOHCheZ1tDnV9XytcdPz+vvvv5vaQE8jR46UyMhIr2W6X8uXL5cdO3aU+noArIOmWACWcNJJJ8kHH3wgubm5JtzNnTtXpk+fbvqIrVq1yoyU1cc8Q5YOuHA5//zzTZOh9hfTfnHaZ89zAID2d9NmRg2K2kTrKTMz0+t+jRo13P3MPAWzDe3T5uoD56L7q9vVffRd7rk9DV5r1qzxe35ZB5ToKGINmw8++KC5lbQNDc4urmZgTxqwdZSy9vPr2bOn6XOozcatW7cu9fUBhA7BDoClREdHm5CnNx3peu2115oarK+//tp08HfRwKEd/100MGmNViBay6W1glqTdd9990mHDh1M6NNaKx3o4BqQ4KIDBXQ+vGPZhm/t19GWF7UoF9Ft6Wvde++9AdfV81Ia177cfffdpoYuEFffw0C1kS5a46c1hxqy58+fL48//rgZwKEB3K5TyADhjmAHwLJ0FKfSAQP/+te/vGq1dBListJRqL/99pu89tprpsbJZcGCBVW6jbJq06aNHDhwoMSg6uJb8+fiqlGLioo66jaOpnHjxmZghd60lk8HTej0NAQ7wJroYwcg5LQ2zrPG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Tz6RESNGVMk5u/baa+XNN9+UPXv2VMnrASgdwQ5AlWrfvr00btzY1Ma56O/nn3++tGrVSpYtW+a1XIOgeuONN+TEE080tVAaAq+88kpJT083jxUWFkqzZs3k+eef93qtn376SSIiImTr1q3m/r59++SGG26QxMREiYuLk9NPP11Wr15d6v7OmjVLjj/+eImNjZUOHTrIc889535sy5Ytpkbxgw8+MPtZq1Yt6dq1qyxdutRrGy+99JKkpKSYxy+88EJ58sknpV69euax2bNny+TJk81+6Lb0pss8mzv1Ofrc4447Tj766KNS9/edd94x+9C0adNSawRnzJghLVu2dN/XIHjBBRfIo48+KsnJyWb/HnroIVOzes8990iDBg3MOX711Ve9ttOpUydp0qSJzJ07t9T9AlA1CHYAqpyGIK2Nc9HftRl2wIAB7uWHDx82NXiuYJeXlycPP/ywCUDa70xDlatWSsPbFVdcIW+99ZbX62hN0imnnCItWrQw94cNG2bC4Oeffy4rV66UHj16yBlnnFFibZM+f8KECfLII4/Ihg0bTOh58MEH5bXXXvNab/z48XL33XfLqlWrpF27dmZfXE3Nixcvlptuukluv/128/iZZ55ptudy2WWXyV133WUC0s6dO81Nl7lo6Lv00ktlzZo1cvbZZ8tVV11Vau2Y1n5qAC6PhQsXyo4dO+S7774z4VObWP/2t79J/fr1TVnocdx4441+fep69eplXheABTgBoIq99NJLztq1azvz8vKcWVlZzho1ajjT09Odb731lrN///5mna+++sqpH1Fbt24NuI3vv//ePL5//35z/6effnI6HA73+gUFBc6mTZs6n3/+eXN/0aJFzri4OGd2drbXdtq0aeN84YUXzO8TJ050du3a1esx3SdPDz/8sLNPnz7m982bN5t9mDVrlvvx9evXm2UbNmww9y+77DLnOeec47WNq666yhkfH+++7/u6LrqdBx54wH3/wIEDZtnnn39e4rnV7Tz00ENeywJtf/r06c4WLVq47w8fPtzc1/Pm0r59e2e/fv3c9/Pz8025/ec///Ha1p133ukcOHBgifsEoOpQYwegymnt3MGDB+X77783NT1ay6XNo1pj5+pnp82wrVu3Nn3slNawnXvuuea+Nsfquq4+ZUqbGrXJ1FVr9+2335raOa2lU1rTp337GjZsKHXq1HHfNm/eHHBwhu6fLr/++uu91v/nP//pt36XLl3cv2szs3I1E+sIVa3R8uR7vzSe265du7ZpQnZtOxCt6dRm4/LQWkOt/XTRJtnOnTu770dGRprz5/v6NWvWlEOHDpXrNQFUrBoVvD0AOKq2bdua/lra7Lp37153SNO+WtoXbcmSJeYx7QPnCllDhgwxN20e1RCogU7v5+bmurerzZQa7MaOHWt+nnXWWSaIKA11vn37XFz93Tzp+q7+cb179/Z6TAOOJx3R66J95Fz9/iqC57Zd2y9t2zpSVc+pJw1rRRWAR2jTdlleqyyvr03DWiYAQo9gByAktO+chiwNIdo536V///6mD9yKFSvk5ptvNst++eUX2b17tzz22GMm+CnPUbUuOqDigQceMLV77733nplaxUX706WmpkqNGjW8Bg2URGurNGj+8ccfJjAey2ARrZn05Hs/OjpaCgoKpCJ0795dfv75Z69lGrr02DXcuYKn9verKOvWrfObqgZAaNAUCyBkwe5///ufCRiuGjulv7/wwgumJs41cEKbXzX8PPPMMyZo6chQHUjhSwNb3759TfOpBqXzzjvP/digQYOkT58+ZuTn/PnzzeALrRnUgQ+BQqJr4MKUKVPk6aeflt9++03Wrl1rRoXqwIKy0vnkPvvsM/Oc33//3RybBldXwHLttzYJ67nQUbA5OTlSXlqLqaNyPYOihq6MjAyZNm2aaUZ+9tlnzT5UBG2C1SA9ePDgCtkegGNDsAMQEhratD+YNstq7ZhnsNMrJrimRXHVOOkUIO+++6507NjR1Nw98cQTAbertWvan06nCNG+Xy4apDRgaY2gzr2m/fouv/xyMxWK5+t70qlRdLoTDXPa10z3TfdDp2UpKx2VqzWHGux0GpL//ve/cuedd3r1g7v44otNs7GeEz3W//znP1JeQ4cONbWSX375pXuZ9j3UaVo00Ok+aG2ojuKtCB9++KEJ3v369auQ7QE4Ng4dQXGM2wAABGHkyJGmebmypgjRAKe1ml988UWll8vJJ58so0ePNs3gAEKPPnYAUMm0dlHnr9NRrdoEqvPgeU50XNF0rjmdjFlrPivzsmLabHzRRReZefsAWAM1dgBQyXSCYR0ookFLp3DRfnc62S8AVDSCHQAAgE0weAIAAMAmCHYAAAA2QbADAACwCYIdAACATRDsAAAAbIJgBwAAYBMEOwAAAJsg2AEAANgEwQ4AAEDs4f8BuBVTU/w5FYMAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "result.plot(db=True)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "9", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "result.show_animation()" ] @@ -221,8 +134,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.10" + "pygments_lexer": "ipython3" } }, "nbformat": 4,