jwt625 commited on
Commit
fa34304
·
1 Parent(s): 176df7d
.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
- title: BPM
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.28.0
8
- app_file: app.py
9
- pinned: false
10
- short_description: beam go brrrr
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ![image](https://github.com/user-attachments/assets/5aad97f5-7521-431d-8578-9c7655831798)
35
+
36
+
37
+ Refractive index distribution of an MMI:
38
+
39
+ ![image](https://github.com/user-attachments/assets/e15c0aac-7a6e-419b-9484-62c910e5ca1e)
40
+
41
+
42
+ Simulated example MMI:
43
+
44
+ ![image](https://github.com/user-attachments/assets/d3ee4359-02d7-42cb-b568-d2f0eb55f7a0)
45
+
46
+
47
+ Simulated example S bend. The waveguide is multimode:
48
+
49
+ ![image](https://github.com/user-attachments/assets/fdf6e0ba-5684-4312-8bb1-c2ab070e5de5)
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()