init
Browse files- .gitignore +8 -0
- LICENSE +21 -0
- README.md +82 -13
- app.py +85 -0
- bpm/__init__.py +8 -0
- bpm/core.py +46 -0
- bpm/mode_solver.py +132 -0
- bpm/pml.py +24 -0
- bpm/refractive_index.py +52 -0
- examples/example_mmi.py +82 -0
- examples/example_waveguide.py +46 -0
- requirements.txt +4 -0
- setup.py +20 -0
- tests/__init__.py +1 -0
- tests/test_core.py +46 -0
- tests/test_mode_solver.py +52 -0
- tests/test_refractive_index.py +43 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
/tests/__pycache__
|
| 3 |
+
/my_bpm.egg-info
|
| 4 |
+
/bpm/__pycache__
|
| 5 |
+
*.png
|
| 6 |
+
/gds_designs/gds_env
|
| 7 |
+
gds_designs/.DS_Store
|
| 8 |
+
.DS_Store
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Wentao
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,13 +1,82 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BPM
|
| 2 |
+
Beam Propagation Method
|
| 3 |
+
|
| 4 |
+
**BPM** is a Python library for simulating beam propagation in integrated photonics using the Beam Propagation Method (BPM). The package provides functions to generate refractive index distributions for various structures (e.g., lenses, waveguides, and MMI splitters), a mode solver for slab waveguides, and BPM propagation routines with support for Perfectly Matched Layers (PML) for absorbing boundary conditions.
|
| 5 |
+
|
| 6 |
+
Currently it is 2D only, and use analytic solutions to launch slab modes. Propagation direction is upward and is called z. Transverse direction is x.
|
| 7 |
+
|
| 8 |
+
## Features
|
| 9 |
+
|
| 10 |
+
- Generate refractive index distributions:
|
| 11 |
+
- Spherical lens
|
| 12 |
+
- S-bend waveguide
|
| 13 |
+
- MMI-based splitter
|
| 14 |
+
- Solve for guided slab waveguide modes (even/odd modes)
|
| 15 |
+
- BPM propagation using a Runge-Kutta integrator
|
| 16 |
+
- PML boundary absorption
|
| 17 |
+
- [] Import from GDSII
|
| 18 |
+
|
| 19 |
+
## Installation
|
| 20 |
+
|
| 21 |
+
Clone the repository and install using pip:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
git clone https://github.com/jwt625/bpm.git
|
| 25 |
+
cd bpm
|
| 26 |
+
pip install -e .
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
## Examples
|
| 31 |
+
|
| 32 |
+
Slab mode solver and launcher:
|
| 33 |
+
|
| 34 |
+

|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
Refractive index distribution of an MMI:
|
| 38 |
+
|
| 39 |
+

|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
Simulated example MMI:
|
| 43 |
+
|
| 44 |
+

|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
Simulated example S bend. The waveguide is multimode:
|
| 48 |
+
|
| 49 |
+

|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
## References
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
Optical tomographic reconstruction based on multi-slice wave propagation method
|
| 58 |
+
- https://doi.org/10.1364/OE.25.022595
|
| 59 |
+
|
| 60 |
+
Light propagation through microlenses: a new simulation method
|
| 61 |
+
- https://doi.org/10.1364/AO.32.004984
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
Light propagation in graded-index optical fibers
|
| 65 |
+
- https://doi.org/10.1364/AO.17.003990
|
| 66 |
+
- this one is a classic
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
Numerical Simulation of Optical Wave Propagation with Examples in MATLAB
|
| 70 |
+
- https://spie.org/publications/book/866274
|
| 71 |
+
|
| 72 |
+
Photonic Devices for Telecommunications
|
| 73 |
+
- https://link.springer.com/book/10.1007/978-3-642-59889-0
|
| 74 |
+
- chapter 2
|
| 75 |
+
|
| 76 |
+
### Papers cited in chapter 2.2.1 of the book:
|
| 77 |
+
|
| 78 |
+
- Chung1990: An assessment of finite difference beam propagation method
|
| 79 |
+
- https://ieeexplore.ieee.org/abstract/document/59679
|
| 80 |
+
- Vassallo1992: Improvement of finite difference methods for step-index optical waveguides
|
| 81 |
+
- https://digital-library.theiet.org/doi/abs/10.1049/ip-j.1992.0024
|
| 82 |
+
|
app.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import tempfile
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
from bpm.refractive_index import generate_waveguide_n_r2
|
| 8 |
+
from bpm.mode_solver import slab_mode_source
|
| 9 |
+
from bpm.core import run_bpm
|
| 10 |
+
from bpm.pml import generate_sigma_x
|
| 11 |
+
|
| 12 |
+
def run_waveguide(w, l, L, n_WG, wavelength, ind_m):
|
| 13 |
+
# Simulation setup
|
| 14 |
+
domain_size = 50.0
|
| 15 |
+
z_total = 500.0
|
| 16 |
+
Nx, Nz = 256, 2000
|
| 17 |
+
x = np.linspace(-domain_size/2, domain_size/2, Nx)
|
| 18 |
+
z = np.linspace(0, z_total, Nz)
|
| 19 |
+
n0 = 1.0
|
| 20 |
+
|
| 21 |
+
# Refractive index map
|
| 22 |
+
n_r2 = generate_waveguide_n_r2(x, z, l, L, w, n_WG, n0)
|
| 23 |
+
|
| 24 |
+
# Mode source
|
| 25 |
+
E0 = slab_mode_source(x, w, n_WG, n0, wavelength, ind_m, x0=0)
|
| 26 |
+
E = np.zeros((Nx, Nz), dtype=np.complex128)
|
| 27 |
+
E[:, 0] = E0
|
| 28 |
+
|
| 29 |
+
# PML and BPM propagation
|
| 30 |
+
dx = domain_size / Nx
|
| 31 |
+
dz = z[1] - z[0]
|
| 32 |
+
sigma_x = generate_sigma_x(x, dx, wavelength, domain_size)
|
| 33 |
+
E_out = run_bpm(E, n_r2, x, z, dx, dz, n0, sigma_x, wavelength)
|
| 34 |
+
|
| 35 |
+
# Plotting
|
| 36 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 37 |
+
im = ax.imshow(
|
| 38 |
+
np.abs(E_out)**2,
|
| 39 |
+
extent=[x[0], x[-1], z[0], z[-1]],
|
| 40 |
+
origin='lower',
|
| 41 |
+
aspect='auto',
|
| 42 |
+
cmap='inferno'
|
| 43 |
+
)
|
| 44 |
+
ax.set_xlabel("x (µm)")
|
| 45 |
+
ax.set_ylabel("z (µm)")
|
| 46 |
+
ax.set_title("Waveguide BPM Propagation")
|
| 47 |
+
fig.colorbar(im, ax=ax, label="Intensity")
|
| 48 |
+
|
| 49 |
+
# Save data for download
|
| 50 |
+
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".npz")
|
| 51 |
+
np.savez(tmp_file.name, E_out=E_out, x=x, z=z)
|
| 52 |
+
tmp_file.close()
|
| 53 |
+
|
| 54 |
+
return fig, tmp_file.name
|
| 55 |
+
|
| 56 |
+
# Build Gradio interface
|
| 57 |
+
with gr.Blocks() as demo:
|
| 58 |
+
gr.Markdown("## Waveguide BPM Simulation")
|
| 59 |
+
|
| 60 |
+
with gr.Row():
|
| 61 |
+
with gr.Column(scale=1):
|
| 62 |
+
w_slider = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="Waveguide width w (µm)")
|
| 63 |
+
l_slider = gr.Slider(0.0, 10.0, value=5.0, step=0.1, label="Lateral offset l (µm)")
|
| 64 |
+
L_slider = gr.Slider(50.0, 500.0, value=200.0, step=10.0, label="S-bend length L (µm)")
|
| 65 |
+
n_WG_slider = gr.Slider(1.0, 2.0, value=1.1, step=0.01, label="Core refractive index n_WG")
|
| 66 |
+
wavelength_slider = gr.Slider(0.4, 1.6, value=0.532, step=0.01, label="Wavelength λ (µm)")
|
| 67 |
+
ind_m_slider = gr.Slider(0, 4, value=0, step=1, label="Mode index ind_m")
|
| 68 |
+
|
| 69 |
+
run_button = gr.Button("Run BPM")
|
| 70 |
+
download_button = gr.DownloadButton(label="Download data")
|
| 71 |
+
|
| 72 |
+
with gr.Column(scale=2):
|
| 73 |
+
plot_output = gr.Plot()
|
| 74 |
+
|
| 75 |
+
inputs = [w_slider, l_slider, L_slider, n_WG_slider, wavelength_slider, ind_m_slider]
|
| 76 |
+
|
| 77 |
+
# Connect run button
|
| 78 |
+
run_button.click(fn=run_waveguide, inputs=inputs, outputs=[plot_output, download_button])
|
| 79 |
+
|
| 80 |
+
# Auto-update on parameter change
|
| 81 |
+
for inp in inputs:
|
| 82 |
+
inp.change(fn=run_waveguide, inputs=inputs, outputs=[plot_output, download_button])
|
| 83 |
+
|
| 84 |
+
demo.launch()
|
| 85 |
+
|
bpm/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
my_bpm: A beam propagation method (BPM) simulation library for integrated photonics.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .core import run_bpm, compute_dE_dz
|
| 6 |
+
from .refractive_index import generate_lens_n_r2, generate_waveguide_n_r2, generate_MMI_n_r2
|
| 7 |
+
from .mode_solver import slab_mode_source
|
| 8 |
+
from .pml import generate_sigma_x
|
bpm/core.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
# Global factors; these might be computed more dynamically in a full implementation.
|
| 4 |
+
laplacian_factor = None
|
| 5 |
+
index_factor = None
|
| 6 |
+
|
| 7 |
+
def compute_dE_dz(E_slice, n_r2_slice, dx, n0, sigma_x, k0):
|
| 8 |
+
"""
|
| 9 |
+
Compute the derivative dE/dz using the BPM equation:
|
| 10 |
+
|
| 11 |
+
∂E/∂z = (i/(2 k0 n0)) (∂^2 E/∂x^2) + i (k0/(2 n0)) [n_r^2 - n0^2] E - sigma(x) E
|
| 12 |
+
"""
|
| 13 |
+
# Finite-difference Laplacian in x
|
| 14 |
+
laplacian_E = (np.roll(E_slice, 1, axis=0) - 2 * E_slice + np.roll(E_slice, -1, axis=0)) / dx**2
|
| 15 |
+
laplacian_term = (1j / (2 * k0 * n0)) * laplacian_E
|
| 16 |
+
index_term = 1j * (k0 / (2 * n0)) * (n_r2_slice - n0**2) * E_slice
|
| 17 |
+
damping_term = - sigma_x * E_slice
|
| 18 |
+
return laplacian_term + index_term + damping_term
|
| 19 |
+
|
| 20 |
+
def run_bpm(E, n_r2, x, z, dx, dz, n0, sigma_x, wavelength):
|
| 21 |
+
"""
|
| 22 |
+
Run the BPM propagation using an RK4 integrator.
|
| 23 |
+
|
| 24 |
+
Parameters:
|
| 25 |
+
E: initial field (2D array, shape (len(x), len(z)); only E[:,0] is used)
|
| 26 |
+
n_r2: refractive index squared distribution (2D array, shape (len(x), len(z)))
|
| 27 |
+
x, z: transverse and propagation coordinates
|
| 28 |
+
dx, dz: grid spacings in x and z
|
| 29 |
+
n0: background refractive index
|
| 30 |
+
sigma_x: 1D array for PML damping in x
|
| 31 |
+
wavelength: wavelength in um
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
E: propagated field (2D array)
|
| 35 |
+
"""
|
| 36 |
+
k0 = 2 * np.pi / wavelength
|
| 37 |
+
Nz = len(z)
|
| 38 |
+
for zi in range(1, Nz):
|
| 39 |
+
E_prev = E[:, zi-1]
|
| 40 |
+
n_r2_slice = n_r2[:, zi-1]
|
| 41 |
+
k1 = dz * compute_dE_dz(E_prev, n_r2_slice, dx, n0, sigma_x, k0)
|
| 42 |
+
k2 = dz * compute_dE_dz(E_prev + k1/2, n_r2_slice, dx, n0, sigma_x, k0)
|
| 43 |
+
k3 = dz * compute_dE_dz(E_prev + k2/2, n_r2_slice, dx, n0, sigma_x, k0)
|
| 44 |
+
k4 = dz * compute_dE_dz(E_prev + k3, n_r2_slice, dx, n0, sigma_x, k0)
|
| 45 |
+
E[:, zi] = E_prev + (k1 + 2*k2 + 2*k3 + k4) / 6
|
| 46 |
+
return E
|
bpm/mode_solver.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import warnings
|
| 3 |
+
|
| 4 |
+
def slab_mode_source(x, w, n_WG, n0, wavelength, ind_m=0, x0=0):
|
| 5 |
+
"""
|
| 6 |
+
Returns the normalized TE mode profile for a symmetric slab waveguide with a lateral shift x0.
|
| 7 |
+
"""
|
| 8 |
+
k0 = 2 * np.pi / wavelength
|
| 9 |
+
|
| 10 |
+
def f_even(beta):
|
| 11 |
+
if beta < n0*k0 or beta > n_WG*k0:
|
| 12 |
+
return None
|
| 13 |
+
inside = n_WG**2 * k0**2 - beta**2
|
| 14 |
+
outside = beta**2 - n0**2 * k0**2
|
| 15 |
+
if inside <= 0 or outside <= 0:
|
| 16 |
+
return None
|
| 17 |
+
kx = np.sqrt(inside)
|
| 18 |
+
kappa = np.sqrt(outside)
|
| 19 |
+
return kx * np.tan(kx * w / 2) - kappa
|
| 20 |
+
|
| 21 |
+
def f_odd(beta):
|
| 22 |
+
if beta < n0*k0 or beta > n_WG*k0:
|
| 23 |
+
return None
|
| 24 |
+
inside = n_WG**2 * k0**2 - beta**2
|
| 25 |
+
outside = beta**2 - n0**2 * k0**2
|
| 26 |
+
if inside <= 0 or outside <= 0:
|
| 27 |
+
return None
|
| 28 |
+
kx = np.sqrt(inside)
|
| 29 |
+
kappa = np.sqrt(outside)
|
| 30 |
+
sin_term = np.sin(kx * w / 2)
|
| 31 |
+
if abs(sin_term) < 1e-12:
|
| 32 |
+
return None
|
| 33 |
+
return - kx * (np.cos(kx * w / 2) / sin_term) - kappa
|
| 34 |
+
|
| 35 |
+
def valid_even(beta):
|
| 36 |
+
inside = n_WG**2 * k0**2 - beta**2
|
| 37 |
+
if inside <= 0:
|
| 38 |
+
return False
|
| 39 |
+
kx = np.sqrt(inside)
|
| 40 |
+
theta = kx * w / 2
|
| 41 |
+
m = int(np.floor(2 * theta / np.pi))
|
| 42 |
+
if m % 2 == 0:
|
| 43 |
+
if m == 0 and theta > (np.pi/2 - 0.1):
|
| 44 |
+
return False
|
| 45 |
+
return True
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def valid_odd(beta):
|
| 49 |
+
inside = n_WG**2 * k0**2 - beta**2
|
| 50 |
+
if inside <= 0:
|
| 51 |
+
return False
|
| 52 |
+
kx = np.sqrt(inside)
|
| 53 |
+
theta = kx * w / 2
|
| 54 |
+
m = int(np.floor(2 * theta / np.pi))
|
| 55 |
+
return (m % 2 == 1)
|
| 56 |
+
|
| 57 |
+
N = 2000
|
| 58 |
+
beta_scan = np.linspace(n0*k0, n_WG*k0, N)
|
| 59 |
+
even_intervals = []
|
| 60 |
+
odd_intervals = []
|
| 61 |
+
f_even_vals = [f_even(b) for b in beta_scan]
|
| 62 |
+
f_odd_vals = [f_odd(b) for b in beta_scan]
|
| 63 |
+
for i in range(N-1):
|
| 64 |
+
if (f_even_vals[i] is not None) and (f_even_vals[i+1] is not None):
|
| 65 |
+
if f_even_vals[i] * f_even_vals[i+1] < 0:
|
| 66 |
+
even_intervals.append((beta_scan[i], beta_scan[i+1]))
|
| 67 |
+
if (f_odd_vals[i] is not None) and (f_odd_vals[i+1] is not None):
|
| 68 |
+
if f_odd_vals[i] * f_odd_vals[i+1] < 0:
|
| 69 |
+
odd_intervals.append((beta_scan[i], beta_scan[i+1]))
|
| 70 |
+
|
| 71 |
+
def refine_root(f, b_left, b_right):
|
| 72 |
+
for _ in range(50):
|
| 73 |
+
b_mid = 0.5*(b_left+b_right)
|
| 74 |
+
val_mid = f(b_mid)
|
| 75 |
+
if val_mid is None:
|
| 76 |
+
b_right = b_mid
|
| 77 |
+
continue
|
| 78 |
+
if abs(val_mid) < 1e-9:
|
| 79 |
+
return b_mid
|
| 80 |
+
val_left = f(b_left)
|
| 81 |
+
if val_left is None or val_left*val_mid > 0:
|
| 82 |
+
b_left = b_mid
|
| 83 |
+
else:
|
| 84 |
+
b_right = b_mid
|
| 85 |
+
return b_mid
|
| 86 |
+
|
| 87 |
+
even_roots = []
|
| 88 |
+
for (b_left, b_right) in even_intervals:
|
| 89 |
+
root = refine_root(f_even, b_left, b_right)
|
| 90 |
+
if valid_even(root):
|
| 91 |
+
even_roots.append(root)
|
| 92 |
+
odd_roots = []
|
| 93 |
+
for (b_left, b_right) in odd_intervals:
|
| 94 |
+
root = refine_root(f_odd, b_left, b_right)
|
| 95 |
+
if valid_odd(root):
|
| 96 |
+
odd_roots.append(root)
|
| 97 |
+
|
| 98 |
+
modes = [("even", r) for r in even_roots] + [("odd", r) for r in odd_roots]
|
| 99 |
+
modes_sorted = sorted(modes, key=lambda tup: tup[1], reverse=True)
|
| 100 |
+
if len(modes_sorted) == 0:
|
| 101 |
+
raise ValueError("No guided slab modes found in [n0*k0, n_WG*k0].")
|
| 102 |
+
if ind_m >= len(modes_sorted):
|
| 103 |
+
warnings.warn(
|
| 104 |
+
f"Requested mode index {ind_m} >= found modes ({len(modes_sorted)}). Using highest mode index {len(modes_sorted)-1}.",
|
| 105 |
+
UserWarning
|
| 106 |
+
)
|
| 107 |
+
ind_m = len(modes_sorted) - 1
|
| 108 |
+
|
| 109 |
+
parity, beta_chosen = modes_sorted[ind_m]
|
| 110 |
+
inside = n_WG**2 * k0**2 - beta_chosen**2
|
| 111 |
+
outside = beta_chosen**2 - n0**2 * k0**2
|
| 112 |
+
kx = np.sqrt(inside)
|
| 113 |
+
kappa = np.sqrt(outside)
|
| 114 |
+
|
| 115 |
+
E = np.zeros_like(x, dtype=np.complex128)
|
| 116 |
+
if parity == "even":
|
| 117 |
+
for i, xi in enumerate(x):
|
| 118 |
+
xp = xi - x0
|
| 119 |
+
if abs(xp) <= w/2:
|
| 120 |
+
E[i] = np.cos(kx * xp)
|
| 121 |
+
else:
|
| 122 |
+
E[i] = np.cos(kx * (w/2)) * np.exp(-kappa * (abs(xp)-w/2))
|
| 123 |
+
else:
|
| 124 |
+
for i, xi in enumerate(x):
|
| 125 |
+
xp = xi - x0
|
| 126 |
+
if abs(xp) <= w/2:
|
| 127 |
+
E[i] = np.sin(kx * xp)
|
| 128 |
+
else:
|
| 129 |
+
E[i] = np.sign(xp) * np.sin(kx * (w/2)) * np.exp(-kappa * (abs(xp)-w/2))
|
| 130 |
+
norm = np.sqrt(np.trapz(np.abs(E)**2, x))
|
| 131 |
+
E /= norm
|
| 132 |
+
return E
|
bpm/pml.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
def generate_sigma_x(x, dx, wavelength, domain_size, sigma_max=0.5, pml_factor=5):
|
| 4 |
+
"""
|
| 5 |
+
Generate the 1D PML damping profile sigma(x) for the x-dimension.
|
| 6 |
+
|
| 7 |
+
Parameters:
|
| 8 |
+
x : 1D numpy array of x coordinates.
|
| 9 |
+
dx : grid spacing in x.
|
| 10 |
+
wavelength : wavelength in microns.
|
| 11 |
+
domain_size : total transverse domain size (in microns).
|
| 12 |
+
sigma_max : maximum damping value (default 0.5).
|
| 13 |
+
pml_factor : number of wavelengths to use for the PML thickness.
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
sigma_x : 1D numpy array of damping values.
|
| 17 |
+
"""
|
| 18 |
+
pml_thickness = int(pml_factor * wavelength / dx)
|
| 19 |
+
pml_width = pml_thickness * dx
|
| 20 |
+
x_edge = domain_size / 2 - pml_width
|
| 21 |
+
sigma_x = np.where(np.abs(x) > x_edge,
|
| 22 |
+
sigma_max * ((np.abs(x) - x_edge) / pml_width) ** 2,
|
| 23 |
+
0)
|
| 24 |
+
return sigma_x
|
bpm/refractive_index.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
def generate_lens_n_r2(x, z, lens_diameter, lens_thickness, R1, R2, n_lens, n0, lens_center_z, x_lens):
|
| 4 |
+
"""
|
| 5 |
+
Generate the squared refractive index distribution for a spherical lens.
|
| 6 |
+
"""
|
| 7 |
+
Nx = len(x)
|
| 8 |
+
Nz = len(z)
|
| 9 |
+
n_r2 = np.full((Nx, Nz), n0**2, dtype=np.float64)
|
| 10 |
+
z1 = lens_center_z - lens_thickness / 2.0
|
| 11 |
+
z2 = lens_center_z + lens_thickness / 2.0
|
| 12 |
+
z_first = z1 + (R1 - np.sqrt(np.maximum(R1**2 - (x - x_lens)**2, 0)))
|
| 13 |
+
z_second = z2 - (R2 - np.sqrt(np.maximum(R2**2 - (x - x_lens)**2, 0)))
|
| 14 |
+
for ix in range(Nx):
|
| 15 |
+
if abs(x[ix] - x_lens) > lens_diameter / 2:
|
| 16 |
+
continue
|
| 17 |
+
in_lens = (z >= z_first[ix]) & (z <= z_second[ix])
|
| 18 |
+
n_r2[ix, in_lens] = n_lens**2
|
| 19 |
+
return n_r2
|
| 20 |
+
|
| 21 |
+
def generate_waveguide_n_r2(x, z, l, L, w, n_WG, n0):
|
| 22 |
+
"""
|
| 23 |
+
Generate the squared refractive index distribution for an S-bend waveguide.
|
| 24 |
+
"""
|
| 25 |
+
Nx = len(x)
|
| 26 |
+
Nz = len(z)
|
| 27 |
+
n_r2 = np.full((Nx, Nz), n0**2, dtype=np.float64)
|
| 28 |
+
x_c = (l / L) * z - (l / (2 * np.pi)) * np.sin((2 * np.pi / L) * z)
|
| 29 |
+
x_c = np.clip(x_c, 0, l)
|
| 30 |
+
for iz in range(Nz):
|
| 31 |
+
lower_edge = x_c[iz] - w / 2.0
|
| 32 |
+
upper_edge = x_c[iz] + w / 2.0
|
| 33 |
+
in_wg = (x >= lower_edge) & (x <= upper_edge)
|
| 34 |
+
n_r2[in_wg, iz] = n_WG**2
|
| 35 |
+
return n_r2
|
| 36 |
+
|
| 37 |
+
def generate_MMI_n_r2(x, z, z_MMI_start, L_MMI, w_MMI, w_wg, d, n_WG, n_MMI, n0):
|
| 38 |
+
"""
|
| 39 |
+
Generate the squared refractive index distribution for an MMI-based splitter.
|
| 40 |
+
"""
|
| 41 |
+
Nx = len(x)
|
| 42 |
+
Nz = len(z)
|
| 43 |
+
X, Z = np.meshgrid(x, z, indexing="ij")
|
| 44 |
+
n_r2 = np.full((Nx, Nz), n0**2, dtype=np.float64)
|
| 45 |
+
z_MMI_end = z_MMI_start + L_MMI
|
| 46 |
+
mask_input = (Z < z_MMI_start) & (((np.abs(X + d/2) <= w_wg/2) | (np.abs(X - d/2) <= w_wg/2)))
|
| 47 |
+
mask_MMI = (Z >= z_MMI_start) & (Z <= z_MMI_end) & (np.abs(X) <= w_MMI/2)
|
| 48 |
+
mask_output = (Z > z_MMI_end) & (((np.abs(X + d/2) <= w_wg/2) | (np.abs(X - d/2) <= w_wg/2)))
|
| 49 |
+
n_r2[mask_input] = n_WG**2
|
| 50 |
+
n_r2[mask_output] = n_WG**2
|
| 51 |
+
n_r2[mask_MMI] = n_MMI**2
|
| 52 |
+
return n_r2
|
examples/example_mmi.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#%%
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
from bpm.refractive_index import generate_MMI_n_r2
|
| 5 |
+
from bpm.mode_solver import slab_mode_source
|
| 6 |
+
from bpm.core import run_bpm
|
| 7 |
+
from bpm.pml import generate_sigma_x
|
| 8 |
+
|
| 9 |
+
# Simulation parameters
|
| 10 |
+
domain_size = 50.0 # um (transverse)
|
| 11 |
+
z_total = 250.0 # um (propagation length)
|
| 12 |
+
Nx = 256
|
| 13 |
+
Nz = 1024
|
| 14 |
+
x = np.linspace(-domain_size/2, domain_size/2, Nx)
|
| 15 |
+
z = np.linspace(0, z_total, Nz)
|
| 16 |
+
|
| 17 |
+
# MMI structure parameters
|
| 18 |
+
z_MMI_start = 50.0
|
| 19 |
+
L_MMI = 130.0 # MMI region length = 40 um
|
| 20 |
+
w_MMI = 8.0 # MMI region width = 40 um
|
| 21 |
+
w_wg = 2.0
|
| 22 |
+
d = 4.0
|
| 23 |
+
|
| 24 |
+
n0 = 1.0
|
| 25 |
+
n_WG = 1.1
|
| 26 |
+
n_MMI = 1.1
|
| 27 |
+
|
| 28 |
+
n_r2 = generate_MMI_n_r2(x, z, z_MMI_start, L_MMI, w_MMI, w_wg, d, n_WG, n_MMI, n0)
|
| 29 |
+
|
| 30 |
+
# Launch a slab mode from the left input waveguide
|
| 31 |
+
# Shift the launched mode so that its center aligns with x = -d/2.
|
| 32 |
+
E0 = slab_mode_source(x, w=w_wg, n_WG=n_WG, n0=n0, wavelength=0.532, ind_m=0, x0=-d/2)
|
| 33 |
+
|
| 34 |
+
# Create initial field
|
| 35 |
+
E = np.zeros((Nx, Nz), dtype=np.complex128)
|
| 36 |
+
E[:, 0] = E0
|
| 37 |
+
|
| 38 |
+
# Generate PML profile in x
|
| 39 |
+
dx = domain_size / Nx
|
| 40 |
+
sigma_x = generate_sigma_x(x, dx, 0.532, domain_size, sigma_max=0.5, pml_factor=5)
|
| 41 |
+
|
| 42 |
+
# Run BPM propagation
|
| 43 |
+
E_out = run_bpm(E, n_r2, x, z, dx, z[1]-z[0], n0, sigma_x, 0.532)
|
| 44 |
+
|
| 45 |
+
# Plot final intensity using Plotly
|
| 46 |
+
fig1 = go.Figure(data=go.Heatmap(
|
| 47 |
+
z=(np.abs(E_out)**2).T,
|
| 48 |
+
x=x,
|
| 49 |
+
y=z,
|
| 50 |
+
colorscale='inferno',
|
| 51 |
+
colorbar=dict(title='Intensity')
|
| 52 |
+
))
|
| 53 |
+
|
| 54 |
+
fig1.update_layout(
|
| 55 |
+
title='MMI Splitter BPM Propagation',
|
| 56 |
+
xaxis_title='x (um)',
|
| 57 |
+
yaxis_title='z (um)',
|
| 58 |
+
width=800,
|
| 59 |
+
height=600
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
fig1.show()
|
| 63 |
+
|
| 64 |
+
# Plot refractive index profile using Plotly
|
| 65 |
+
fig2 = go.Figure(data=go.Heatmap(
|
| 66 |
+
z=np.sqrt(n_r2).T,
|
| 67 |
+
x=x,
|
| 68 |
+
y=z,
|
| 69 |
+
colorscale='inferno',
|
| 70 |
+
colorbar=dict(title='Refractive Index')
|
| 71 |
+
))
|
| 72 |
+
|
| 73 |
+
fig2.update_layout(
|
| 74 |
+
title='Refractive Index Profile',
|
| 75 |
+
xaxis_title='x (um)',
|
| 76 |
+
yaxis_title='z (um)',
|
| 77 |
+
width=800,
|
| 78 |
+
height=600
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
fig2.show()
|
| 82 |
+
# %%
|
examples/example_waveguide.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#%%
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from bpm.refractive_index import generate_waveguide_n_r2
|
| 5 |
+
from bpm.mode_solver import slab_mode_source
|
| 6 |
+
from bpm.core import run_bpm
|
| 7 |
+
from bpm.pml import generate_sigma_x
|
| 8 |
+
|
| 9 |
+
# Simulation parameters
|
| 10 |
+
domain_size = 50.0 # um
|
| 11 |
+
z_total = 500.0 # um
|
| 12 |
+
Nx = 256
|
| 13 |
+
Nz = 2000
|
| 14 |
+
x = np.linspace(-domain_size/2, domain_size/2, Nx)
|
| 15 |
+
z = np.linspace(0, z_total, Nz)
|
| 16 |
+
|
| 17 |
+
# Waveguide parameters
|
| 18 |
+
l = 5.0 # lateral offset
|
| 19 |
+
L = 200.0 # length of S-bend
|
| 20 |
+
w = 1.0 # waveguide width
|
| 21 |
+
n0 = 1.0
|
| 22 |
+
n_WG = 1.1
|
| 23 |
+
|
| 24 |
+
n_r2 = generate_waveguide_n_r2(x, z, l, L, w, n_WG, n0)
|
| 25 |
+
|
| 26 |
+
# Launch a mode; here we use a mode source with no shift (x0 = 0)
|
| 27 |
+
E0 = slab_mode_source(x, w, n_WG, n0, wavelength=0.532, ind_m=0, x0=0)
|
| 28 |
+
E = np.zeros((Nx, Nz), dtype=np.complex128)
|
| 29 |
+
E[:, 0] = E0
|
| 30 |
+
|
| 31 |
+
dx = domain_size / Nx
|
| 32 |
+
sigma_x = generate_sigma_x(x, dx, 0.532, domain_size, sigma_max=0.5, pml_factor=5)
|
| 33 |
+
|
| 34 |
+
E_out = run_bpm(E, n_r2, x, z, dx, z[1]-z[0], n0, sigma_x, 0.532)
|
| 35 |
+
|
| 36 |
+
plt.figure(figsize=(8,6))
|
| 37 |
+
plt.imshow((np.abs(E_out)**2).T, extent=[x[0], x[-1], z[0], z[-1]],
|
| 38 |
+
origin='lower', aspect='auto', cmap='inferno',
|
| 39 |
+
vmin=0, vmax=0.6)
|
| 40 |
+
plt.xlabel("x (um)")
|
| 41 |
+
plt.ylabel("z (um)")
|
| 42 |
+
plt.title("Waveguide BPM Propagation")
|
| 43 |
+
plt.colorbar(label="Intensity")
|
| 44 |
+
plt.show()
|
| 45 |
+
|
| 46 |
+
# %%
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
matplotlib
|
| 3 |
+
pytest
|
| 4 |
+
plotly
|
setup.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name='my_bpm',
|
| 5 |
+
version='0.1.0',
|
| 6 |
+
description='A BPM simulation library for integrated photonics',
|
| 7 |
+
author='Your Name',
|
| 8 |
+
author_email='your.email@example.com',
|
| 9 |
+
url='https://github.com/jwt625/bpm', # adjust as needed
|
| 10 |
+
packages=find_packages(),
|
| 11 |
+
install_requires=[
|
| 12 |
+
'numpy',
|
| 13 |
+
'matplotlib',
|
| 14 |
+
],
|
| 15 |
+
classifiers=[
|
| 16 |
+
'Development Status :: 3 - Alpha',
|
| 17 |
+
'Intended Audience :: Science/Research',
|
| 18 |
+
'Programming Language :: Python :: 3',
|
| 19 |
+
],
|
| 20 |
+
)
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file can be empty; it just makes 'tests' a package.
|
tests/test_core.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
from bpm.core import run_bpm, compute_dE_dz
|
| 4 |
+
from bpm.pml import generate_sigma_x
|
| 5 |
+
|
| 6 |
+
def test_core_propagation_plot():
|
| 7 |
+
# Simulation parameters
|
| 8 |
+
domain_size = 50 # um
|
| 9 |
+
wavelength = 0.532
|
| 10 |
+
n0 = 1.0
|
| 11 |
+
Nx = 256
|
| 12 |
+
Nz = 100
|
| 13 |
+
dx = domain_size / Nx
|
| 14 |
+
dz = 2 * dx
|
| 15 |
+
x = np.linspace(-domain_size/2, domain_size/2, Nx)
|
| 16 |
+
z = np.linspace(0, dz * Nz, Nz)
|
| 17 |
+
|
| 18 |
+
# Create an initial Gaussian field
|
| 19 |
+
E_init = np.exp(-x**2 / 2)
|
| 20 |
+
# Make the field 2D (each column is the same as initial condition)
|
| 21 |
+
E = np.tile(E_init, (Nz, 1)).T # shape (Nx, Nz)
|
| 22 |
+
|
| 23 |
+
# Use a homogeneous refractive index distribution (n0 everywhere)
|
| 24 |
+
n_r2 = np.full((Nx, Nz), n0**2, dtype=np.float64)
|
| 25 |
+
|
| 26 |
+
# Generate the PML damping profile in x
|
| 27 |
+
sigma_x = generate_sigma_x(x, dx, wavelength, domain_size, sigma_max=0.5, pml_factor=5)
|
| 28 |
+
|
| 29 |
+
# Run BPM propagation
|
| 30 |
+
E_out = run_bpm(E.copy(), n_r2, x, z, dx, dz, n0, sigma_x, wavelength)
|
| 31 |
+
|
| 32 |
+
# Plot the final intensity distribution
|
| 33 |
+
plt.figure(figsize=(8, 6))
|
| 34 |
+
plt.imshow(np.abs(E_out)**2, extent=[x[0], x[-1], z[0], z[-1]],
|
| 35 |
+
origin='lower', aspect='auto', cmap='inferno')
|
| 36 |
+
plt.xlabel("x (um)")
|
| 37 |
+
plt.ylabel("z (um)")
|
| 38 |
+
plt.title("Final Field Intensity from BPM Propagation")
|
| 39 |
+
plt.colorbar(label="Intensity")
|
| 40 |
+
plt.tight_layout()
|
| 41 |
+
# Save the figure as a PNG file
|
| 42 |
+
plt.savefig("core_test_field.png")
|
| 43 |
+
plt.close()
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
test_core_propagation_plot()
|
tests/test_mode_solver.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import plotly.tools as tls
|
| 4 |
+
import plotly.io as pio
|
| 5 |
+
import warnings
|
| 6 |
+
from bpm.mode_solver import slab_mode_source
|
| 7 |
+
|
| 8 |
+
def test_slab_mode_plot():
|
| 9 |
+
# Define transverse coordinate and waveguide parameters
|
| 10 |
+
x = np.linspace(-10, 10, 1000) # Adjust range/resolution as needed
|
| 11 |
+
w = 5.0 # waveguide width in microns
|
| 12 |
+
n_WG = 1.1 # core refractive index
|
| 13 |
+
n0 = 1.0 # cladding refractive index
|
| 14 |
+
wavelength = 0.532 # in microns
|
| 15 |
+
|
| 16 |
+
# Try to compute up to 5 modes
|
| 17 |
+
num_modes = 5
|
| 18 |
+
mode_fields = []
|
| 19 |
+
mode_indices = []
|
| 20 |
+
|
| 21 |
+
for m in range(num_modes):
|
| 22 |
+
try:
|
| 23 |
+
E_mode = slab_mode_source(x, w, n_WG, n0, wavelength, ind_m=m)
|
| 24 |
+
mode_fields.append(E_mode)
|
| 25 |
+
mode_indices.append(m)
|
| 26 |
+
except Exception as err:
|
| 27 |
+
warnings.warn(f"Mode {m} not found: {err}. Stopping mode search.")
|
| 28 |
+
break
|
| 29 |
+
|
| 30 |
+
# Plot the real part of each mode with an offset for clarity
|
| 31 |
+
plt.rcParams.update({'font.size': 14})
|
| 32 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 33 |
+
offset = 1.0 # Vertical offset between mode plots
|
| 34 |
+
for i, E_mode in enumerate(mode_fields):
|
| 35 |
+
ax.plot(x, np.real(E_mode) + i * offset, label=f"Mode {mode_indices[i]}")
|
| 36 |
+
ax.set_xlabel("x (µm)")
|
| 37 |
+
ax.set_ylabel("Field amplitude (Real part)")
|
| 38 |
+
ax.set_title("Slab Waveguide TE Modes")
|
| 39 |
+
ax.legend()
|
| 40 |
+
plt.grid(True)
|
| 41 |
+
plt.tight_layout()
|
| 42 |
+
# Save the plot as a PNG file
|
| 43 |
+
plt.savefig("test_mode_solver.png")
|
| 44 |
+
# plt.show()
|
| 45 |
+
|
| 46 |
+
# Optionally convert the matplotlib figure to a Plotly figure for interactive viewing
|
| 47 |
+
# plotly_fig = tls.mpl_to_plotly(fig)
|
| 48 |
+
# plotly_fig.update_layout(legend=dict(font=dict(size=12)))
|
| 49 |
+
# pio.show(plotly_fig)
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
test_slab_mode_plot()
|
tests/test_refractive_index.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
from bpm.refractive_index import generate_MMI_n_r2
|
| 4 |
+
|
| 5 |
+
def test_refractive_index_distribution():
|
| 6 |
+
# Simulation parameters
|
| 7 |
+
domain_size = 50.0 # um, transverse extent
|
| 8 |
+
Nx = 256
|
| 9 |
+
Nz = 512
|
| 10 |
+
x = np.linspace(-domain_size / 2, domain_size / 2, Nx)
|
| 11 |
+
z = np.linspace(0, 200, Nz)
|
| 12 |
+
|
| 13 |
+
# MMI structure parameters:
|
| 14 |
+
z_MMI_start = 50.0 # MMI region begins at z = 50 um
|
| 15 |
+
L_MMI = 130.0 # MMI region length = 40 um
|
| 16 |
+
w_MMI = 8.0 # MMI region width = 40 um
|
| 17 |
+
w_wg = 2.0 # input/output waveguide width = 4 um
|
| 18 |
+
d = 4.0 # center-to-center separation of waveguides = 12 um
|
| 19 |
+
|
| 20 |
+
# Refractive indices:
|
| 21 |
+
n0 = 1.0 # background index
|
| 22 |
+
n_WG = 1.1 # waveguide index
|
| 23 |
+
n_MMI = 1.1 # MMI region index
|
| 24 |
+
|
| 25 |
+
# Generate the refractive index distribution.
|
| 26 |
+
n_r2 = generate_MMI_n_r2(x, z, z_MMI_start, L_MMI, w_MMI, w_wg, d, n_WG, n_MMI, n0)
|
| 27 |
+
|
| 28 |
+
# Plot the refractive index distribution (plotting sqrt(n_r2) to get n_r).
|
| 29 |
+
plt.figure(figsize=(8, 6))
|
| 30 |
+
plt.imshow(np.sqrt(n_r2).T, extent=[x[0], x[-1], z[0], z[-1]],
|
| 31 |
+
origin='lower', aspect='auto', cmap='viridis')
|
| 32 |
+
plt.xlabel("x (um)")
|
| 33 |
+
plt.ylabel("z (um)")
|
| 34 |
+
plt.title("MMI Splitter Refractive Index Distribution (n_r)")
|
| 35 |
+
plt.colorbar(label="n_r")
|
| 36 |
+
plt.tight_layout()
|
| 37 |
+
|
| 38 |
+
# Save the plot as a PNG file
|
| 39 |
+
plt.savefig("refractive_index_test.png")
|
| 40 |
+
plt.close()
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
test_refractive_index_distribution()
|