Metadata-Version: 2.4
Name: elvis-lvs
Version: 0.1.6
Requires-Dist: build>=1.4.0 ; extra == 'dev'
Requires-Dist: gdsfactory>=9.34.0 ; extra == 'dev'
Requires-Dist: gdsfactoryplus>=1.3.13 ; extra == 'dev'
Requires-Dist: maturin>=1.11.5 ; extra == 'dev'
Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
Requires-Dist: pytest-regressions>=2.9.1 ; extra == 'dev'
Requires-Dist: klayout ; extra == 'klayout'
Provides-Extra: dev
Provides-Extra: klayout
License-File: LICENSE
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# Elvis 0.1.6

A simple LVS (Layout vs. Schematic) tool for GDSFactory.

## Features

- Extract netlists from GDS files (kfactory/gdsfactory format)
- Run LVS comparison between schematic and layout
- Graph-based connectivity tracing through 2-port routing components
- Hierarchical LVS with explicit child netlist files
- Open detection for dangling ports, with equivalent port grouping
- Geometric short detection with per-layer and cross-layer support
- Array instance support (GDS `AREF` expansion)
- Output errors in KLayout lyrdb format, YAML, or JSON
- Convert between netlist formats
- CLI and Python bindings

## Building

```bash
cargo build --release
```

## Quickstart

### 1. Extract a netlist from a GDS file

```bash
elvis extract path/to/layout.gds
```

Output:

```yaml
instances:
  mmi: mmi1x2
  wg_in: straight
  wg_out1: straight
nets:
  - p1: wg_in,o2
    p2: mmi,o1
  - p1: mmi,o2
    p2: wg_out1,o1
ports:
  o1: wg_in,o1
  o2: wg_out1,o2
```

### 2. Run LVS

Compare a schematic netlist (.pic.yml) against a layout (GDS):

```bash
elvis lvs layout.gds schematic.pic.yml
```

Output formats:

```bash
# KLayout lyrdb (default) - viewable in KLayout
elvis lvs layout.gds schematic.pic.yml -o report.lyrdb

# YAML
elvis lvs layout.gds schematic.pic.yml -f yaml

# JSON
elvis lvs layout.gds schematic.pic.yml -f json

# Custom port matching tolerance (default: 1nm)
elvis lvs layout.gds schematic.pic.yml -t 10

# Geometric short detection on layer 1/0
elvis lvs layout.gds schematic.pic.yml --short-layer 1,0

# Cross-layer short detection (e.g., M1 connects to M2 through VIA)
elvis lvs layout.gds schematic.pic.yml \
    --short-layer 1,0 --short-layer 3,0 \
    --connected-layers 1,0:2,0 --connected-layers 2,0:3,0

# Equivalent port grouping (suppress opens for equivalent ports)
elvis lvs layout.gds schematic.pic.yml --equivalent-ports pad:e1,e2,e3,e4

# Hierarchical LVS (pass all child .pic.yml files explicitly)
elvis lvs layout.gds mzi.pic.yml mmi1x2.pic.yml

# Override component name for a schematic file
elvis lvs layout.gds my_mzi:mzi.pic.yml

# Override top cell name for hierarchical schematics
elvis lvs layout.gds mzi.pic.yml --top-cell mzi

```

### Configuration file

LVS settings can be stored in `elvis.toml` or in `pyproject.toml` under `[tool.elvis]`,
so you don't have to repeat flags on every invocation. Elvis looks for `elvis.toml` first,
then falls back to `pyproject.toml`. CLI flags and Python kwargs override config values.

**`elvis.toml`**:

```toml
tolerance = 1
top-cell = "my_design"

short-layers = [[49, 0], [45, 0], [41, 0], [44, 0], [1, 0]]
connected-layers = [[[44, 0], [41, 0]], [[44, 0], [45, 0]]]

[equivalent-ports]
pad = ["e1", "e2", "e3", "e4", "pad"]
```

**`pyproject.toml`**:

```toml
[tool.elvis]
short-layers = [[49, 0], [45, 0]]

[tool.elvis.equivalent-ports]
pad = ["e1", "e2", "e3", "e4", "pad"]
```

With a config file, `elvis lvs layout.gds schematic.pic.yml` picks up all settings
automatically. Pass a flag explicitly to override a specific config value.

Example YAML output:

```yaml
ok: false
error_count: 2
instance_errors:
  - error_type: missing_in_layout
    name: mmi1
    component: mmi1x2
net_errors:
  - error_type: missing_in_schematic
    p1: wg_in,o2
    p2: mmi,o1
```

### 3. Extract component ports from a GDS file

List the ports defined on each component cell (from kfactory metadata):

```bash
elvis extract-ports layout.gds
```

Output:

```yaml
mmi1x2:
  - o1
  - o2
  - o3
straight:
  - o1
  - o2
pad:
  - e1
  - e2
  - e3
  - e4
  - pad
```

### 4. Convert netlist formats

Convert a GDSFactory netlist (.pic.yml) to the simplified elvis-netlist format:

```bash
elvis convert schematic.pic.yml
```

### 5. Output formats

```bash
# YAML (default)
elvis extract layout.gds

# JSON
elvis extract layout.gds -f json

# Save to file
elvis extract layout.gds -o netlist.yml
```

### 6. Python usage

Install with maturin:

```bash
maturin develop
```

```python
import elvis

# Extract netlist from GDS (returns dict)
netlist = elvis.extract_netlist("layout.gds")
# {'instances': {'mmi': 'mmi1x2', ...}, 'nets': [...], 'ports': {...}}

# Extract component port table from GDS (returns dict)
ports = elvis.extract_ports("layout.gds")
# {'mmi1x2': ['o1', 'o2', 'o3'], 'straight': ['o1', 'o2'], ...}

# Load schematic from .pic.yml file(s) (returns GDSFactory netlist dict)
schematic = elvis.load_schematic("schematic.pic.yml")

# Run LVS comparison (returns dict)
result = elvis.lvs("layout.gds", schematic)
# {'ok': False, 'error_count': 2, 'instance_errors': [...], ...}

# Run LVS comparison (returns klayout.rdb.ReportDatabase)
report = elvis.lvs_rdb("layout.gds", schematic)
report.save("report.lyrdb")  # save for viewing in KLayout

# Custom port matching tolerance (default: 1nm)
result = elvis.lvs("layout.gds", schematic, tolerance_nm=10)

# Geometric short detection
result = elvis.lvs("layout.gds", schematic, short_layers=[(1, 0)])

# Cross-layer short detection (M1 ↔ VIA ↔ M2)
result = elvis.lvs("layout.gds", schematic,
                    short_layers=[(1, 0), (3, 0)],
                    connected_layers=[((1, 0), (2, 0)), ((2, 0), (3, 0))])

# Equivalent port grouping
result = elvis.lvs("layout.gds", schematic,
                    equivalent_ports=[("pad", ["e1", "e2", "e3", "e4"])])

# Hierarchical LVS — pass all child files explicitly
schematic = elvis.load_schematic("mzi.pic.yml", "mmi1x2.pic.yml")
result = elvis.lvs("layout.gds", schematic)

# Quick pass/fail check
if elvis.lvs_ok("layout.gds", schematic):
    print("LVS passed!")

# Pass a flat schematic dict directly (no file needed)
result = elvis.lvs("layout.gds", {
    "instances": {"mmi": {"component": "mmi1x2"}, "wg": {"component": "straight"}},
    "connections": {"wg,o2": "mmi,o1"},
    "ports": {"o1": "wg,o1", "o2": "mmi,o2"},
})

# Convert GDSFactory netlist to elvis format (returns dict)
converted = elvis.convert("schematic.pic.yml")
```

## How it works

### Netlist Extraction

Elvis extracts connectivity information from GDS files through a multi-step process:

#### 1. Parsing GDS Structure

The GDS file contains a hierarchy of cells (structures). Elvis identifies the **top
cell** - the cell that is not instantiated by any other cell. This is the design being
verified.

```
GDS File
├── top_cell (not referenced by others → this is extracted)
│   ├── instance: mmi (references mmi1x2 cell)
│   ├── instance: wg_in (references straight cell)
│   └── instance: wg_out1 (references straight cell)
├── mmi1x2 (component cell)
├── straight (component cell)
└── bend_euler (component cell)
```

#### 2. Extracting Port Metadata

kfactory/gdsfactory stores port information in GDS PROPATTR records with a specific
format:

```
kfactory:ports:0')={'name'=>'o1','port_type'=>'optical','trans'=>[trans:r180 -35500,625]}
```

Elvis parses these properties to extract:

- **name**: Port identifier (e.g., `o1`, `o2`)
- **position**: X, Y coordinates in database units
- **orientation**: Rotation angle (0°, 90°, 180°, 270°)
- **port_type**: Optional type (e.g., `optical`, `electrical`)

#### 3. Instance Name Resolution

Each cell reference (SREF) in the GDS becomes an instance. The instance name is
determined by:

1. **Explicit name**: From GDS property with `attr=0` (if present)
2. **Generated name**: `{cell_name}_{x}_{y}[_r{rotation}][_m]` as fallback

The generated name includes rotation and mirror suffixes to ensure uniqueness when
multiple instances of the same cell exist at the same position with different
transforms.

#### 4. Port Transformation

Each instance's ports are transformed from local (cell) coordinates to global (layout)
coordinates:

```
Global Position = Instance Position + Rotate(Local Position, Instance Rotation)
Global Orientation = Local Orientation + Instance Rotation (± mirror)
```

#### 5. Connection Detection

Two ports are considered **connected** when ALL of the following conditions are met:

| Condition       | Requirement                                  |
| --------------- | -------------------------------------------- |
| **Position**    | Within tolerance (default: 1nm)              |
| **Orientation** | Facing opposite directions (180° ± 1°)       |
| **Port type**   | Same type, or either type is unknown         |
| **Instance**    | On different instances (no self-connections) |

```
        ┌─────────┐           ┌───────┐
        │  mmi    │  o2 ←→ o1 │  wg   │
        │         │─────●─────│       │
        └─────────┘  180°  0° └───────┘
                   Connected: same position, opposite orientation
```

The **port type check** ensures that only compatible ports connect:

- `optical` ↔ `optical`: ✓ Connected
- `electrical` ↔ `electrical`: ✓ Connected
- `optical` ↔ `electrical`: ✗ Not connected (will appear as open)
- `unknown` ↔ `anything`: ✓ Connected (permissive fallback)

### LVS Algorithm

Elvis uses a **graph-based connectivity comparison** where 2-port routing instances are
removed from the net-comparisons.

#### The Problem with Naive Comparison

A direct comparison would fail on any routed design:

```
Schematic (what the designer specified):
┌──────────┐      ┌──────────┐
│ splitter │──────│ arm_top  │
└──────────┘      └──────────┘
     2 instances, 1 net

Layout (after auto-routing):
┌──────────┐   ┌──────┐   ┌────────┐   ┌──────┐   ┌──────────┐
│ splitter │───│ bend │───│straight│───│ bend │───│ arm_top  │
└──────────┘   └──────┘   └────────┘   └──────┘   └──────────┘
     5 instances, 4 nets

Naive LVS: FAILED - 3 extra instances, 3 extra nets
```

#### The Solution: Graph-Based Tracing

Elvis treats the layout as a connectivity graph and traces through **2-port components**
(which act as "wires") to find connections between **reference instances** (components
from the schematic).

**Key insight:** A 2-port component (like a waveguide or bend) doesn't change
connectivity - it just extends a path. Only components with 3+ ports (like splitters,
couplers) are true circuit elements that define the topology.

#### Algorithm Steps

**Step 1: Identify Reference Instances**

Reference instances are those that appear in the schematic. These are the "real"
components we care about - they define the circuit topology.

```python
reference_instances = {name for name in schematic.instances}
# e.g., {"splitter", "arm_top", "arm_bottom", "combiner"}
```

**Step 2: Build Connectivity Graph**

Create a graph from the layout where:

- Nodes are `(instance, port)` pairs
- Edges connect ports that are physically connected

```
Layout connectivity graph:
(splitter,o2) ─── (bend_1,o1)
(bend_1,o2) ─── (straight_1,o1)
(straight_1,o2) ─── (bend_2,o1)
(bend_2,o2) ─── (arm_top,o1)
```

**Step 3: Classify Traversable Instances**

An instance is **traversable** if:

1. It has exactly 2 ports (it's a "wire-like" component)
2. It is NOT a reference instance (not in the schematic)

```python
def is_traversable(instance):
    return port_count[instance] == 2 and instance not in reference_instances
```

Reference instances are **never traversable** - they are endpoints where tracing stops.

**Step 4: Trace Through 2-Port Intermediates**

For each connection in the schematic, verify it exists in the layout by tracing through
traversable instances:

```python
def trace_to_endpoint(start_instance, start_port):
    current = get_connected_port(start_instance, start_port)

    while is_traversable(current.instance):
        # Find the other port on this 2-port instance
        other_port = get_other_port(current.instance, current.port)
        # Follow the connection from that port
        current = get_connected_port(current.instance, other_port)

    return current  # Returns the endpoint (a reference instance)
```

Example trace:

```
Start: (splitter, o2)
  → connected to (bend_1, o1)
  → bend_1 is traversable, other port is o2
  → (bend_1, o2) connected to (straight_1, o1)
  → straight_1 is traversable, other port is o2
  → (straight_1, o2) connected to (bend_2, o1)
  → bend_2 is traversable, other port is o2
  → (bend_2, o2) connected to (arm_top, o1)
  → arm_top is NOT traversable (it's a reference instance)
End: (arm_top, o1) ✓
```

**Step 5: Mark Valid Intermediates**

All 2-port instances encountered on valid paths are marked as **valid intermediates**.
These won't trigger "missing in schematic" errors.

```
Valid intermediates: {bend_1, straight_1, bend_2}
These exist in layout but not schematic - that's OK, they're routing.
```

**Step 6: Check for Extra Connections**

Also verify that the layout doesn't have extra connections between reference instances
that aren't in the schematic (topological shorts).

#### Array Instance Support

GDS array references (`AREF`) are expanded into individual instances with `<col.row>`
naming (e.g., `pads<0.0>`, `pads<1.0>`). Schematic array instances are likewise
expanded during netlist conversion, so both sides use the same naming convention.

#### Visual Example

```
SCHEMATIC:
                    ┌─────────┐
              ┌─────┤ arm_top ├─────┐
              │     └─────────┘     │
        ┌─────┴─────┐         ┌─────┴─────┐
   ───○─┤ splitter  │         │ combiner  ├─○───
        └─────┬─────┘         └─────┬─────┘
              │     ┌─────────┐     │
              └─────┤ arm_bot ├─────┘
                    └─────────┘

   4 reference instances, 4 nets

LAYOUT (with routing):
                         ╭───────────────────╮
                    ┌────┤ arm_top           ├────┐
                    │    └───────────────────┘    │
              ╭─────╯                             ╰─────╮
              │  (bends and straights)                  │
        ┌─────┴─────┐                            ┌──────┴────┐
   ───○─┤ splitter  │                            │ combiner  ├─○───
        └─────┬─────┘                            └─────┬─────┘
              │  (bends and straights)                 │
              ╰─────╮                             ╭────╯
                    │    ┌───────────────────┐    │
                    └────┤ arm_bot           ├────┘
                         └───────────────────┘

   64 instances total (4 reference + 60 routing), 64 nets

LVS RESULT: PASSED
   - All 4 schematic connections verified through routing
   - 60 routing instances are valid intermediates
```

#### Error Types

| Error                           | Description                                                   | Geometric Marker           |
| ------------------------------- | ------------------------------------------------------------- | -------------------------- |
| `instance.missing_in_layout`    | Schematic instance not found in layout                        | Box at schematic placement |
| `instance.missing_in_schematic` | Layout instance not in schematic AND not a valid intermediate | Box around instance ports  |
| `instance.component_mismatch`   | Instance exists but wrong component type                      | Box around instance ports  |
| `net.missing_in_layout`         | Schematic connection not found (even through routing)         | Edge between the two ports |
| `net.missing_in_schematic`      | Extra connection between reference instances                  | Edge between the two ports |
| `port.missing_in_layout`        | Top-level port in schematic but not layout                    | Point at port position     |
| `port.missing_in_schematic`     | Top-level port in layout but not schematic                    | Point at port position     |
| `open`                          | Dangling port not connected and not a top-level port          | Box around instance ports  |
| `short`                         | Polygons from different chains overlap unexpectedly           | Polygon at overlap region  |

### Open Detection

After tracing, any port that is neither connected to another port nor exposed as a
top-level port is flagged as an **open**. This catches dangling ports that the designer
likely forgot to connect.

#### Equivalent Port Grouping

Components like pads may have multiple ports that are electrically equivalent (e.g.,
`e1`, `e2`, `e3`, `e4`). When any port in such a group is connected, the others
shouldn't be flagged as open. Use `--equivalent-ports` to declare these groups:

```bash
elvis lvs layout.gds schematic.pic.yml --equivalent-ports pad:e1,e2,e3,e4
```

Unconnected ports within a group are merged into a single open error (with markers for
each port) rather than generating one error per port.

### Hierarchical LVS

Elvis supports hierarchical schematics where the design is split across multiple
`.pic.yml` files. Pass all required schematic files explicitly on the command line:

```bash
# mzi.pic.yml references "mmi1x2" → pass mmi1x2.pic.yml as well
elvis lvs layout.gds mzi.pic.yml mmi1x2.pic.yml
```

For flat netlists, the filename stem (e.g. `mzi` from `mzi.pic.yml`) is used as the
component name. You can override this with the `name:path` syntax:

```bash
elvis lvs layout.gds my_mzi:mzi.pic.yml mmi1x2.pic.yml
```

The hierarchy expansion is controlled by which components have sub-netlists. Instances
whose component matches a loaded sub-netlist are expanded in both the schematic
(flattened with `~` separator) and the layout (hierarchical polygon/instance extraction).

Use `--top-cell` to override the top cell name when it differs from the filename:

```bash
elvis lvs layout.gds mzi.pic.yml mmi1x2.pic.yml --top-cell mzi_optimized
```

### Geometric Short Detection

Elvis can detect geometric shorts: polygon overlaps between different chains (routes or
reference instances) on specified layers. Enable with `--short-layer`:

```bash
elvis lvs layout.gds schematic.pic.yml --short-layer 1,0
```

#### How it works

1. **Chain assignment**: Each instance in the layout is assigned to a chain. A route
   chain is the path of 2-port instances between two reference instance ports. A
   reference instance forms its own chain.

2. **Polygon collection**: For each specified layer, polygons are collected per chain
   and tracked by layer.

3. **Per-layer-pair detection**: For each same-layer pair (from `--short-layer`) and each
   cross-layer pair (from `--connected-layers`), polygon intersections between different
   chains are computed. Overlaps at port locations (where chains connect) are excluded.
   Overlaps smaller than 0.001 um² are filtered as slivers.

4. **Schematic-aware suppression**: A short between two chains induces cross-connections
   between all pairs of their endpoints. If ALL of these cross-connections exist as direct
   nets in the schematic, the short is suppressed (it's intentional). Any "missing in
   layout" net errors satisfied by suppressed shorts are also removed.

#### Cross-layer shorts

In multi-layer designs, layers may be electrically connected through vias. Use
`--connected-layers` to declare pairwise layer connectivity:

```bash
# M1 (1,0) connects to VIA (2,0), VIA connects to M2 (3,0)
elvis lvs layout.gds schematic.pic.yml \
    --short-layer 1,0 --short-layer 3,0 \
    --connected-layers 1,0:2,0 --connected-layers 2,0:3,0
```

Each `--short-layer` checks for same-layer shorts independently. Each
`--connected-layers` pair adds cross-layer overlap checks between the two specified
layers. Without `--connected-layers`, layers are checked in isolation (no cross-layer
false positives).

### Output Formats

#### KLayout lyrdb (default)

The lyrdb format is viewable in KLayout's marker browser. Each error includes:

- Category hierarchy for filtering
- Text description
- **Geometric markers** you can click to navigate to the error location

```xml
<item>
  <category>LVS.net.missing_in_layout</category>
  <cell>top_cell</cell>
  <values>
    <value>text: Net splitter,o2 &lt;-&gt; arm_top,o1 missing</value>
    <value>edge: (10.5,0.625;25.0,15.5)</value>
  </values>
</item>
```

#### YAML/JSON

Structured reports for programmatic processing:

```yaml
ok: false
error_count: 1
net_errors:
  - error_type: missing_in_layout
    p1: splitter,o2
    p2: arm_top,o1
```

## Crates

- **gf-netlist** - GDSFactory netlist schema (input .pic.yml format)
- **elvis-netlist** - Extracted netlist schema (simplified subset for LVS)
- **elvis-rdb** - KLayout Report Database (lyrdb) format
- **elvis-core** - Core extraction and LVS functionality
- **elvis-cli** - Command-line interface
- **elvis-python** - Python bindings via PyO3

## License

Apache 2.0 License. See [LICENSE](LICENSE) for details.

