FDFD solver for simulating phase-driven fields through dielectric microelements
Project description
PyNJ
PyNJ is a small Python API for building finite-difference frequency-domain (FDFD) simulations of photonic nanojet-style microelement domains. It focuses on three pieces:
- a domain: wavelength, refractive indices, grid resolution, bounds, PML, and microelements
- a source line: the complex input field placed on a horizontal line in the domain
- a result: solved electromagnetic fields, intensity, coordinates, source, and permittivity map
The public package name on PyPI is PyNJ; the Python import is:
import pynj
For compatibility, import PyNJ maps to the same public API.
Installation
pip install PyNJ
PyNJ depends on numpy, autograd, ceviche, and matplotlib.
Quick Start
from pathlib import Path
import numpy as np
import pynj
out = Path("demo_output")
out.mkdir(exist_ok=True)
domain = pynj.domain(
lambda0=532e-9,
n_bg=1.0,
Lx=(-10e-6, 10e-6),
Ly=(-8e-6, 12e-6),
ppum=30,
input_phase_line=9e-6,
)
domain.add_microelement(n=1.49, radius=3e-6, x0=-3e-6, y0=4e-6, shape="square")
domain.add_microelement(n=1.49, radius=2.5e-6, x0=4e-6, y0=4e-6, shape="circle")
phase = np.linspace(0.0, 2.0 * np.pi, 120)
source_item = pynj.build_source_line(phase, x_bounds=(-2e-6, 2e-6), ppum=30)
src_line = domain.build_empty_input_line()
src_line.insert_source((-2e-6, 2e-6), source_item)
result = domain.solve(src_line, title="quick_start", save_path=out)
result.preview(save_path=out / "field.png")
Units
All physical distances are in meters.
For readability, examples often write micrometer-scale numbers as 10e-6 or
multiply by 1e6 when printing.
Domains
A domain is created with pynj.domain(...).
domain = pynj.domain(
lambda0=532e-9,
n_bg=1.0,
Lx=(-10e-6, 10e-6),
Ly=(-8e-6, 12e-6),
ppum=30,
pml_x=2e-6,
pml_y=2e-6,
input_phase_line=9e-6,
)
Domain Parameters
| Parameter | Type | Meaning |
|---|---|---|
lambda0 |
float |
Free-space wavelength in meters. Used to compute angular frequency. |
n_bg |
float |
Background refractive index. The base permittivity is n_bg ** 2. |
Lx |
float or (xmin, xmax) |
Physical x span. A float creates a centered domain from -Lx/2 to Lx/2; a tuple uses explicit bounds. |
Ly |
float or (ymin, ymax) |
Physical y span. A float creates a centered domain; a tuple uses explicit bounds. |
ppum |
int |
Pixels per micrometer. The grid spacing is dx = 1e-6 / ppum. |
pml_x |
float |
PML thickness on each x side, in meters. Default: 2e-6. |
pml_y |
float |
PML thickness on each y side, in meters. Default: 2e-6. |
input_phase_line |
float or None |
y-coordinate where the source line is injected. If omitted, PyNJ places it just above the highest microelement. |
After construction, the domain exposes:
| Attribute | Meaning |
|---|---|
domain.x_min, domain.x_max |
Physical x bounds without PML. |
domain.y_min, domain.y_max |
Physical y bounds without PML. |
domain.Lx, domain.Ly |
Physical width and height. |
domain.dx |
Grid spacing in meters. |
domain.source_y |
Actual y-coordinate used for the input source line. |
domain.source_x0 |
Center of the x support. |
domain.source_width |
Width of the source support. |
Source Line Position
domain.source_y is determined like this:
- If
input_phase_linewas provided, that value is used. - Otherwise, if the domain has microelements, the line is placed at
max(element.y0 + element.radius) + domain.dx. - Otherwise, it is placed just inside the top boundary at
domain.y_max - domain.dx.
During solving, the solver chooses the nearest y-grid index:
j_src = argmin(abs(y - domain.source_y))
The source is then inserted into the 2D simulation source array as:
source[:, j_src] = src_line
So the input field is a horizontal line at domain.source_y.
Microelements
Microelements are added with domain.add_microelement(...).
domain.add_microelement(
n=1.49,
radius=3e-6,
x0=0.0,
y0=4e-6,
shape="square",
)
Microelement Parameters
| Parameter | Type | Meaning |
|---|---|---|
n |
float |
Refractive index inside the microelement. The solver writes n ** 2 into the permittivity map. |
radius |
float |
Characteristic radius in meters. For squares this is the half-width. |
x0, y0 |
float |
Center position in meters. |
shape |
str |
Shape name. Supported: "circle", "square", "superformula". |
**shape_kwargs |
varies | Extra parameters for shape-specific masks. |
For shape="superformula", use:
| Parameter | Meaning |
|---|---|
m |
Symmetry/frequency parameter. |
n1, n2, n3 |
Superformula exponents. |
Example:
domain.add_microelement(
n=1.49,
radius=2.5e-6,
x0=4e-6,
y0=4e-6,
shape="superformula",
m=4,
n1=3.5,
n2=8.4,
n3=8.4,
)
PyNJ checks that microelements stay inside the physical domain. Overlapping microelements are allowed only when they have the same refractive index.
Source Lines
The solver expects a 1D complex input line. PyNJ represents this with
SourceLine.
There are two common workflows.
Full-Domain Source Line
Use this when you know the full input line:
src_line = domain.build_source_line(0.0)
Real values are interpreted as phase in radians and converted to
exp(1j * phase). Complex values are used directly.
Narrow Source Chunks
If you have a narrow source, build it with its own x bounds and insert it into a full line:
phase = np.linspace(0.0, np.pi, 120)
source_item = pynj.build_source_line(
phase,
x_bounds=(-2e-6, 2e-6),
ppum=30,
)
src_line = domain.build_empty_input_line()
src_line.insert_source((-2e-6, 2e-6), source_item)
This is important: domain.solve(...) does not guess where a narrow source
belongs. The x-position is encoded by insert_source(...). Once inserted, the
line has the full solver width and can be injected unambiguously.
SourceLine Attributes
| Attribute | Meaning |
|---|---|
values |
Complex source samples. |
x |
x-coordinate array in meters, if available. |
length |
Number of samples. |
resolution / dx |
Average x spacing in meters. |
x_bounds |
(x_min, x_max) in meters. |
physical_length |
x_max - x_min, in meters. |
occupied_intervals |
Intervals inserted with insert_source. |
SourceLine Methods
| Method | Meaning |
|---|---|
insert_source(x_pos, source) |
Insert a scalar, array, or another SourceLine into an interval. Rejects overlapping insertions. |
apply_mask(intervals) |
Keep only one interval or a list of intervals; set amplitude to zero elsewhere. |
subset(x_pos) |
Return a new SourceLine containing only an interval. |
preview(save_path=...) |
Plot the phase of the source line. |
save(title=..., path=...) |
Save only the source line to .npz. |
Load saved source lines with:
src_line = pynj.load_source_line("manual_input_line.npz")
load_source_line supports both new SourceLine.save(...) files and older
simulation .npz files that contain a src_line key.
Time Reversal
domain.timereversal(...) computes a source line by back-propagating from a
target point:
src_line = domain.timereversal(x_pnj=0.0, y_pnj=-2e-6)
src_line.apply_mask([(-9e-6, -1e-6), (1e-6, 9e-6)])
The returned object is a regular SourceLine, so it can be previewed, masked,
saved, loaded, or passed to domain.solve(...).
Solving
Run a simulation with:
result = domain.solve(src_line, title="run_001", save_path="results")
Solve Parameters
| Parameter | Meaning |
|---|---|
input_phase |
None, scalar phase, phase array, complex array, InputPhase, or SourceLine. |
title |
Optional title. Also used to name saved .npz files when save_path is a directory. |
save_path |
Output path for automatic result saving. Use None to disable automatic saving. |
If save_path is a directory and title="run_001", the solver writes
results/run_001.npz. If save_path=None, no automatic result file is written.
Where the Source Is Inserted
Inside the solver:
src_line = self._build_source_line(input_phase)
source = np.zeros_like(self.X, dtype=np.complex64)
source[:, self.j_src] = src_line
j_src is the y-index closest to domain.source_y. The x placement of narrow
sources must already be represented in the full-width SourceLine.values.
Results
domain.solve(...) returns a SimulationResult.
| Attribute | Meaning |
|---|---|
Hz |
Complex magnetic field. |
Ex, Ey |
Complex electric field components. |
Iz |
Normalized abs(Hz). |
x, y |
Coordinate axes in meters, including PML. |
src_line |
Injected full-width complex source line. |
source |
Full 2D source array. |
eps_r |
Relative permittivity map. |
j_src |
y-grid index of the source line. |
dx |
Grid spacing in meters. |
lambda0, omega |
Wavelength and angular frequency. |
NPML |
PML grid thickness as [NPMLx, NPMLy]. |
Save or plot a result:
result.preview(save_path="field.png")
result.save(path="result.npz", title="my_result")
Save methods write files and return None; they do not store hidden
save_path state on the objects.
Saving and Loading Domains
Domains are saved as readable JSON:
domain.save(title="two_lenses", path="two_lenses.json")
domain2 = pynj.load_domain("two_lenses.json")
Save methods also accept positional arguments:
domain.save("two_lenses", "two_lenses.json")
src_line.save("manual_input_line", "manual_input_line.npz")
result.save("my_result", "result.npz")
The JSON stores all domain parameters and microelements needed to reconstruct the same domain.
Plotting
Each object has a small preview helper:
domain.preview(save_path="domain.png")
src_line.preview(save_path="phase.png")
result.preview(save_path="field.png")
If save_path is omitted, Matplotlib calls plt.show().
PyNJ sets a writable Matplotlib config directory automatically when it is
imported, unless MPLCONFIGDIR is already set. The first plot can take a little
longer because Matplotlib builds its font cache; after that it should be reused.
The domain preview uses a black/white mask-style view and draws the input phase line. The source preview plots phase in radians. The field preview plots the normalized magnetic-field magnitude.
Examples
Examples are installed with the package under pynj.examples and can be run
with python -m:
python -m pynj.examples.domain_preview
python -m pynj.examples.source_lines
python -m pynj.examples.load_saved_source
python -m pynj.examples.time_reversal_forward
They write files to ./pynj_example_output.
The examples show:
- domain creation with explicit bounds
- square, circle, and superformula microelements
- readable domain JSON files
- manual source-line construction
- insertion of narrow source chunks into full input lines
- source-line saving/loading
- time-reversal source generation
- forward solving and result saving
Public API Summary
| API | Purpose |
|---|---|
pynj.domain(...) |
Build a simulation domain. |
domain.add_microelement(...) |
Add a lens/microelement. |
domain.preview(...) |
Plot the domain mask and source line. |
domain.save(...) |
Save a readable JSON domain. |
pynj.load_domain(...) |
Load a saved domain JSON. |
domain.build_empty_input_line() |
Build a full-width zero-amplitude SourceLine. |
domain.build_source_line(values) |
Build a full-width SourceLine. |
pynj.build_source_line(...) |
Build a standalone source chunk or line. |
pynj.load_source_line(...) |
Load a saved source-line .npz. |
domain.timereversal(...) |
Generate a time-reversal SourceLine. |
domain.solve(...) |
Run a forward solve. |
result.preview(...) |
Plot normalized abs(Hz). |
result.save(...) |
Save result arrays to .npz. |
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pynj-0.2.2.tar.gz.
File metadata
- Download URL: pynj-0.2.2.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
563b7c39b9d4d3cb3ca7a36cf1a7af0d72de2d9aa4828e472a2d1ff6c7acfc41
|
|
| MD5 |
7a46ca0d56f5e5ed971e883a873eef64
|
|
| BLAKE2b-256 |
0bcef84305a13bd33bfdf0faf82164050e1fdb0f40b1516eb1d12698c4fe5bd1
|
File details
Details for the file pynj-0.2.2-py3-none-any.whl.
File metadata
- Download URL: pynj-0.2.2-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b301fcdd24c1b3589c6643851cbcfa79196da86af2100c8216bfd0407ab2dd09
|
|
| MD5 |
90e3445ae751366f610c1599c6c114f2
|
|
| BLAKE2b-256 |
656d26f7fdaf3d4db8854020e680d41de23095f20817f72df5afe066bb77f8a1
|