Metadata-Version: 2.4
Name: lumicron_api
Version: 0.3.7
Summary: Layout scripting for photonic and electronic chip design
Author: Elyon Semiconductor
License-Expression: MIT
Project-URL: Homepage, https://pypi.org/project/lumicron-api/
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: gdstk>=0.9
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# Lumicron API

Layout scripting for photonic and electronic chip design.

Define cells, place components, route waveguides, and export to GDS/OASIS — all in Python.

## Install

```bash
pip install lumicron_api
```

Requires Python 3.12+.

## Quick Start

```python
import lumicron_api.all as lm
import lumicron_api.pdks.elyon_demo.all as pdk

@lm.cell_params
def MZI(arm_length: float = 300,
        delay_y: float = 50):
    c = lm.CELL("MZI")

    # Components
    splitter = pdk.YSplitter(layer=pdk.LAYER.SILC)
    combiner = pdk.YSplitter(layer=pdk.LAYER.SILC)

    # Add and place
    s = c.add(splitter)
    k = c.add(combiner)
    c.Place(s).at((0, 0))
    c.Place(k).using("i1").at((arm_length + 50, 0)).rotate_by(180)

    # Route
    c.Route(s.port["o1"], k.port["o2"], radius=10)
    c.Route(s.port["o2"], k.port["o1"], radius=10).jog("S", by=delay_y)

    return c

mzi = MZI()
mzi.to_layout().to_gds("mzi.gds")
```

## Features

- **Hierarchical Cells** — build complex designs from reusable parametric components
- **Fluid Chain-based Syntax** - chain verbs and descriptors to your place and route commands using intuitive commands to "speak" your layout into existence. `c.Place(mmi).using("in1").relative_to(heater.port["in"]).move_by(dx=-50, dy=-50).rotate_by(90)`
- **Intuitive Placement System** - anchor-based positioning with directional helpers (`.above()`, `.below()`, `.right_of()`, `.left_of()`) and `.move_by()` offset helper
- **Smart Routing** — auto-router that automatically selects route style based on port position, overridable by `style="manhattan"`, `"sbend"`, or `"direct"`. Automatically validates bend radius and automatically tapers routes with flexible positional placement
- **Robust routing** — routing supported for ports at arbitrary angles. Contains intuitive route management via `.go()` commands: `c.Route(coupler.port["out"], mod_array.port["m0_in"], bend_radius=25).go("N", to=mod_array.S, dy=-100).go("W", to=mod_array.port["m0_in"])`. Allows custom bend and taper PCell to be auto-placed into routes
- **Route shaping** — `.jog(direction, by)` inserts a U-shaped manhattan detour at the route's bend radius for delay lines, MZI arms, and pad fanout
- **Bend types** — circular (default), euler (clothoid), and custom bend PCells with adjustable `p` parameter
- **Bundle routing** — `c.RouteBundle(ports_a, ports_b, spacing=10)` routes multiple port pairs with a single call
- **Layout-aware routing** — `c.Route(a, b, clearance=5)` auto-detours routes around placed cell bounding boxes, bend-radius-aware with path simplification
- **Waveguide** — standalone path primitive for spirals, delay lines, and tapered structures with custom bend PCell support
- **Arrays** — 1D and 2D arrays with per-element rotation
- **Port promotion** — access deeply nested ports from any level in the hierarchy
- **Shapes** — Rectangle, Circle, Ring, Oval, Arc, Triangle, Parallelogram, Trapezoid
- **PDK support** — ships with Elyon Demo PDK (17 layers, 11 components, 3 route profiles)
- **Export** — GDSII, OASIS with auto-filename from cell name

## Placement

```python
# Absolute
c.Place(shape).at((100, 200))

# Relative
c.Place(pad).right_of(coupler)
c.Place(stalk).below(segment).move_by(dy=5)

# Anchor + rotate
c.Place(splitter).using("i1").at(frame.NW).rotate_by(90)
```

## Routing

```python
# Auto-route with circular bends
c.Route(port_a, port_b, radius=10)

# Euler bends (smooth curvature transition)
c.Route(port_a, port_b, radius=10, bend="euler")
c.Route(port_a, port_b, radius=10, bend="euler", p=0.3)  # adjust euler fraction

# Off-angle ports — auto-detected
c.Route(angled_port_a, angled_port_b, radius=25)

# Force a routing style
c.Route(port_a, port_b, radius=10, style="manhattan")
c.Route(port_a, port_b, radius=10, style="sbend")
c.Route(port_a, port_b, radius=10, style="direct")

# Custom bend PCell (user-defined 90° bend geometry)
c.Route(port_a, port_b, bend=my_euler_bend_cell)

# Manual with GPS-style directions
c.Route(port_a, port_b, radius=15) \
    .go("E", by=50) \
    .go("N", to=port_b, dy=-20)

# Route jog — U-shaped detour for delay lines, MZI arms, pad fanout
c.Route(port_a, port_b, radius=10).jog("S", by=50)

# Positional taper — place taper 50µm from port A
c.Route(port_a, port_b, radius=10).start_taper(length=10, at=50)

# With route profile
c.Route(port_a, port_b, profile=pdk.RPROF.siln_strip)

# Layout-aware routing — detour around placed cells
c.Route(port_a, port_b, radius=10, clearance=5)

# Control distance from port to first/last bend (direct paths)
c.Route(port_a, port_b, radius=10, start_straight=20, end_straight=15)

# Bundle routing — route multiple port pairs at once
c.RouteBundle(
    ports_a=[ref_a.port["o1"], ref_a.port["o2"]],
    ports_b=[ref_b.port["i1"], ref_b.port["i2"]],
    spacing=10, radius=10,
)
```

## Waveguide

```python
# Standalone path from points — auto-generates i1/o1 ports
wg = lm.Waveguide(
    points=[(0, 0), (50, 0), (50, 100), (100, 100)],
    layer=pdk.LAYER.SILC, width=0.5, radius=10,
)

# Tapered waveguide (width interpolates linearly)
wg = lm.Waveguide(
    points=spiral_points,
    layer=pdk.LAYER.SILC, width=0.2, width_end=1.0, radius=10,
)

# Custom bend PCell in waveguide
wg = lm.Waveguide(
    points=[(0, 0), (50, 0), (50, 50)],
    layer=pdk.LAYER.SILC, width=0.5, bend=my_bend_cell,
)

ref = c.add(wg)
c.Route(some_port, ref.port["o1"], radius=10)
```

## Arrays

```python
couplers = lm.ARRAY(coupler, count=8, pitch=(0, 127), rotation=90)
arr = c.add(couplers)
c.Place(arr).at((0, 0))

# Access individual element ports
arr[0].port["o1"]
arr[3].C
```

## Parametric Cells

```python
@lm.cell_params
def Ring(radius: float = 50, width: float = 5):
    c = lm.CELL("Ring")
    c.add(lm.Ring(outer_radius=radius, width=width, layer=pdk.LAYER.SILC))
    return c

Ring()              # cell name: "Ring"
Ring(radius=100)    # cell name: "Ring_radius=100"
```

## Documentation

See [`docs/user_guide.md`](docs/user_guide.md) for the full API reference.

## License

MIT
