Metadata-Version: 2.4
Name: PyNJ
Version: 0.2.1
Summary: FDFD solver for simulating phase-driven fields through dielectric microelements
Author-email: Tobias Abilock Mikkelsen <tobias.ab.mikk@gmail.com>, Cristian Placinta <cristianplacinta04@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/tmname64/PyNJ
Project-URL: Repository, https://github.com/tmname64/PyNJ
Project-URL: Bug Tracker, https://github.com/tmname64/PyNJ/issues
Keywords: photonics,nanojet,FDFD,simulation,timereversal,time-reversal,pnj
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Intended Audience :: Science/Research
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: autograd
Requires-Dist: ceviche
Requires-Dist: matplotlib
Dynamic: license-file

# 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:

```python
import pynj
```

For compatibility, `import PyNJ` maps to the same public API.

## Installation

```bash
pip install PyNJ
```

PyNJ depends on `numpy`, `autograd`, `ceviche`, and `matplotlib`.

## Quick Start

```python
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(...)`.

```python
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:

1. If `input_phase_line` was provided, that value is used.
2. Otherwise, if the domain has microelements, the line is placed at
   `max(element.y0 + element.radius) + domain.dx`.
3. Otherwise, it is placed at `domain.y_max + domain.dx`.

During solving, the solver chooses the nearest y-grid index:

```python
j_src = argmin(abs(y - domain.source_y))
```

The source is then inserted into the 2D simulation source array as:

```python
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(...)`.

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
domain.save(title="two_lenses", path="two_lenses.json")
domain2 = pynj.load_domain("two_lenses.json")
```

The JSON stores all domain parameters and microelements needed to reconstruct
the same domain.

## Plotting

Each object has a small preview helper:

```python
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. You should not need to add
`os.environ["MPLCONFIGDIR"] = ...` in your scripts.

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`:

```bash
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
