Introduction

laykit is a production-ready Rust library for reading, writing, and manipulating GDSII and OASIS IC layout files.

What it does

  • Read and write GDSII (.gds) files — the industry-standard binary format for IC layouts
  • Read and write OASIS (.oas) files — the modern, more compact replacement
  • Convert bidirectionally between the two formats
  • Perform geometric operations: bounding boxes, transforms, polygon area/perimeter, point-in-polygon, fillet, fracture
  • Boolean polygon operations: union, intersection, difference, XOR, slice, offset
  • Generate complex shapes: FlexPath with configurable joins/caps, arcs, Bezier curves, ellipses, splines, spirals
  • Manage cell hierarchies: flatten, dependency ordering, cycle detection, library merge
  • Stream large files without loading them fully into memory

Design

  • Zero external dependencies — pure Rust std only
  • 164 tests, 0 failures
  • Idiomatic Rust: Result-based error handling, enums for element types, no unsafe code in public API
  • Rust edition 2024

Features

  • ✅ GDSII read/write (all 7 element types)
  • ✅ OASIS read/write (all element types)
  • ✅ Bidirectional format conversion
  • ✅ Geometry module
  • ✅ Boolean operations
  • ✅ FlexPath and curve primitives
  • ✅ Cell topology utilities
  • ✅ Streaming parser
  • ✅ AREF expansion
  • ✅ Property management
  • ✅ CLI tool (convert, info, validate)
  • ✅ File format detection by magic bytes

Getting Started

This guide will help you get started with LayKit, from installation to your first working program.

Prerequisites

Before using LayKit, ensure you have:

  • Rust 1.70 or later - Install Rust
  • Cargo - Comes with Rust installation
  • Basic familiarity with Rust programming

System Requirements

LayKit works on all major platforms:

  • Linux - Fully supported and tested
  • macOS - Fully supported (both Intel and ARM)
  • Windows - Fully supported (including WSL2)

Memory Requirements:

  • Minimum: 100MB RAM
  • Recommended: System RAM > 2× your largest layout file size
  • Files are loaded entirely into memory

Next Steps

Continue to:

Getting Help

If you encounter any issues:

  1. Check the examples for similar use cases
  2. Review the API Reference for detailed documentation
  3. Search GitHub Issues for known problems
  4. Open a new issue if you find a bug or have a feature request

Installation

From Crates.io

Add laykit to your project:

cargo add laykit

Or add it manually to your Cargo.toml (check crates.io/crates/laykit for the latest version):

[dependencies]
laykit = "0"

From Source

Clone and reference locally:

git clone https://github.com/giridharsalana/laykit.git
[dependencies]
laykit = { path = "../laykit" }

Or pin to a specific release tag:

[dependencies]
laykit = { git = "https://github.com/giridharsalana/laykit", tag = "vX.Y.Z" }

Verifying Installation

use laykit::GDSIIFile;

fn main() {
    let gds = GDSIIFile::new("TEST".to_string());
    println!("LayKit is working! Library: {}", gds.library_name);
}
cargo run
# LayKit is working! Library: TEST

Building from Source

git clone https://github.com/giridharsalana/laykit.git
cd laykit

cargo build --release
cargo test
cargo doc --open

Requirements

  • Rust — any recent stable release
  • Zero runtime dependencies — only std

Next Steps

Continue to the Quick Start guide.

Quick Start

Get up and running with LayKit in minutes!

Minimal Example

Here's the simplest possible LayKit program:

use laykit::GDSIIFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read a GDSII file
    let gds = GDSIIFile::read_from_file("layout.gds")?;
    
    // Print information
    println!("Library: {}", gds.library_name);
    println!("Structures: {}", gds.structures.len());
    
    // Write it back
    gds.write_to_file("output.gds")?;
    
    Ok(())
}

Common Operations

Reading Files

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, OASISFile};

// Read GDSII
let gds = GDSIIFile::read_from_file("design.gds")?;

// Read OASIS
let oasis = OASISFile::read_from_file("design.oas")?;
}

Creating Files

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, Boundary};

let mut gds = GDSIIFile::new("MY_LIBRARY".to_string());
gds.units = (1e-6, 1e-9); // 1 micron, 1nm

let mut structure = GDSStructure {
    name: "TOP".to_string(),
    creation_time: GDSTime::now(),
    modification_time: GDSTime::now(),
    elements: Vec::new(),
};

// Add a rectangle
structure.elements.push(GDSElement::Boundary(Boundary {
    layer: 1,
    datatype: 0,
    xy: vec![(0, 0), (1000, 0), (1000, 1000), (0, 1000), (0, 0)],
    properties: Vec::new(),
}));

gds.structures.push(structure);
gds.write_to_file("output.gds")?;
}

Converting Between Formats

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, converter};

// GDSII to OASIS
let gds = GDSIIFile::read_from_file("input.gds")?;
let oasis = converter::gdsii_to_oasis(&gds)?;
oasis.write_to_file("output.oas")?;

// OASIS to GDSII
let oasis = OASISFile::read_from_file("input.oas")?;
let gds = converter::oasis_to_gdsii(&oasis)?;
gds.write_to_file("output.gds")?;
}

Running Examples

LayKit comes with complete working examples:

# Clone the repository
git clone https://github.com/giridharsalana/laykit.git
cd laykit

# Run the basic usage example
cargo run --example basic_usage

# Run GDSII-only example
cargo run --example gdsii_only

# Run OASIS-only example
cargo run --example oasis_only

Next Steps

GDSII Format

GDSII Stream Format is the industry-standard format for IC layout data. LayKit provides complete support for reading and writing GDSII files.

Overview

GDSII is a binary format with these characteristics:

  • Binary encoding: Big-endian byte order
  • Record-based structure: Each record has type, data type, and data
  • Hierarchical organization: Libraries contain structures (cells)
  • Custom floating point: 8-byte Real8 format
  • File extension: .gds

Main Types

GDSIIFile

The root structure representing a complete GDSII file:

#![allow(unused)]
fn main() {
pub struct GDSIIFile {
    pub version: i16,
    pub library_name: String,
    pub units: (f64, f64),  // (user_unit, database_unit) in meters
    pub structures: Vec<GDSStructure>,
}
}

Key methods:

  • new(library_name: String) -> Self - Create empty file
  • read_from_file(path: &str) -> Result<Self, Box<dyn Error>> - Read from disk
  • write_to_file(&self, path: &str) -> Result<(), Box<dyn Error>> - Write to disk

GDSStructure

Represents a cell/structure in the design:

#![allow(unused)]
fn main() {
pub struct GDSStructure {
    pub name: String,
    pub creation_time: GDSTime,
    pub modification_time: GDSTime,
    pub elements: Vec<GDSElement>,
}
}

GDSElement

An enum of all possible GDSII elements:

#![allow(unused)]
fn main() {
pub enum GDSElement {
    Boundary(Boundary),      // Polygons
    Path(GPath),             // Wires/traces
    Text(GText),             // Text labels
    StructRef(StructRef),    // Cell instances
    ArrayRef(ArrayRef),      // Cell arrays
    Node(Node),              // Net topology
    Box(GDSBox),             // Box elements
}
}

Supported Elements

ElementDescriptionStatus
BoundaryPolygon with layer/datatype✅ Full support
PathWire with width✅ Full support
TextText labels✅ Full support
StructRefCell instance✅ Full support
ArrayRefCell array✅ Full support
NodeNet topology✅ Full support
BoxBox element✅ Full support

Binary Format Details

Record Structure

[2 bytes: length] [1 byte: record type] [1 byte: data type] [n bytes: data]

Units

Units are stored as a pair of f64 values in meters:

  • units.0 - User unit (typically 1e-6 for micrometers)
  • units.1 - Database unit (typically 1e-9 for nanometers)

Coordinates

All coordinates are 32-bit signed integers (i32) in database units.

Next Steps

Reading GDSII Files

Learn how to read and parse GDSII files with LayKit.

Basic Reading

The simplest way to read a GDSII file:

use laykit::GDSIIFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    println!("Loaded: {}", gds.library_name);
    Ok(())
}

Accessing File Information

#![allow(unused)]
fn main() {
let gds = GDSIIFile::read_from_file("design.gds")?;

println!("Library: {}", gds.library_name);
println!("Version: {}", gds.version);
println!("Units: {} user, {} database (meters)", gds.units.0, gds.units.1);
println!("Structures: {}", gds.structures.len());
}

Iterating Through Structures

#![allow(unused)]
fn main() {
for structure in &gds.structures {
    println!("\nStructure: {}", structure.name);
    println!("  Created: {:04}-{:02}-{:02}",
        structure.creation_time.year,
        structure.creation_time.month,
        structure.creation_time.day
    );
    println!("  Elements: {}", structure.elements.len());
}
}

Processing Elements

#![allow(unused)]
fn main() {
use laykit::GDSElement;

for element in &structure.elements {
    match element {
        GDSElement::Boundary(b) => {
            println!("Boundary: layer={}, {} vertices", b.layer, b.xy.len());
        }
        GDSElement::Path(p) => {
            println!("Path: layer={}, width={:?}", p.layer, p.width);
        }
        GDSElement::Text(t) => {
            println!("Text: \"{}\" at ({}, {})", t.string, t.xy.0, t.xy.1);
        }
        GDSElement::StructRef(s) => {
            println!("Reference: {}", s.sname);
        }
        _ => {}
    }
}
}

Error Handling

#![allow(unused)]
fn main() {
match GDSIIFile::read_from_file("design.gds") {
    Ok(gds) => {
        println!("Successfully read {} structures", gds.structures.len());
    }
    Err(e) => {
        eprintln!("Error reading GDSII file: {}", e);
    }
}
}

Writing GDSII Files

Learn how to create and write GDSII files with LayKit.

Creating a New File

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, Boundary};

let mut gds = GDSIIFile::new("MY_LIBRARY".to_string());
gds.units = (1e-6, 1e-9); // 1µm user unit, 1nm database unit
}

Adding Structures

#![allow(unused)]
fn main() {
let mut structure = GDSStructure {
    name: "TOP_CELL".to_string(),
    creation_time: GDSTime::now(),
    modification_time: GDSTime::now(),
    elements: Vec::new(),
};

// Add elements to structure...

gds.structures.push(structure);
}

Adding Elements

Rectangle

#![allow(unused)]
fn main() {
use laykit::{GDSElement, Boundary};

structure.elements.push(GDSElement::Boundary(Boundary {
    layer: 1,
    datatype: 0,
    xy: vec![
        (0, 0),
        (10000, 0),
        (10000, 5000),
        (0, 5000),
        (0, 0),
    ],
    properties: Vec::new(),
}));
}

Path

#![allow(unused)]
fn main() {
use laykit::{GDSElement, GPath};

structure.elements.push(GDSElement::Path(GPath {
    layer: 2,
    datatype: 0,
    pathtype: 0,
    width: Some(100),
    xy: vec![(0, 0), (1000, 1000), (2000, 0)],
    properties: Vec::new(),
}));
}

Text

#![allow(unused)]
fn main() {
use laykit::{GDSElement, GText};

structure.elements.push(GDSElement::Text(GText {
    layer: 3,
    texttype: 0,
    string: "LABEL".to_string(),
    xy: (500, 500),
    strans: None,
    properties: Vec::new(),
}));
}

Writing to File

#![allow(unused)]
fn main() {
gds.write_to_file("output.gds")?;
println!("GDSII file written successfully!");
}

Complete Example

See Complete Examples for full working programs.

GDSII Elements

Detailed reference for all GDSII element types supported by LayKit.

Boundary

Polygon element with arbitrary vertices.

#![allow(unused)]
fn main() {
pub struct Boundary {
    pub layer: i16,
    pub datatype: i16,
    pub xy: Vec<(i32, i32)>,
    pub properties: Vec<GDSProperty>,
}
}

Usage: Representing filled shapes, design regions, and complex polygons.

Path

Wire or trace element with specified width.

#![allow(unused)]
fn main() {
pub struct GPath {
    pub layer: i16,
    pub datatype: i16,
    pub pathtype: i16,
    pub width: Option<i32>,
    pub xy: Vec<(i32, i32)>,
    pub properties: Vec<GDSProperty>,
}
}

Path types:

  • 0 - Square ends (default)
  • 1 - Round ends
  • 2 - Flush ends

Text

Text label element.

#![allow(unused)]
fn main() {
pub struct GText {
    pub layer: i16,
    pub texttype: i16,
    pub string: String,
    pub xy: (i32, i32),
    pub strans: Option<STrans>,
    pub properties: Vec<GDSProperty>,
}
}

StructRef (SREF)

Reference to another structure (cell instance).

#![allow(unused)]
fn main() {
pub struct StructRef {
    pub sname: String,
    pub xy: (i32, i32),
    pub strans: Option<STrans>,
    pub properties: Vec<GDSProperty>,
}
}

ArrayRef (AREF)

Array of structure references.

#![allow(unused)]
fn main() {
pub struct ArrayRef {
    pub sname: String,
    pub columns: i16,
    pub rows: i16,
    pub xy: [(i32, i32); 3],
    pub strans: Option<STrans>,
    pub properties: Vec<GDSProperty>,
}
}

The xy array contains:

  • [0] - Reference point
  • [1] - Column spacing point
  • [2] - Row spacing point

STrans

Transformation for references and text.

#![allow(unused)]
fn main() {
pub struct STrans {
    pub reflect_x: bool,
    pub absolute_mag: bool,
    pub absolute_angle: bool,
    pub magnification: Option<f64>,
    pub angle: Option<f64>,
}
}

Node

Net connectivity element.

#![allow(unused)]
fn main() {
pub struct Node {
    pub layer: i16,
    pub nodetype: i16,
    pub xy: Vec<(i32, i32)>,
    pub properties: Vec<GDSProperty>,
}
}

Box

Box element (rare in modern designs).

#![allow(unused)]
fn main() {
pub struct GDSBox {
    pub layer: i16,
    pub boxtype: i16,
    pub xy: [(i32, i32); 5],
    pub properties: Vec<GDSProperty>,
}
}

OASIS Format

Open Artwork System Interchange Standard (OASIS) is a modern IC layout format designed to be more compact and efficient than GDSII.

Overview

OASIS format characteristics:

  • Compact binary encoding: Variable-length integers
  • Modern design: Optimized for file size
  • Primitive shapes: Rectangles, trapezoids, circles
  • Name tables: String compression
  • IEEE 754 floats: Standard floating point
  • File extension: .oas

Main Types

OASISFile

The root structure:

#![allow(unused)]
fn main() {
pub struct OASISFile {
    pub version: String,
    pub unit: f64,
    pub names: NameTable,
    pub cells: Vec<OASISCell>,
}
}

OASISCell

Cell/structure in the design:

#![allow(unused)]
fn main() {
pub struct OASISCell {
    pub name: String,
    pub elements: Vec<OASISElement>,
}
}

OASISElement

Enum of OASIS elements:

#![allow(unused)]
fn main() {
pub enum OASISElement {
    Rectangle(Rectangle),
    Polygon(Polygon),
    Path(OPath),
    Trapezoid(Trapezoid),
    CTrapezoid(CTrapezoid),
    Circle(Circle),
    Text(OText),
    Placement(Placement),
}
}

Supported Elements

ElementDescriptionStatus
RectangleAxis-aligned rectangle✅ Full support
PolygonGeneral polygon✅ Full support
PathWire with extensions✅ Full support
TrapezoidTrapezoidal shape✅ Full support
CTrapezoidConstrained trapezoid✅ Full support
CircleCircle primitive✅ Full support
TextText label✅ Full support
PlacementCell instance✅ Full support

Name Tables

OASIS uses name tables for efficient string storage:

#![allow(unused)]
fn main() {
pub struct NameTable {
    pub cell_names: HashMap<u32, String>,
    pub text_strings: HashMap<u32, String>,
    pub prop_names: HashMap<u32, String>,
}
}

Binary Format

Variable-Length Integers

Unsigned integers use 7 bits per byte:

0xxxxxxx = 0-127
1xxxxxxx 0yyyyyyy = 128-16383
...

Zigzag Encoding

Signed integers use zigzag encoding:

0 → 0
-1 → 1
1 → 2
-2 → 3

Next Steps

Reading OASIS Files

Learn how to read and parse OASIS files with LayKit.

Basic Reading

use laykit::OASISFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let oasis = OASISFile::read_from_file("design.oas")?;
    println!("Loaded OASIS file with {} cells", oasis.cells.len());
    Ok(())
}

Accessing File Information

#![allow(unused)]
fn main() {
let oasis = OASISFile::read_from_file("design.oas")?;

println!("Version: {}", oasis.version);
println!("Unit: {} meters", oasis.unit);
println!("Cells: {}", oasis.cells.len());
println!("Cell names in table: {}", oasis.names.cell_names.len());
}

Iterating Through Cells

#![allow(unused)]
fn main() {
for cell in &oasis.cells {
    println!("\nCell: {}", cell.name);
    println!("  Elements: {}", cell.elements.len());
    
    // Count element types
    let mut rectangles = 0;
    let mut polygons = 0;
    let mut paths = 0;
    
    for element in &cell.elements {
        match element {
            OASISElement::Rectangle(_) => rectangles += 1,
            OASISElement::Polygon(_) => polygons += 1,
            OASISElement::Path(_) => paths += 1,
            _ => {}
        }
    }
    
    println!("  Rectangles: {}, Polygons: {}, Paths: {}", 
        rectangles, polygons, paths);
}
}

Processing Elements

#![allow(unused)]
fn main() {
use laykit::OASISElement;

for element in &cell.elements {
    match element {
        OASISElement::Rectangle(r) => {
            println!("Rectangle: layer={}, {}×{} at ({},{})",
                r.layer, r.width, r.height, r.x, r.y);
        }
        OASISElement::Polygon(p) => {
            println!("Polygon: layer={}, {} points at ({},{})",
                p.layer, p.points.len(), p.x, p.y);
        }
        OASISElement::Path(p) => {
            println!("Path: layer={}, {} points",
                p.layer, p.points.len());
        }
        OASISElement::Trapezoid(t) => {
            println!("Trapezoid: layer={}", t.layer);
        }
        OASISElement::CTrapezoid(ct) => {
            println!("CTrapezoid: layer={}", ct.layer);
        }
        OASISElement::Circle(c) => {
            println!("Circle: layer={}, radius={}", c.layer, c.radius);
        }
        OASISElement::Text(t) => {
            println!("Text: \"{}\" at ({},{})", t.string, t.x, t.y);
        }
        OASISElement::Placement(p) => {
            println!("Placement: cell reference at ({},{})", p.x, p.y);
        }
    }
}
}

Working with Name Tables

OASIS uses name tables for efficient string storage:

#![allow(unused)]
fn main() {
// Access cell names
for (id, name) in &oasis.names.cell_names {
    println!("Cell ID {}: {}", id, name);
}

// Access text strings
for (id, text) in &oasis.names.text_strings {
    println!("Text ID {}: {}", id, text);
}

// Access property names
for (id, prop) in &oasis.names.prop_names {
    println!("Property ID {}: {}", id, prop);
}
}

Error Handling

#![allow(unused)]
fn main() {
match OASISFile::read_from_file("design.oas") {
    Ok(oasis) => {
        println!("Successfully read {} cells", oasis.cells.len());
    }
    Err(e) => {
        eprintln!("Error reading OASIS file: {}", e);
        eprintln!("Make sure the file exists and is a valid OASIS file");
    }
}
}

Reading from Buffer

You can also read from any Read source:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::BufReader;
use laykit::OASISFile;

let file = File::open("design.oas")?;
let mut reader = BufReader::new(file);
let oasis = OASISFile::read(&mut reader)?;
}

Writing OASIS Files

Learn how to create and write OASIS files with LayKit.

Creating a New File

#![allow(unused)]
fn main() {
use laykit::{OASISFile, OASISCell, OASISElement, Rectangle};

let mut oasis = OASISFile::new();
oasis.unit = 1e-9; // 1nm database unit
}

Adding Cells

#![allow(unused)]
fn main() {
let mut cell = OASISCell {
    name: "TOP_CELL".to_string(),
    elements: Vec::new(),
};

// Add elements to cell...

oasis.cells.push(cell);
}

Registering Names

Before referencing cells or strings, register them in name tables:

#![allow(unused)]
fn main() {
// Register cell name
oasis.names.cell_names.insert(0, "TOP_CELL".to_string());

// Register text strings
oasis.names.text_strings.insert(0, "LABEL_TEXT".to_string());

// Register property names
oasis.names.prop_names.insert(0, "PROPERTY_NAME".to_string());
}

Adding Elements

Rectangle

#![allow(unused)]
fn main() {
use laykit::{OASISElement, Rectangle};

cell.elements.push(OASISElement::Rectangle(Rectangle {
    layer: 1,
    datatype: 0,
    x: 0,
    y: 0,
    width: 10000,
    height: 5000,
    repetition: None,
    properties: Vec::new(),
}));
}

Polygon

#![allow(unused)]
fn main() {
use laykit::{OASISElement, Polygon};

cell.elements.push(OASISElement::Polygon(Polygon {
    layer: 2,
    datatype: 0,
    x: 0,
    y: 0,
    points: vec![
        (0, 0),
        (10000, 0),
        (5000, 10000),
        (0, 0),
    ],
    repetition: None,
    properties: Vec::new(),
}));
}

Path

#![allow(unused)]
fn main() {
use laykit::{OASISElement, OPath};

cell.elements.push(OASISElement::Path(OPath {
    layer: 3,
    datatype: 0,
    width: 100,
    x: 0,
    y: 0,
    points: vec![(0, 0), (1000, 1000), (2000, 0)],
    start_extension: 0,
    end_extension: 0,
    repetition: None,
    properties: Vec::new(),
}));
}

Trapezoid

#![allow(unused)]
fn main() {
use laykit::{OASISElement, Trapezoid};

cell.elements.push(OASISElement::Trapezoid(Trapezoid {
    layer: 4,
    datatype: 0,
    x: 0,
    y: 0,
    width: 1000,
    height: 1000,
    delta_a: 100,
    delta_b: 200,
    repetition: None,
    properties: Vec::new(),
}));
}

Circle

#![allow(unused)]
fn main() {
use laykit::{OASISElement, Circle};

cell.elements.push(OASISElement::Circle(Circle {
    layer: 5,
    datatype: 0,
    x: 500,
    y: 500,
    radius: 250,
    repetition: None,
    properties: Vec::new(),
}));
}

Text

#![allow(unused)]
fn main() {
use laykit::{OASISElement, OText};

cell.elements.push(OASISElement::Text(OText {
    layer: 6,
    texttype: 0,
    string: "LABEL".to_string(),
    x: 1000,
    y: 1000,
    repetition: None,
    properties: Vec::new(),
}));
}

Placement (Cell Instance)

#![allow(unused)]
fn main() {
use laykit::{OASISElement, Placement};

// Make sure to register the cell name first
oasis.names.cell_names.insert(1, "SUBCELL".to_string());

cell.elements.push(OASISElement::Placement(Placement {
    cell_name: "SUBCELL".to_string(),
    x: 2000,
    y: 2000,
    magnification: None,
    angle: None,
    mirror_x: false,
    repetition: None,
    properties: Vec::new(),
}));
}

Writing to File

#![allow(unused)]
fn main() {
oasis.write_to_file("output.oas")?;
println!("OASIS file written successfully!");
}

Complete Example

use laykit::{OASISFile, OASISCell, OASISElement, Rectangle, Polygon};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut oasis = OASISFile::new();
    oasis.unit = 1e-9; // 1nm
    
    // Register cell name
    oasis.names.cell_names.insert(0, "DEMO".to_string());
    
    let mut cell = OASISCell {
        name: "DEMO".to_string(),
        elements: Vec::new(),
    };
    
    // Add rectangle
    cell.elements.push(OASISElement::Rectangle(Rectangle {
        layer: 1,
        datatype: 0,
        x: 0,
        y: 0,
        width: 10000,
        height: 5000,
        repetition: None,
        properties: Vec::new(),
    }));
    
    // Add triangle
    cell.elements.push(OASISElement::Polygon(Polygon {
        layer: 2,
        datatype: 0,
        x: 15000,
        y: 0,
        points: vec![(0, 0), (5000, 0), (2500, 5000), (0, 0)],
        repetition: None,
        properties: Vec::new(),
    }));
    
    oasis.cells.push(cell);
    oasis.write_to_file("demo.oas")?;
    
    println!("✅ Created demo.oas");
    Ok(())
}

Writing to Buffer

You can also write to any Write destination:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::BufWriter;

let file = File::create("output.oas")?;
let mut writer = BufWriter::new(file);
oasis.write(&mut writer)?;
}

OASIS Elements

Detailed reference for all OASIS element types supported by LayKit.

Rectangle

Axis-aligned rectangle primitive - the most common shape in OASIS.

#![allow(unused)]
fn main() {
pub struct Rectangle {
    pub layer: u32,
    pub datatype: u32,
    pub x: i64,
    pub y: i64,
    pub width: u64,
    pub height: u64,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Optimized for rectangular shapes, much more compact than polygons.

Example:

#![allow(unused)]
fn main() {
Rectangle {
    layer: 1,
    datatype: 0,
    x: 0,
    y: 0,
    width: 1000,
    height: 500,
    repetition: None,
    properties: Vec::new(),
}
}

Polygon

General polygon with arbitrary vertices.

#![allow(unused)]
fn main() {
pub struct Polygon {
    pub layer: u32,
    pub datatype: u32,
    pub x: i64,
    pub y: i64,
    pub points: Vec<(i64, i64)>,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: For complex shapes that aren't rectangles or other primitives.

Note: Points are relative to (x, y).

Path

Wire or trace element.

#![allow(unused)]
fn main() {
pub struct OPath {
    pub layer: u32,
    pub datatype: u32,
    pub width: u64,
    pub x: i64,
    pub y: i64,
    pub points: Vec<(i64, i64)>,
    pub start_extension: i64,
    pub end_extension: i64,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Representing wires, interconnects, and traces.

Extensions: Control how the path extends beyond its endpoints.

Trapezoid

Trapezoidal shape primitive.

#![allow(unused)]
fn main() {
pub struct Trapezoid {
    pub layer: u32,
    pub datatype: u32,
    pub x: i64,
    pub y: i64,
    pub width: u64,
    pub height: u64,
    pub delta_a: i64,
    pub delta_b: i64,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Efficiently representing trapezoidal shapes common in IC layouts.

Parameters:

  • delta_a: Horizontal offset at top
  • delta_b: Horizontal offset at bottom

CTrapezoid (Constrained Trapezoid)

Constrained trapezoid with specific geometry.

#![allow(unused)]
fn main() {
pub struct CTrapezoid {
    pub layer: u32,
    pub datatype: u32,
    pub x: i64,
    pub y: i64,
    pub width: u64,
    pub height: u64,
    pub ctrapezoid_type: u8,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Specific trapezoid types with predefined constraints for even more compact storage.

Circle

Circle primitive.

#![allow(unused)]
fn main() {
pub struct Circle {
    pub layer: u32,
    pub datatype: u32,
    pub x: i64,
    pub y: i64,
    pub radius: u64,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Representing circular shapes, vias, and contacts.

Note: Center is at (x, y).

Text

Text label element.

#![allow(unused)]
fn main() {
pub struct OText {
    pub layer: u32,
    pub texttype: u32,
    pub string: String,
    pub x: i64,
    pub y: i64,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Adding text annotations and labels to the layout.

Placement

Cell instance (reference to another cell).

#![allow(unused)]
fn main() {
pub struct Placement {
    pub cell_name: String,
    pub x: i64,
    pub y: i64,
    pub magnification: Option<f64>,
    pub angle: Option<f64>,
    pub mirror_x: bool,
    pub repetition: Option<Repetition>,
    pub properties: Vec<OASISProperty>,
}
}

Usage: Creating hierarchical designs by instantiating other cells.

Transformations:

  • x, y: Position
  • magnification: Scaling factor
  • angle: Rotation in degrees
  • mirror_x: Mirror across X-axis

Repetition

OASIS supports repetition patterns for creating arrays:

#![allow(unused)]
fn main() {
pub struct Repetition {
    pub x_dimension: u32,
    pub y_dimension: u32,
    pub x_space: i64,
    pub y_space: i64,
}
}

Usage: Efficiently representing repeated patterns without duplicating elements.

Example:

#![allow(unused)]
fn main() {
repetition: Some(Repetition {
    x_dimension: 10,  // 10 columns
    y_dimension: 5,   // 5 rows
    x_space: 1000,    // 1000nm spacing in X
    y_space: 2000,    // 2000nm spacing in Y
})
}

Properties

Additional metadata attached to elements:

#![allow(unused)]
fn main() {
pub struct OASISProperty {
    pub name: String,
    pub values: Vec<PropertyValue>,
}

pub enum PropertyValue {
    Integer(i64),
    Real(f64),
    String(String),
    Boolean(bool),
}
}

Coordinate System

  • All coordinates are 64-bit signed integers (i64)
  • Units defined by OASISFile.unit (typically in meters)
  • Typical database unit: 1nm (1e-9 meters)
  • Relative coordinates used in polygons and paths for compactness

Format Conversion

LayKit provides bidirectional conversion between GDSII and OASIS formats, preserving geometry and hierarchy.

Why Convert?

GDSII to OASIS

  • Smaller file size - OASIS uses compression and variable-length encoding
  • Modern format - Better support for modern IC design features
  • Primitive shapes - Rectangles, trapezoids, and circles for efficiency
  • Faster I/O - Compact format means less data to read/write

OASIS to GDSII

  • Universal compatibility - GDSII is supported by virtually all EDA tools
  • Legacy tool support - Older tools may not support OASIS
  • Industry standard - Still the most widely used format

Conversion Features

Both conversion directions provide:

Complete geometry preservation - All shapes are accurately converted
Hierarchy maintenance - Cell references and structure are preserved
Layer mapping - Layers and datatypes are maintained
Property transfer - Element metadata is converted
Smart optimization - Intelligent shape detection (e.g., rectangles from polygons)

Basic Usage

GDSII → OASIS

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, converter};

let gds = GDSIIFile::read_from_file("input.gds")?;
let oasis = converter::gdsii_to_oasis(&gds)?;
oasis.write_to_file("output.oas")?;
}

OASIS → GDSII

#![allow(unused)]
fn main() {
use laykit::{OASISFile, converter};

let oasis = OASISFile::read_from_file("input.oas")?;
let gds = converter::oasis_to_gdsii(&oasis)?;
gds.write_to_file("output.gds")?;
}

Element Mapping

GDSII to OASIS Element Conversion

GDSII ElementOASIS ElementNotes
Boundary (rectangle)RectangleDetected automatically
Boundary (polygon)PolygonGeneral polygons
PathPathWidth and extensions preserved
TextTextText strings maintained
StructRefPlacementSingle instance
ArrayRefPlacement with RepetitionArray converted to repetition
NodePolygonConverted to boundary
BoxRectangleConverted to rectangle

OASIS to GDSII Element Conversion

OASIS ElementGDSII ElementNotes
RectangleBoundaryConverted to 5-vertex polygon
PolygonBoundaryDirect mapping
PathPathWidth preserved
TrapezoidBoundaryConverted to polygon
CTrapezoidBoundaryConverted to polygon
CircleBoundaryApproximated with polygon
TextTextDirect mapping
PlacementStructRefSingle instance
Placement with RepetitionArrayRefRepetition converted to array

Complete Conversion Example

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, OASISFile, converter};

fn convert_both_ways() -> Result<(), Box<dyn std::error::Error>> {
    println!("=== Format Conversion Demo ===\n");
    
    // Read original GDSII
    println!("1. Reading GDSII file...");
    let gds_original = GDSIIFile::read_from_file("design.gds")?;
    println!("   Library: {}", gds_original.library_name);
    println!("   Structures: {}", gds_original.structures.len());
    
    // Convert to OASIS
    println!("\n2. Converting GDSII → OASIS...");
    let oasis = converter::gdsii_to_oasis(&gds_original)?;
    println!("   Cells: {}", oasis.cells.len());
    oasis.write_to_file("converted.oas")?;
    println!("   ✅ Written to converted.oas");
    
    // Convert back to GDSII
    println!("\n3. Converting OASIS → GDSII...");
    let gds_converted = converter::oasis_to_gdsii(&oasis)?;
    println!("   Structures: {}", gds_converted.structures.len());
    gds_converted.write_to_file("roundtrip.gds")?;
    println!("   ✅ Written to roundtrip.gds");
    
    // Verify
    println!("\n4. Verification:");
    println!("   Original structures: {}", gds_original.structures.len());
    println!("   Roundtrip structures: {}", gds_converted.structures.len());
    println!("   Match: {}", 
        gds_original.structures.len() == gds_converted.structures.len());
    
    Ok(())
}
}

Conversion Options

Units Handling

GDSII → OASIS:

  • GDSII user units and database units are converted to OASIS database unit
  • Default: Uses GDSII database unit (typically 1nm)

OASIS → GDSII:

  • OASIS unit becomes GDSII database unit
  • User unit set to 1000× database unit (e.g., 1µm user, 1nm database)

Rectangle Detection

When converting GDSII to OASIS, the converter automatically detects rectangles:

#![allow(unused)]
fn main() {
// A 5-vertex GDSII boundary that forms a rectangle
let boundary = Boundary {
    xy: vec![(0,0), (1000,0), (1000,500), (0,500), (0,0)],
    ...
};

// Is automatically converted to an OASIS Rectangle
let rectangle = Rectangle {
    x: 0, y: 0,
    width: 1000, height: 500,
    ...
};
}

This results in much smaller OASIS files!

Error Handling

#![allow(unused)]
fn main() {
match converter::gdsii_to_oasis(&gds) {
    Ok(oasis) => {
        println!("Conversion successful!");
        oasis.write_to_file("output.oas")?;
    }
    Err(e) => {
        eprintln!("Conversion failed: {}", e);
        eprintln!("Check input file for unsupported features");
    }
}
}

Performance Tips

  1. Batch conversions - Process multiple files in parallel
  2. Stream large files - For very large files, consider chunked processing
  3. Verify output - Always check converted files in your EDA tool
  4. Keep originals - Maintain backups before conversion

Limitations

Current implementation:

  • ✅ All standard elements supported
  • ✅ Hierarchical designs fully supported
  • ✅ Properties and metadata preserved
  • ⚠️ Custom extensions may need manual handling
  • ⚠️ Extremely large files (>1GB) may need memory optimization

Next Steps

GDSII to OASIS Conversion

Detailed guide for converting GDSII files to OASIS format.

Basic Conversion

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, converter};

let gds = GDSIIFile::read_from_file("input.gds")?;
let oasis = converter::gdsii_to_oasis(&gds)?;
oasis.write_to_file("output.oas")?;
}

What Gets Converted

File-Level

  • Library name → Stored in OASIS structure
  • Units → GDSII database unit becomes OASIS unit
  • Version → OASIS version set to "1.0"
  • Structures → Converted to OASIS cells

Structure-Level

  • Structure name → Cell name
  • Timestamps → Not preserved (OASIS has different metadata)
  • Elements → Converted with type mapping

Element-Level Mapping

Boundary → Rectangle or Polygon

If the boundary has exactly 5 vertices forming a rectangle:

#![allow(unused)]
fn main() {
// GDSII Boundary (rectangle)
Boundary {
    layer: 1,
    datatype: 0,
    xy: vec![(0,0), (100,0), (100,50), (0,50), (0,0)],
}

// Converts to OASIS Rectangle
Rectangle {
    layer: 1,
    datatype: 0,
    x: 0,
    y: 0,
    width: 100,
    height: 50,
}
}

Otherwise, converts to polygon:

#![allow(unused)]
fn main() {
// GDSII Boundary (triangle)
Boundary {
    xy: vec![(0,0), (100,0), (50,100), (0,0)],
}

// Converts to OASIS Polygon
Polygon {
    x: 0,
    y: 0,
    points: vec![(0,0), (100,0), (50,100), (0,0)],
}
}

Path → Path

Direct conversion with width preserved:

#![allow(unused)]
fn main() {
// GDSII Path
GPath {
    layer: 2,
    datatype: 0,
    width: Some(10),
    xy: vec![(0,0), (100,100)],
}

// Converts to OASIS Path
OPath {
    layer: 2,
    datatype: 0,
    width: 10,
    points: vec![(0,0), (100,100)],
}
}

Text → Text

Direct text conversion:

#![allow(unused)]
fn main() {
// GDSII Text
GText {
    string: "LABEL",
    xy: (100, 100),
}

// Converts to OASIS Text
OText {
    string: "LABEL",
    x: 100,
    y: 100,
}
}

StructRef → Placement

Single cell instances:

#![allow(unused)]
fn main() {
// GDSII StructRef
StructRef {
    sname: "SUBCELL",
    xy: (1000, 2000),
    strans: Some(...),
}

// Converts to OASIS Placement
Placement {
    cell_name: "SUBCELL",
    x: 1000,
    y: 2000,
    // Transformation preserved
}
}

ArrayRef → Placement with Repetition

Arrays are converted to repetitions:

#![allow(unused)]
fn main() {
// GDSII ArrayRef
ArrayRef {
    sname: "SUBCELL",
    columns: 10,
    rows: 5,
    xy: [
        (0, 0),      // Reference point
        (1000, 0),   // Column spacing
        (0, 2000),   // Row spacing
    ],
}

// Converts to OASIS Placement with Repetition
Placement {
    cell_name: "SUBCELL",
    x: 0,
    y: 0,
    repetition: Some(Repetition {
        x_dimension: 10,
        y_dimension: 5,
        x_space: 1000,
        y_space: 2000,
    }),
}
}

Node → Polygon

Nodes are converted to polygons:

#![allow(unused)]
fn main() {
// GDSII Node
Node {
    layer: 1,
    xy: vec![(0,0), (100,0), (100,100)],
}

// Converts to OASIS Polygon
Polygon {
    layer: 1,
    points: vec![(0,0), (100,0), (100,100)],
}
}

Box → Rectangle

Boxes become rectangles:

#![allow(unused)]
fn main() {
// GDSII Box (5 points)
GDSBox {
    xy: [(0,0), (100,0), (100,100), (0,100), (0,0)],
}

// Converts to OASIS Rectangle
Rectangle {
    x: 0,
    y: 0,
    width: 100,
    height: 100,
}
}

Units Conversion

#![allow(unused)]
fn main() {
// GDSII units
gds.units = (1e-6, 1e-9);  // 1µm user, 1nm database

// OASIS conversion
oasis.unit = 1e-9;  // Uses database unit
}

Name Table Population

OASIS requires pre-registered names:

#![allow(unused)]
fn main() {
// Cell names are automatically registered
oasis.names.cell_names.insert(0, "CELL1".to_string());
oasis.names.cell_names.insert(1, "CELL2".to_string());

// Text strings are registered
oasis.names.text_strings.insert(0, "LABEL".to_string());
}

Transformation Handling

STrans (transformation) data is converted:

#![allow(unused)]
fn main() {
// GDSII STrans
STrans {
    reflect_x: true,
    magnification: Some(1.5),
    angle: Some(90.0),
}

// OASIS Placement transformation
Placement {
    mirror_x: true,
    magnification: Some(1.5),
    angle: Some(90.0),
    ...
}
}

Properties

Element properties are preserved where possible:

#![allow(unused)]
fn main() {
// GDSII Property
GDSProperty {
    attribute: 1,
    value: "property_value",
}

// OASIS Property
OASISProperty {
    name: "attr_1",
    values: vec![PropertyValue::String("property_value")],
}
}

Complete Example with Analysis

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, converter};

fn analyze_and_convert(input_path: &str, output_path: &str) 
    -> Result<(), Box<dyn std::error::Error>> 
{
    // Read GDSII
    let gds = GDSIIFile::read_from_file(input_path)?;
    
    // Analyze before conversion
    println!("GDSII Analysis:");
    println!("  Library: {}", gds.library_name);
    println!("  Structures: {}", gds.structures.len());
    
    let mut total_elements = 0;
    let mut boundaries = 0;
    let mut rectangles_detected = 0;
    
    for structure in &gds.structures {
        total_elements += structure.elements.len();
        for element in &structure.elements {
            if let GDSElement::Boundary(b) = element {
                boundaries += 1;
                if converter::is_rectangle(&b.xy).is_some() {
                    rectangles_detected += 1;
                }
            }
        }
    }
    
    println!("  Total elements: {}", total_elements);
    println!("  Boundaries: {}", boundaries);
    println!("  Rectangles detected: {}", rectangles_detected);
    
    // Convert
    let oasis = converter::gdsii_to_oasis(&gds)?;
    
    // Analyze after conversion
    println!("\nOASIS Analysis:");
    println!("  Cells: {}", oasis.cells.len());
    
    let mut oasis_elements = 0;
    let mut oasis_rectangles = 0;
    
    for cell in &oasis.cells {
        oasis_elements += cell.elements.len();
        for element in &cell.elements {
            if let OASISElement::Rectangle(_) = element {
                oasis_rectangles += 1;
            }
        }
    }
    
    println!("  Total elements: {}", oasis_elements);
    println!("  Rectangles: {}", oasis_rectangles);
    
    // Write output
    oasis.write_to_file(output_path)?;
    println!("\n✅ Conversion complete: {}", output_path);
    
    Ok(())
}
}

Tips for Best Results

  1. Clean input files - Remove unused structures before conversion
  2. Use rectangles - Rectangular boundaries convert to compact rectangles
  3. Simplify hierarchies - Flatten unnecessary hierarchy levels if needed
  4. Check layer maps - Verify layer numbers are within OASIS limits
  5. Validate output - Always check converted files in your EDA tool

File Size Comparison

Typical OASIS files are 2-5× smaller than equivalent GDSII:

#![allow(unused)]
fn main() {
// Example file sizes:
// design.gds:  10.5 MB
// design.oas:   2.3 MB  (78% reduction)
}

This is due to:

  • Variable-length integer encoding
  • Rectangle primitives
  • Name table compression
  • Delta encoding for coordinates

OASIS to GDSII Conversion

Detailed guide for converting OASIS files to GDSII format.

Basic Conversion

#![allow(unused)]
fn main() {
use laykit::{OASISFile, converter};

let oasis = OASISFile::read_from_file("input.oas")?;
let gds = converter::oasis_to_gdsii(&oasis)?;
gds.write_to_file("output.gds")?;
}

What Gets Converted

File-Level

  • OASIS version → Stored as GDSII version (typically 3 or 5)
  • Unit → OASIS unit becomes GDSII database unit
  • Cells → Converted to GDSII structures
  • Name tables → Expanded to actual strings

Cell-Level

  • Cell name → Structure name
  • Timestamps → Set to current time (OASIS doesn't always store timestamps)
  • Elements → Converted with type mapping

Element-Level Mapping

Rectangle → Boundary

OASIS rectangles become 5-vertex closed polygons:

#![allow(unused)]
fn main() {
// OASIS Rectangle
Rectangle {
    layer: 1,
    datatype: 0,
    x: 0,
    y: 0,
    width: 100,
    height: 50,
}

// Converts to GDSII Boundary
Boundary {
    layer: 1,
    datatype: 0,
    xy: vec![
        (0, 0),
        (100, 0),
        (100, 50),
        (0, 50),
        (0, 0),  // Closed polygon
    ],
}
}

Polygon → Boundary

Direct conversion:

#![allow(unused)]
fn main() {
// OASIS Polygon
Polygon {
    layer: 2,
    datatype: 0,
    x: 0,
    y: 0,
    points: vec![(0,0), (100,0), (50,100), (0,0)],
}

// Converts to GDSII Boundary
Boundary {
    layer: 2,
    datatype: 0,
    xy: vec![(0,0), (100,0), (50,100), (0,0)],
}
}

Note: OASIS polygon points are relative to (x, y), so they're adjusted to absolute coordinates.

Path → Path

Paths are directly converted:

#![allow(unused)]
fn main() {
// OASIS Path
OPath {
    layer: 3,
    datatype: 0,
    width: 10,
    points: vec![(0,0), (100,100)],
    start_extension: 5,
    end_extension: 5,
}

// Converts to GDSII Path
GPath {
    layer: 3,
    datatype: 0,
    pathtype: 0,
    width: Some(10),
    xy: vec![(0,0), (100,100)],
    // Extensions handled by pathtype
}
}

Trapezoid → Boundary

Trapezoids are converted to 5-vertex polygons:

#![allow(unused)]
fn main() {
// OASIS Trapezoid
Trapezoid {
    x: 0,
    y: 0,
    width: 100,
    height: 50,
    delta_a: 10,  // Top offset
    delta_b: -10, // Bottom offset
}

// Converts to GDSII Boundary
Boundary {
    xy: vec![
        (0, 0),
        (90, 0),      // width + delta_b
        (110, 50),    // width + delta_a at top
        (10, 50),     // delta_a
        (0, 0),
    ],
}
}

CTrapezoid → Boundary

Constrained trapezoids are also converted to polygons:

#![allow(unused)]
fn main() {
// OASIS CTrapezoid
CTrapezoid {
    layer: 4,
    ctrapezoid_type: 0,
    width: 100,
    height: 50,
    ...
}

// Converts to GDSII Boundary with appropriate vertices
}

Circle → Boundary (Approximated)

Circles are approximated with polygons:

#![allow(unused)]
fn main() {
// OASIS Circle
Circle {
    layer: 5,
    x: 500,
    y: 500,
    radius: 100,
}

// Converts to GDSII Boundary with 32 vertices (approximation)
Boundary {
    layer: 5,
    xy: vec![
        // 32 points forming a circle
        // calculated as: (x + r*cos(θ), y + r*sin(θ))
    ],
}
}

Note: The number of segments can be adjusted for accuracy vs. file size.

Text → Text

Direct text conversion:

#![allow(unused)]
fn main() {
// OASIS Text
OText {
    layer: 6,
    texttype: 0,
    string: "LABEL",
    x: 1000,
    y: 1000,
}

// Converts to GDSII Text
GText {
    layer: 6,
    texttype: 0,
    string: "LABEL",
    xy: (1000, 1000),
}
}

Placement → StructRef or ArrayRef

Single placements:

#![allow(unused)]
fn main() {
// OASIS Placement (no repetition)
Placement {
    cell_name: "SUBCELL",
    x: 1000,
    y: 2000,
    magnification: Some(1.5),
    angle: Some(90.0),
    mirror_x: true,
    repetition: None,
}

// Converts to GDSII StructRef
StructRef {
    sname: "SUBCELL",
    xy: (1000, 2000),
    strans: Some(STrans {
        reflect_x: true,
        magnification: Some(1.5),
        angle: Some(90.0),
        ...
    }),
}
}

Placements with repetition:

#![allow(unused)]
fn main() {
// OASIS Placement with Repetition
Placement {
    cell_name: "SUBCELL",
    x: 0,
    y: 0,
    repetition: Some(Repetition {
        x_dimension: 10,
        y_dimension: 5,
        x_space: 1000,
        y_space: 2000,
    }),
}

// Converts to GDSII ArrayRef
ArrayRef {
    sname: "SUBCELL",
    columns: 10,
    rows: 5,
    xy: [
        (0, 0),           // Reference point
        (10000, 0),       // Column spacing (10 * 1000)
        (0, 10000),       // Row spacing (5 * 2000)
    ],
}
}

Units Conversion

#![allow(unused)]
fn main() {
// OASIS unit
oasis.unit = 1e-9;  // 1nm

// GDSII units conversion
gds.units = (1e-6, 1e-9);  // 1µm user unit, 1nm database unit
//              ^      ^
//          user   database (from OASIS)
}

The user unit is set to 1000× the database unit by default.

Timestamp Handling

Since OASIS may not store modification times:

#![allow(unused)]
fn main() {
// All structures get current timestamp
structure.creation_time = GDSTime::now();
structure.modification_time = GDSTime::now();
}

Name Table Expansion

OASIS name tables are expanded:

#![allow(unused)]
fn main() {
// OASIS has compact name tables
oasis.names.cell_names: {0: "CELL1", 1: "CELL2"}
oasis.names.text_strings: {0: "LABEL"}

// Expanded in GDSII as actual strings
gds.structures[0].name = "CELL1";
gds.structures[1].name = "CELL2";
// Text elements contain "LABEL" directly
}

Coordinate Conversion

OASIS uses 64-bit coordinates, GDSII uses 32-bit:

#![allow(unused)]
fn main() {
// OASIS coordinates (i64)
let oasis_x: i64 = 1_000_000_000;

// Converted to GDSII (i32)
let gds_x: i32 = oasis_x as i32;  // May truncate if too large!
}

Warning: Very large OASIS coordinates may overflow GDSII's 32-bit limit.

Complete Example with Validation

#![allow(unused)]
fn main() {
use laykit::{OASISFile, converter, GDSElement};

fn convert_and_validate(input_path: &str, output_path: &str) 
    -> Result<(), Box<dyn std::error::Error>> 
{
    // Read OASIS
    let oasis = OASISFile::read_from_file(input_path)?;
    
    println!("OASIS Analysis:");
    println!("  Version: {}", oasis.version);
    println!("  Unit: {} meters", oasis.unit);
    println!("  Cells: {}", oasis.cells.len());
    
    let mut total_elements = 0;
    let mut rectangles = 0;
    let mut circles = 0;
    let mut trapezoids = 0;
    
    for cell in &oasis.cells {
        total_elements += cell.elements.len();
        for element in &cell.elements {
            match element {
                OASISElement::Rectangle(_) => rectangles += 1,
                OASISElement::Circle(_) => circles += 1,
                OASISElement::Trapezoid(_) => trapezoids += 1,
                OASISElement::CTrapezoid(_) => trapezoids += 1,
                _ => {}
            }
        }
    }
    
    println!("  Total elements: {}", total_elements);
    println!("  Rectangles: {}", rectangles);
    println!("  Circles: {} (will be approximated)", circles);
    println!("  Trapezoids: {}", trapezoids);
    
    // Convert
    let gds = converter::oasis_to_gdsii(&oasis)?;
    
    // Analyze GDSII output
    println!("\nGDSII Output:");
    println!("  Library: {}", gds.library_name);
    println!("  Structures: {}", gds.structures.len());
    println!("  Units: {}µm user, {}nm database", 
        gds.units.0 * 1e6, gds.units.1 * 1e9);
    
    let mut gds_elements = 0;
    let mut boundaries = 0;
    
    for structure in &gds.structures {
        gds_elements += structure.elements.len();
        for element in &structure.elements {
            if let GDSElement::Boundary(_) = element {
                boundaries += 1;
            }
        }
    }
    
    println!("  Total elements: {}", gds_elements);
    println!("  Boundaries: {} (includes converted shapes)", boundaries);
    
    // Write output
    gds.write_to_file(output_path)?;
    println!("\n✅ Conversion complete: {}", output_path);
    
    Ok(())
}
}

Handling Edge Cases

Large Coordinates

#![allow(unused)]
fn main() {
// Check for coordinate overflow
if oasis_coord > i32::MAX as i64 {
    eprintln!("Warning: Coordinate {} exceeds GDSII limit", oasis_coord);
    // May need to scale design or split into multiple files
}
}

Complex Repetitions

#![allow(unused)]
fn main() {
// Large arrays might create many instances
if repetition.x_dimension * repetition.y_dimension > 10000 {
    println!("Warning: Large array ({} instances)", 
        repetition.x_dimension * repetition.y_dimension);
}
}

Circle Approximation Quality

#![allow(unused)]
fn main() {
// For critical circles, verify approximation:
// - Check vertex count (typically 32-64)
// - Ensure radius error is acceptable
// - Consider increasing segments for large circles
}

Tips for Best Results

  1. Check coordinate ranges - Ensure they fit in 32-bit integers
  2. Verify circle approximations - May need visual inspection
  3. Test in target tool - Load converted GDSII in your EDA software
  4. Compare file sizes - GDSII will be larger than OASIS
  5. Preserve metadata - Add comments about conversion if needed

File Size Comparison

GDSII files are typically larger than OASIS:

#![allow(unused)]
fn main() {
// Example file sizes:
// design.oas:  2.3 MB
// design.gds: 10.5 MB  (4.5× larger)
}

This is expected due to GDSII's less efficient encoding.

Geometry

The geometry module provides geometric primitives and operations on polygons.

Bounding Box

#![allow(unused)]
fn main() {
use laykit::{bounding_box, BoundingBox};

let pts = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 5.0), (0.0, 5.0)];
let bb = bounding_box(&pts).unwrap();

println!("width:  {}", bb.width());   // 10.0
println!("height: {}", bb.height());  // 5.0
println!("area:   {}", bb.area());    // 50.0
println!("center: {:?}", bb.center()); // (5.0, 2.5)

// Combine two bounding boxes
let bb2 = BoundingBox { x_min: 5.0, x_max: 20.0, y_min: -2.0, y_max: 3.0 };
let merged = bb.union(&bb2);

// Per-element helpers
use laykit::{gds_element_bounding_box, structure_bounding_box, library_bounding_box};
}

Polygon Metrics

#![allow(unused)]
fn main() {
use laykit::{polygon_area, polygon_perimeter, polygon_centroid, polygon_signed_area};

let square = vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)];

println!("area:      {}", polygon_area(&square));       // 100.0
println!("perimeter: {}", polygon_perimeter(&square));  // 40.0
println!("centroid:  {:?}", polygon_centroid(&square)); // (5.0, 5.0)

// Positive = CCW, negative = CW
let signed = polygon_signed_area(&square);
}

Point-in-Polygon

#![allow(unused)]
fn main() {
use laykit::{point_in_polygon, inside};

let poly = vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)];

assert!(point_in_polygon((5.0, 5.0), &poly));   // inside
assert!(!point_in_polygon((15.0, 5.0), &poly)); // outside

// Batch query across multiple polygons
let results = inside(&[(5.0,5.0),(20.0,20.0)], &[poly]);
// results = [true, false]
}

Transforms

#![allow(unused)]
fn main() {
use laykit::{translate, rotate, scale, mirror_x, mirror_y, affine_transform};

let pts = vec![(1.0, 0.0), (2.0, 0.0)];

let moved    = translate(&pts, 5.0, 3.0);
let rotated  = rotate(&pts, std::f64::consts::PI / 2.0, 0.0, 0.0); // 90°
let scaled   = scale(&pts, 2.0, 2.0, 0.0, 0.0);
let flipped  = mirror_x(&pts, 0.0); // reflect over y = 0
let flipped2 = mirror_y(&pts, 0.0); // reflect over x = 0

// 2×3 affine matrix [a, b, c, d, tx, ty]
let mat = [1.0_f64, 0.0, 0.0, 1.0, 5.0, 3.0]; // pure translation
let transformed = affine_transform(&pts, &mat);
}

Orientation & Utilities

#![allow(unused)]
fn main() {
use laykit::{is_counter_clockwise, ensure_counter_clockwise, fillet, fracture_to_rectangles};

let pts = vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)];

// Check/enforce winding order
let ccw = is_counter_clockwise(&pts);
let enforced = ensure_counter_clockwise(&pts);

// Round polygon corners (radius, points per arc quadrant)
let rounded = fillet(&pts, 1.0, 8);

// Decompose polygon into non-overlapping rectangles
let rects = fracture_to_rectangles(&pts);
}

Boolean Operations

The boolean_ops module provides polygon clipping, offsetting, slicing, and convex hull.

Boolean Operations

#![allow(unused)]
fn main() {
use laykit::{boolean, BooleanOp};

let a = vec![vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)]];
let b = vec![vec![(5.0,0.0),(15.0,0.0),(15.0,10.0),(5.0,10.0)]];

let union  = boolean(&a, &b, BooleanOp::Or);   // A ∪ B
let inter  = boolean(&a, &b, BooleanOp::And);  // A ∩ B
let diff   = boolean(&a, &b, BooleanOp::Not);  // A − B
let xor    = boolean(&a, &b, BooleanOp::Xor);  // A △ B
}

Each function takes &[Vec<(f64, f64)>] (a slice of polygons) and returns Vec<Vec<(f64, f64)>>.

Offset (Expand / Shrink)

#![allow(unused)]
fn main() {
use laykit::offset;

let polys = vec![vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)]];

// Expand outward by 1 unit
let expanded = offset(&polys, 1.0, 0.01);

// Shrink inward by 1 unit
let shrunk = offset(&polys, -1.0, 0.01);
}

The third argument (tolerance) controls arc discretisation for curved corners — smaller values give smoother results.

Slice

#![allow(unused)]
fn main() {
use laykit::{slice, Axis};

let polys = vec![vec![(0.0,0.0),(10.0,0.0),(10.0,10.0),(0.0,10.0)]];

// Cut vertically at x = 5
let (left, right) = slice(&polys, 5.0, Axis::X);

// Cut horizontally at y = 4
let (below, above) = slice(&polys, 4.0, Axis::Y);
}

Convex Hull

#![allow(unused)]
fn main() {
use laykit::convex_hull;

let pts = vec![(0.0,0.0),(5.0,0.0),(5.0,5.0),(0.0,5.0),(2.5,2.5)];
let hull = convex_hull(&pts);
// Returns the 4 outer corners; the interior point is excluded
}

FlexPath & Curves

FlexPath

FlexPath generates a filled polygon from a centerline path, like a stroke with configurable width, join style, and end caps.

#![allow(unused)]
fn main() {
use laykit::{FlexPath, EndCap, Join};
use std::f64::consts::PI;

let mut path = FlexPath::new((0.0, 0.0), 2.0, 1, 0);
// new(start, width, layer, datatype)

// Add segments
path.segment((10.0, 0.0), None, None, false);     // absolute
path.segment((5.0, 0.0), Some(3.0), None, true);  // relative, taper to width 3

// Add an arc
path.arc(5.0, 0.0, PI / 2.0, None); // radius, initial_angle, final_angle

// Add a cubic Bezier
path.bezier((2.0, 4.0), (8.0, 4.0), (10.0, 0.0), None, 32);

// Styling
path.end_caps = (EndCap::Round, EndCap::Flush);
path.join = Join::Miter;

// Convert to polygon
let poly = path.to_polygon().unwrap();
let bb   = path.bounding_box().unwrap();
let len  = path.length();
}

EndCap variants

VariantDescription
FlushSquare cap flush with end point
HalfWidthSquare cap extended by half the path width
Extended(f64)Square cap extended by a fixed distance
RoundSemicircular cap

Join variants

VariantDescription
NaturalNo extra join geometry
MiterSharp corner (default)
BevelClipped corner
RoundCircular join

RobustPath

RobustPath wraps FlexPath and adds self-intersection checking (for complex paths that loop back on themselves):

#![allow(unused)]
fn main() {
use laykit::RobustPath;

let mut rp = RobustPath::new((0.0, 0.0), 1.0, 1, 0);
rp.segment((10.0, 0.0), None, false);
let poly = rp.to_polygon().unwrap();
}

Curve

Curve builds a polyline from a sequence of geometric primitives, which can then be used as a path centerline or polygon outline.

#![allow(unused)]
fn main() {
use laykit::Curve;
use std::f64::consts::PI;

let mut c = Curve::new((0.0, 0.0));

c.line((10.0, 0.0), false);                      // line to absolute point
c.arc(5.0, 0.0, PI / 2.0);                       // arc by angles
c.bezier2((5.0, 5.0), (10.0, 0.0));              // quadratic Bezier
c.bezier3((2.0,4.0), (8.0,4.0), (10.0,0.0));    // cubic Bezier
c.smooth_bezier((8.0, 4.0), (10.0, 0.0));        // smooth cubic (auto first ctrl)
c.ellipse_arc(5.0, 3.0, 0.0, false, true, (15.0, 0.0)); // SVG-style elliptical arc
c.interpolate(&points, 0.0);                      // Catmull-Rom spline
c.close();

let pts = c.get_points();
let len  = c.length();
}

Standalone Shape Functions

#![allow(unused)]
fn main() {
use laykit::{ellipse, regular_polygon, rounded_rectangle, star, spiral};

// Ellipse
let pts = ellipse((0.0,0.0), 10.0, 5.0, 0.0, 0.01);
// center, rx, ry, initial_angle, tolerance

// Regular polygon (e.g. hexagon)
let hex = regular_polygon((0.0,0.0), 5.0, 6, 0.0);
// center, circumradius, sides, initial_angle

// Rounded rectangle
let rr = rounded_rectangle((0.0,0.0), 20.0, 10.0, 2.0, 0.01);
// corner, width, height, corner_radius, tolerance

// Star
let s = star((0.0,0.0), 2.0, 5.0, 5, 0.0);
// center, inner_radius, outer_radius, points, initial_angle

// Spiral
let sp = spiral((0.0,0.0), 1.0, 10.0, 3.0, 0.01);
// center, r_start, r_end, turns, tolerance
}

Topology

The topology module manages cell hierarchies — dependency ordering, flattening, layer queries, and library merging.

Cell Dependencies

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, top_level_cells, direct_references, cell_dependencies, dependency_order};

let gds = GDSIIFile::read_from_file("design.gds")?;

// Cells not referenced by any other cell
let tops = top_level_cells(&gds);
for cell in &tops {
    println!("top: {}", cell.name);
}

// Direct children of a cell
let children = direct_references(&gds.structures[0]);

// All transitive dependencies of a cell
let all_deps = cell_dependencies("TOP", &gds);

// Topological sort: leaf cells first, root last
let order = dependency_order(&gds);
for i in order {
    println!("{}", gds.structures[i].name);
}
}

Hierarchy Validation

#![allow(unused)]
fn main() {
use laykit::{detect_cycles, validate_hierarchy};

// Find circular references
let cycles = detect_cycles(&gds);
if !cycles.is_empty() {
    println!("Cycles: {:?}", cycles);
}

// Full validation (missing refs, cycles)
match validate_hierarchy(&gds) {
    Ok(())   => println!("Hierarchy is valid"),
    Err(err) => println!("Errors: {:?}", err),
}
}

Flattening

#![allow(unused)]
fn main() {
use laykit::flatten_structure;

// Expand all cell references into a flat list of elements
// (coordinates are transformed to the top-level frame)
let flat = flatten_structure("TOP", &gds, None);        // unlimited depth
let flat2 = flatten_structure("TOP", &gds, Some(2));    // max 2 levels deep
}

Layer Queries

#![allow(unused)]
fn main() {
use laykit::{layers_in_structure, layers_in_library, filter_by_layer,
             element_layer, total_element_count};

// Which layers are used?
let layers = layers_in_library(&gds);

// Elements on a specific layer
let metal1 = filter_by_layer(&gds.structures[0], 1);

// Layer of a single element
if let Some(layer) = element_layer(&element) {
    println!("layer {}", layer);
}

// Total element count across all structures
println!("{} elements total", total_element_count(&gds));
}

Library Merge

#![allow(unused)]
fn main() {
use laykit::{merge_library, merge_library_overwrite};

let mut target = GDSIIFile::read_from_file("base.gds")?;
let source = GDSIIFile::read_from_file("extra.gds")?;

// Add cells from source that don't already exist in target
let added = merge_library(&mut target, &source);

// Add all cells, overwriting duplicates
let replaced = merge_library_overwrite(&mut target, &source);

println!("{} cells added", added);
}

CLI Tool

LayKit includes a command-line tool for quick file operations without writing code.

Installation

The CLI tool is automatically built when you build the project:

cargo build --release

The binary will be available at target/release/laykit.

Commands

Convert

Convert between GDSII and OASIS formats:

# GDSII to OASIS
laykit convert input.gds output.oas

# OASIS to GDSII
laykit convert input.oas output.gds

Format Detection: The input file format is automatically detected by reading the magic bytes at the beginning of the file, not by file extension. This means you can convert files regardless of their extension:

# Works even if the file has the wrong extension
laykit convert myfile.dat output.oas  # Detects actual format from file content
  • GDSII magic bytes: 00 06 00 02 (HEADER record)
  • OASIS magic bytes: %SEMI-OASIS\r\n

Info

Display detailed information about a layout file:

laykit info design.gds

Note: Like the convert command, the info command detects the file format using magic bytes, so it works regardless of the file extension.

Output includes:

  • File size and format
  • Library/cell names
  • Structure count
  • Element counts by type
  • Creation timestamps

Example output:

═══════════════════════════════════════════════════════
  GDSII File Information
═══════════════════════════════════════════════════════

File: design.gds
Size: 15234 bytes (14.88 KB)

Library: MY_LIBRARY
Version: 600
Units: 1.000e-06 user, 1.000e-09 database (meters)

Structures: 5

  [1] TOP
      Created: 2025-01-15 14:30:00
      Elements: 145
  [2] SUBCELL_A
      Created: 2025-01-15 14:30:00
      Elements: 23
  ...

Total Elements: 312

Element Breakdown:
  Boundary     156
  Path          89
  Text          45
  StructRef     22

Validate

Validate file structure and check for common issues:

laykit validate layout.gds

Note: The validate command also uses magic byte detection to identify the file format automatically.

The validator checks for:

  • Empty library/structure names
  • Invalid unit values
  • Unclosed boundaries
  • Paths with insufficient points
  • Undefined structure references
  • Invalid array dimensions
  • Duplicate structure names

Example output:

═══════════════════════════════════════════════════════
  Validation Results
═══════════════════════════════════════════════════════

File: layout.gds

⚠ Found 2 issue(s):

  [1] Structure 'TOP' element 5 (Boundary): not closed
  [2] Structure 'CELL2': references undefined structure 'MISSING'

Help

Show usage information:

laykit help
# or
laykit --help

Use Cases

Quick Format Conversion

# Convert all GDS files to OASIS
for file in *.gds; do
    laykit convert "$file" "${file%.gds}.oas"
done

Batch Validation

# Validate all layout files
for file in *.gds *.oas; do
    echo "Validating $file"
    laykit validate "$file"
done

File Inspection

# Get quick info about multiple files
for file in designs/*.gds; do
    echo "=== $file ==="
    laykit info "$file" | grep -E "Structures:|Total Elements:"
done

Error Handling

The CLI tool returns appropriate exit codes:

  • 0 - Success
  • 1 - Error (file not found, invalid format, etc.)

This makes it suitable for use in scripts and CI/CD pipelines:

if laykit validate input.gds; then
    echo "File is valid"
    laykit convert input.gds output.oas
else
    echo "Validation failed"
    exit 1
fi

Performance

The CLI tool is optimized for:

  • Fast startup (no heavy initialization)
  • Efficient memory usage
  • Streaming I/O where possible
  • Minimal overhead for small files

For very large files (>1GB), consider using the streaming parser API directly in your Rust code.

Streaming Parser

The streaming parser allows you to process large GDSII files without loading the entire file into memory. This is essential for working with multi-gigabyte layout files.

Overview

Instead of reading the entire file at once, the streaming parser:

  • Reads the file header and metadata
  • Processes structures one at a time
  • Uses callbacks to handle each structure
  • Minimizes memory usage

Basic Usage

#![allow(unused)]
fn main() {
use laykit::{StreamingGDSIIReader, StatisticsCollector};
use std::fs::File;
use std::io::BufReader;

// Open file
let file = File::open("large_design.gds")?;
let reader = BufReader::new(file);

// Create streaming reader
let mut streaming_reader = StreamingGDSIIReader::new(reader)?;

// Access file metadata
println!("Library: {}", streaming_reader.library_name());
println!("Units: {:?}", streaming_reader.units());

// Process structures with callback
let mut stats = StatisticsCollector::new();
streaming_reader.process_structures(&mut stats)?;

println!("Processed {} structures", stats.structure_count);
}

Callback Interface

Implement the StructureCallback trait to process structures:

#![allow(unused)]
fn main() {
use laykit::{StructureCallback, GDSStructure};

struct MyCallback {
    count: usize,
}

impl StructureCallback for MyCallback {
    fn on_structure(&mut self, structure: &GDSStructure) 
        -> Result<(), Box<dyn std::error::Error>> 
    {
        println!("Processing: {}", structure.name);
        println!("  Elements: {}", structure.elements.len());
        self.count += 1;
        Ok(())
    }
}

// Use it
let mut callback = MyCallback { count: 0 };
streaming_reader.process_structures(&mut callback)?;
}

Built-in Collectors

Statistics Collector

Collect basic statistics about the file:

#![allow(unused)]
fn main() {
use laykit::StatisticsCollector;

let mut stats = StatisticsCollector::new();
streaming_reader.process_structures(&mut stats)?;

println!("Structures: {}", stats.structure_count);
println!("Total elements: {}", stats.element_count);
}

Structure Name Collector

Extract all structure names:

#![allow(unused)]
fn main() {
use laykit::StructureNameCollector;

let mut collector = StructureNameCollector::new();
streaming_reader.process_structures(&mut collector)?;

for name in &collector.names {
    println!("Structure: {}", name);
}
}

Advanced Usage

Filtering Structures

Process only specific structures:

#![allow(unused)]
fn main() {
struct FilterCallback {
    pattern: String,
    matches: Vec<String>,
}

impl StructureCallback for FilterCallback {
    fn on_structure(&mut self, structure: &GDSStructure) 
        -> Result<(), Box<dyn std::error::Error>> 
    {
        if structure.name.contains(&self.pattern) {
            self.matches.push(structure.name.clone());
            println!("Found: {}", structure.name);
        }
        Ok(())
    }
}
}

Extracting Specific Data

Extract specific information while streaming:

#![allow(unused)]
fn main() {
struct LayerAnalyzer {
    layer_counts: HashMap<i16, usize>,
}

impl StructureCallback for LayerAnalyzer {
    fn on_structure(&mut self, structure: &GDSStructure) 
        -> Result<(), Box<dyn std::error::Error>> 
    {
        for element in &structure.elements {
            match element {
                GDSElement::Boundary(b) => {
                    *self.layer_counts.entry(b.layer).or_insert(0) += 1;
                }
                GDSElement::Path(p) => {
                    *self.layer_counts.entry(p.layer).or_insert(0) += 1;
                }
                _ => {}
            }
        }
        Ok(())
    }
}
}

Early Termination

Stop processing when a condition is met:

#![allow(unused)]
fn main() {
struct FindStructure {
    target: String,
    found: bool,
}

impl StructureCallback for FindStructure {
    fn on_structure(&mut self, structure: &GDSStructure) 
        -> Result<(), Box<dyn std::error::Error>> 
    {
        if structure.name == self.target {
            self.found = true;
            // Return error to stop processing
            return Err("Found target structure".into());
        }
        Ok(())
    }
}
}

Memory Considerations

Memory Usage

The streaming parser uses minimal memory:

  • File metadata: ~1 KB
  • Current structure: Depends on structure size
  • Callback data: Depends on your implementation

Total memory: Typically < 10 MB for callback overhead + largest single structure

Comparison

Approach1 GB File Memory10 GB File Memory
Full Load~2 GB~20 GB
Streaming~50 MB~50 MB

Performance

The streaming parser is slightly slower than loading the full file but uses dramatically less memory:

File SizeFull LoadStreamingMemory Saved
100 MB0.5s0.7s~150 MB
1 GB5.0s7.0s~1.5 GB
10 GB50s70s~15 GB

Limitations

Current streaming implementation:

  • ✅ Reads file headers
  • ✅ Processes structures sequentially
  • ✅ Minimal memory usage
  • ⚠️ Element parsing is simplified (structure-level only)
  • ⚠️ Cannot seek backward (forward-only)
  • ⚠️ GDSII only (OASIS streaming coming in v0.2.0)

When to Use Streaming

Use streaming when:

  • Working with files > 500 MB
  • Memory is limited
  • Only need specific information
  • Processing files sequentially

Use full loading when:

  • Files < 100 MB
  • Need random access to structures
  • Modifying the file
  • Performance is critical and memory is available

Future Enhancements

Planned for v0.2.0:

  • Full element parsing in streaming mode
  • OASIS streaming support
  • Parallel structure processing
  • Seeking/indexing support

Property Utilities

LayKit provides utilities for working with GDSII and OASIS properties, making it easier to add, query, and manage metadata.

Overview

Properties are metadata attached to layout elements. They're used for:

  • Design annotations
  • Manufacturing instructions
  • Verification markers
  • Tool-specific data
  • Custom metadata

Property Builder (GDSII)

The PropertyBuilder provides a fluent interface for creating properties:

#![allow(unused)]
fn main() {
use laykit::PropertyBuilder;

let properties = PropertyBuilder::new()
    .add(1, "DEVICE_TYPE=NMOS".to_string())
    .add(2, "WIDTH=10um".to_string())
    .add(3, "LENGTH=0.5um".to_string())
    .build();

// Use in an element
let boundary = Boundary {
    layer: 1,
    datatype: 0,
    xy: vec![(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)],
    properties,
};
}

Property Manager

The PropertyManager helps query and manipulate existing properties:

#![allow(unused)]
fn main() {
use laykit::PropertyManager;

// Create from existing properties
let manager = PropertyManager::from_properties(&boundary.properties);

// Query properties
if let Some(value) = manager.get(1) {
    println!("Device type: {}", value);
}

// Check existence
if manager.has_property(2) {
    println!("Width property exists");
}

// Get all attributes
let attrs = manager.attributes();
println!("Property attributes: {:?}", attrs);

// Convert back to property list
let updated_props = manager.to_properties();
}

OASIS Properties

OASIS properties support multiple value types:

#![allow(unused)]
fn main() {
use laykit::OASISPropertyBuilder;

let properties = OASISPropertyBuilder::new()
    .add_string("name".to_string(), "MyDevice".to_string())
    .add_integer("count".to_string(), 42)
    .add_real("temperature".to_string(), 25.5)
    .add_boolean("verified".to_string(), true)
    .build();

// Use in OASIS element
let rectangle = Rectangle {
    layer: 1,
    datatype: 0,
    x: 0,
    y: 0,
    width: 1000,
    height: 500,
    repetition: None,
    properties,
};
}

Common Use Cases

Device Annotations

#![allow(unused)]
fn main() {
fn annotate_device(boundary: &mut Boundary, device_type: &str, params: &HashMap<String, String>) {
    let mut builder = PropertyBuilder::new();
    
    // Add device type
    builder = builder.add(1, format!("TYPE={}", device_type));
    
    // Add parameters
    for (i, (key, value)) in params.iter().enumerate() {
        builder = builder.add((i + 2) as i16, format!("{}={}", key, value));
    }
    
    boundary.properties = builder.build();
}
}

Manufacturing Instructions

#![allow(unused)]
fn main() {
fn add_manufacturing_notes(element: &mut GDSElement, notes: Vec<String>) {
    let mut builder = PropertyBuilder::new();
    
    for (i, note) in notes.iter().enumerate() {
        builder = builder.add((i + 100) as i16, note.clone());
    }
    
    match element {
        GDSElement::Boundary(b) => b.properties = builder.build(),
        GDSElement::Path(p) => p.properties = builder.build(),
        _ => {}
    }
}
}

Property Filtering

#![allow(unused)]
fn main() {
fn filter_by_property(structure: &GDSStructure, attr: i16, value: &str) -> Vec<&GDSElement> {
    structure.elements.iter()
        .filter(|elem| {
            let props = match elem {
                GDSElement::Boundary(b) => &b.properties,
                GDSElement::Path(p) => &p.properties,
                _ => return false,
            };
            
            let manager = PropertyManager::from_properties(props);
            manager.get(attr) == Some(value)
        })
        .collect()
}
}

Property Extraction

#![allow(unused)]
fn main() {
fn extract_all_properties(file: &GDSIIFile) -> HashMap<String, Vec<String>> {
    let mut property_map = HashMap::new();
    
    for structure in &file.structures {
        for element in &structure.elements {
            let props = match element {
                GDSElement::Boundary(b) => &b.properties,
                GDSElement::Path(p) => &p.properties,
                GDSElement::Text(t) => &t.properties,
                _ => continue,
            };
            
            let manager = PropertyManager::from_properties(props);
            for attr in manager.attributes() {
                if let Some(value) = manager.get(attr) {
                    property_map
                        .entry(format!("ATTR_{}", attr))
                        .or_insert_with(Vec::new)
                        .push(value.to_string());
                }
            }
        }
    }
    
    property_map
}
}

Property Best Practices

Attribute Numbering

Use consistent attribute numbering:

#![allow(unused)]
fn main() {
// Define constants for property attributes
const PROP_DEVICE_TYPE: i16 = 1;
const PROP_WIDTH: i16 = 2;
const PROP_LENGTH: i16 = 3;
const PROP_MANUFACTURER: i16 = 100;
const PROP_LOT_NUMBER: i16 = 101;

let props = PropertyBuilder::new()
    .add(PROP_DEVICE_TYPE, "NMOS".to_string())
    .add(PROP_WIDTH, "10".to_string())
    .add(PROP_MANUFACTURER, "ACME Corp".to_string())
    .build();
}

Value Formatting

Use consistent value formatting:

#![allow(unused)]
fn main() {
// Good: Structured format
builder.add(1, format!("DEVICE=NMOS WIDTH={}um LENGTH={}um", w, l));

// Good: Key-value pairs
builder.add(1, format!("device:NMOS"));
builder.add(2, format!("width:{}um", w));

// Avoid: Inconsistent formats
builder.add(1, format!("NMOS {} {}", w, l)); // Hard to parse
}

Property Validation

Validate properties after loading:

#![allow(unused)]
fn main() {
fn validate_properties(element: &GDSElement) -> Result<(), String> {
    let props = match element {
        GDSElement::Boundary(b) => &b.properties,
        _ => return Ok(()),
    };
    
    let manager = PropertyManager::from_properties(props);
    
    // Check required properties
    if !manager.has_property(PROP_DEVICE_TYPE) {
        return Err("Missing DEVICE_TYPE property".to_string());
    }
    
    // Validate values
    if let Some(width) = manager.get(PROP_WIDTH) {
        if width.parse::<f64>().is_err() {
            return Err("Invalid WIDTH value".to_string());
        }
    }
    
    Ok(())
}
}

Performance Notes

  • Properties are stored as vectors (O(n) lookup)
  • PropertyManager uses HashMap for fast access (O(1) average)
  • Use PropertyManager for frequent queries
  • Use PropertyBuilder for construction
  • Properties add minimal overhead to file size

GDSII vs OASIS Properties

FeatureGDSIIOASIS
AttributeInteger (i16)String name
Value TypeString onlyMultiple types
SizeFixed overheadVariable
LookupBy numberBy name

Future Enhancements

Planned improvements:

  • Property schema validation
  • Type-safe property accessors
  • Property template system
  • Conversion utilities between GDSII/OASIS properties

AREF Expansion

Array references (AREF) in GDSII allow efficient representation of repeated cell instances. LayKit provides utilities to expand these arrays into individual structure references.

Overview

An AREF represents multiple cell instances in a regular grid pattern:

AREF "SUBCELL" 3x2 array:
[0,0] [100,0] [200,0]
[0,100] [100,100] [200,100]

Expansion converts this single AREF into 6 individual SREF (structure reference) elements.

Basic Usage

#![allow(unused)]
fn main() {
use laykit::{expand_array_ref, ArrayRef};

let aref = ArrayRef {
    sname: "SUBCELL".to_string(),
    columns: 3,
    rows: 2,
    xy: vec![
        (0, 0),      // Origin
        (300, 0),    // Column reference point
        (0, 200),    // Row reference point
    ],
    strans: None,
    properties: Vec::new(),
};

// Expand into individual references
let expanded = expand_array_ref(&aref);

// Result: 6 StructRef elements at positions:
// (0,0), (100,0), (200,0), (0,100), (100,100), (200,100)
println!("Created {} instances", expanded.len());
}

How AREF Works

Reference Points

An AREF has three reference points:

  1. Origin - First instance position
  2. Column point - Defines column spacing
  3. Row point - Defines row spacing
#![allow(unused)]
fn main() {
let xy = vec![
    (0, 0),      // Origin: first instance
    (300, 0),    // Column: 3 columns spanning 300 units
    (0, 200),    // Row: 2 rows spanning 200 units
];

// Spacing calculated as:
// col_spacing = (300 - 0) / 3 = 100 per column
// row_spacing = (200 - 0) / 2 = 100 per row
}

Instance Positions

Each instance position is calculated:

position = origin + (col * col_spacing) + (row * row_spacing)

Expanding All Arrays

Process all array references in a structure:

#![allow(unused)]
fn main() {
use laykit::expand_all_array_refs;

let mut structure = /* load structure */;

// Expand all ARefs to SRefs
structure.elements = expand_all_array_refs(&structure.elements);

// Save modified structure
gds.write_to_file("expanded.gds")?;
}

Counting Instances

Count total instances before expansion:

#![allow(unused)]
fn main() {
use laykit::count_expanded_instances;

let count = count_expanded_instances(&structure.elements);
println!("Total instances after expansion: {}", count);
}

Preserving Transformations

AREF transformations are preserved in each expanded instance:

#![allow(unused)]
fn main() {
let aref = ArrayRef {
    sname: "CELL".to_string(),
    columns: 2,
    rows: 2,
    xy: vec![(0, 0), (200, 0), (0, 200)],
    strans: Some(STrans {
        reflection_x: true,
        absolute_magnification: false,
        absolute_angle: false,
        magnification: Some(2.0),
        angle: Some(90.0),
    }),
    properties: vec![
        GDSProperty {
            attribute: 1,
            value: "ARRAYINSTANCE".to_string(),
        }
    ],
};

let expanded = expand_array_ref(&aref);

// Each StructRef has the same strans and properties
for elem in &expanded {
    if let GDSElement::StructRef(sref) = elem {
        assert!(sref.strans.is_some());
        assert_eq!(sref.properties.len(), 1);
    }
}
}

Use Cases

Flattening Arrays

#![allow(unused)]
fn main() {
fn flatten_arrays(gds: &mut GDSIIFile) {
    for structure in &mut gds.structures {
        structure.elements = expand_all_array_refs(&structure.elements);
    }
}
}

Selective Expansion

#![allow(unused)]
fn main() {
fn expand_large_arrays(elements: &[GDSElement], threshold: usize) -> Vec<GDSElement> {
    elements.iter()
        .flat_map(|elem| {
            match elem {
                GDSElement::ArrayRef(aref) => {
                    let count = (aref.rows as usize) * (aref.columns as usize);
                    if count > threshold {
                        expand_array_ref(aref)
                    } else {
                        vec![elem.clone()]
                    }
                }
                _ => vec![elem.clone()]
            }
        })
        .collect()
}
}

Array Analysis

#![allow(unused)]
fn main() {
fn analyze_arrays(structure: &GDSStructure) {
    for element in &structure.elements {
        if let GDSElement::ArrayRef(aref) = element {
            let total = (aref.rows as usize) * (aref.columns as usize);
            println!("Array '{}': {}x{} = {} instances",
                aref.sname, aref.columns, aref.rows, total);
            
            // Calculate bounding box
            let col_spacing = (aref.xy[1].0 - aref.xy[0].0) / aref.columns as i32;
            let row_spacing = (aref.xy[2].1 - aref.xy[0].1) / aref.rows as i32;
            
            println!("  Spacing: col={}, row={}", col_spacing, row_spacing);
        }
    }
}
}

Instance Position Mapping

#![allow(unused)]
fn main() {
fn get_instance_positions(aref: &ArrayRef) -> Vec<(i32, i32)> {
    let expanded = expand_array_ref(aref);
    
    expanded.iter()
        .filter_map(|elem| {
            if let GDSElement::StructRef(sref) = elem {
                Some(sref.xy)
            } else {
                None
            }
        })
        .collect()
}
}

Performance Considerations

Memory Usage

Expansion increases memory usage:

#![allow(unused)]
fn main() {
// Original: 1 AREF element
let aref = ArrayRef { /* 10x10 array */ };

// Expanded: 100 SREF elements
let expanded = expand_array_ref(&aref);

// Memory increase: ~100x for this case
}

When to Expand

Expand when:

  • Tool doesn't support AREF
  • Need to modify individual instances
  • Performing instance-level analysis
  • Converting to formats without array support

Keep AREF when:

  • File size matters
  • Regular grid pattern is important
  • Tool supports AREF natively
  • No modification needed

File Size Impact

Array SizeAREF SizeExpanded SizeRatio
10x10~100 bytes~10 KB100x
100x100~100 bytes~1 MB10,000x
1000x1000~100 bytes~100 MB1,000,000x

Advanced Features

Non-orthogonal Arrays

Arrays don't have to be axis-aligned:

#![allow(unused)]
fn main() {
let diagonal_array = ArrayRef {
    sname: "CELL".to_string(),
    columns: 5,
    rows: 5,
    xy: vec![
        (0, 0),       // Origin
        (500, 500),   // Diagonal columns
        (-500, 500),  // Diagonal rows (perpendicular)
    ],
    strans: None,
    properties: Vec::new(),
};

let expanded = expand_array_ref(&diagonal_array);
// Creates 25 instances in a diamond pattern
}

Validating Arrays

#![allow(unused)]
fn main() {
fn validate_aref(aref: &ArrayRef) -> Result<(), String> {
    if aref.xy.len() != 3 {
        return Err(format!("AREF must have 3 points, found {}", aref.xy.len()));
    }
    
    if aref.columns == 0 || aref.rows == 0 {
        return Err(format!("Invalid dimensions: {}x{}", aref.columns, aref.rows));
    }
    
    // Check for zero spacing
    let col_spacing_x = (aref.xy[1].0 - aref.xy[0].0) / aref.columns as i32;
    let col_spacing_y = (aref.xy[1].1 - aref.xy[0].1) / aref.columns as i32;
    
    if col_spacing_x == 0 && col_spacing_y == 0 {
        return Err("Column spacing is zero".to_string());
    }
    
    Ok(())
}
}

Limitations

Current implementation:

  • ✅ Regular grids
  • ✅ Transformation preservation
  • ✅ Property preservation
  • ⚠️ Assumes valid 3-point AREF format
  • ⚠️ No automatic overlap detection
  • ⚠️ No optimization for partial expansion

Future Enhancements

Planned for future versions:

  • Partial array expansion (expand only a region)
  • Array optimization (detect expandable SREF patterns)
  • Overlap detection and warning
  • Support for OASIS repetitions
  • Array pattern recognition

Basic Usage Examples

Simple examples to get you started with LayKit.

Reading a GDSII File

The most basic operation - reading and displaying information:

use laykit::GDSIIFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("layout.gds")?;
    
    println!("Library: {}", gds.library_name);
    println!("Structures: {}", gds.structures.len());
    
    for structure in &gds.structures {
        println!("  - {} ({} elements)", 
            structure.name, structure.elements.len());
    }
    
    Ok(())
}

Reading an OASIS File

Similar to GDSII but with different structure:

use laykit::OASISFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let oasis = OASISFile::read_from_file("layout.oas")?;
    
    println!("Version: {}", oasis.version);
    println!("Cells: {}", oasis.cells.len());
    
    for cell in &oasis.cells {
        println!("  - {} ({} elements)", 
            cell.name, cell.elements.len());
    }
    
    Ok(())
}

Creating a Simple GDSII File

Create a file with a single rectangle:

use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, Boundary};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create file
    let mut gds = GDSIIFile::new("SIMPLE".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create structure
    let mut structure = GDSStructure {
        name: "TOP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Add rectangle
    structure.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![
            (0, 0),
            (1000, 0),
            (1000, 500),
            (0, 500),
            (0, 0),
        ],
        properties: Vec::new(),
    }));
    
    gds.structures.push(structure);
    gds.write_to_file("simple.gds")?;
    
    println!("✅ Created simple.gds");
    Ok(())
}

Creating a Simple OASIS File

Create an OASIS file with a rectangle:

use laykit::{OASISFile, OASISCell, OASISElement, Rectangle};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut oasis = OASISFile::new();
    oasis.unit = 1e-9;
    
    // Register cell name
    oasis.names.cell_names.insert(0, "TOP".to_string());
    
    let mut cell = OASISCell {
        name: "TOP".to_string(),
        elements: Vec::new(),
    };
    
    // Add rectangle
    cell.elements.push(OASISElement::Rectangle(Rectangle {
        layer: 1,
        datatype: 0,
        x: 0,
        y: 0,
        width: 1000,
        height: 500,
        repetition: None,
        properties: Vec::new(),
    }));
    
    oasis.cells.push(cell);
    oasis.write_to_file("simple.oas")?;
    
    println!("✅ Created simple.oas");
    Ok(())
}

Quick Format Conversion

Convert GDSII to OASIS in just a few lines:

use laykit::{GDSIIFile, converter};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("input.gds")?;
    let oasis = converter::gdsii_to_oasis(&gds)?;
    oasis.write_to_file("output.oas")?;
    println!("✅ Converted to OASIS");
    Ok(())
}

Convert OASIS to GDSII:

use laykit::{OASISFile, converter};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let oasis = OASISFile::read_from_file("input.oas")?;
    let gds = converter::oasis_to_gdsii(&oasis)?;
    gds.write_to_file("output.gds")?;
    println!("✅ Converted to GDSII");
    Ok(())
}

Copying a File

Read and write back (useful for validation):

use laykit::GDSIIFile;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("original.gds")?;
    gds.write_to_file("copy.gds")?;
    println!("✅ File copied");
    Ok(())
}

Error Handling

Proper error handling with user-friendly messages:

use laykit::GDSIIFile;

fn main() {
    match GDSIIFile::read_from_file("layout.gds") {
        Ok(gds) => {
            println!("✅ Successfully read {} structures", gds.structures.len());
        }
        Err(e) => {
            eprintln!("❌ Error: {}", e);
            eprintln!("Make sure the file exists and is a valid GDSII file");
            std::process::exit(1);
        }
    }
}

Next Steps

Working with Elements

Learn how to work with different element types in LayKit.

Counting Elements

Count different element types in a GDSII file:

use laykit::{GDSIIFile, GDSElement};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    
    let mut counts = ElementCounts::default();
    
    for structure in &gds.structures {
        for element in &structure.elements {
            match element {
                GDSElement::Boundary(_) => counts.boundaries += 1,
                GDSElement::Path(_) => counts.paths += 1,
                GDSElement::Text(_) => counts.texts += 1,
                GDSElement::StructRef(_) => counts.refs += 1,
                GDSElement::ArrayRef(_) => counts.arrays += 1,
                GDSElement::Node(_) => counts.nodes += 1,
                GDSElement::Box(_) => counts.boxes += 1,
            }
        }
    }
    
    println!("Element counts:");
    println!("  Boundaries: {}", counts.boundaries);
    println!("  Paths: {}", counts.paths);
    println!("  Texts: {}", counts.texts);
    println!("  References: {}", counts.refs);
    println!("  Arrays: {}", counts.arrays);
    println!("  Nodes: {}", counts.nodes);
    println!("  Boxes: {}", counts.boxes);
    
    Ok(())
}

#[derive(Default)]
struct ElementCounts {
    boundaries: usize,
    paths: usize,
    texts: usize,
    refs: usize,
    arrays: usize,
    nodes: usize,
    boxes: usize,
}

Filtering by Layer

Extract all elements on a specific layer:

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSElement};

fn filter_by_layer(gds: &GDSIIFile, target_layer: i16) {
    for structure in &gds.structures {
        let mut count = 0;
        
        for element in &structure.elements {
            let layer = match element {
                GDSElement::Boundary(b) => Some(b.layer),
                GDSElement::Path(p) => Some(p.layer),
                GDSElement::Text(t) => Some(t.layer),
                GDSElement::Node(n) => Some(n.layer),
                GDSElement::Box(b) => Some(b.layer),
                _ => None,
            };
            
            if layer == Some(target_layer) {
                count += 1;
            }
        }
        
        if count > 0 {
            println!("{}: {} elements on layer {}", 
                structure.name, count, target_layer);
        }
    }
}
}

Calculating Bounding Box

Find the bounding box of all boundaries:

use laykit::{GDSIIFile, GDSElement};

fn calculate_bounds(gds: &GDSIIFile) -> Option<(i32, i32, i32, i32)> {
    let mut min_x = i32::MAX;
    let mut min_y = i32::MAX;
    let mut max_x = i32::MIN;
    let mut max_y = i32::MIN;
    let mut found = false;
    
    for structure in &gds.structures {
        for element in &structure.elements {
            if let GDSElement::Boundary(b) = element {
                for (x, y) in &b.xy {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(*x);
                    max_y = max_y.max(*y);
                    found = true;
                }
            }
        }
    }
    
    if found {
        Some((min_x, min_y, max_x, max_y))
    } else {
        None
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    
    if let Some((min_x, min_y, max_x, max_y)) = calculate_bounds(&gds) {
        println!("Bounding box:");
        println!("  Min: ({}, {})", min_x, min_y);
        println!("  Max: ({}, {})", max_x, max_y);
        println!("  Size: {} × {}", max_x - min_x, max_y - min_y);
    } else {
        println!("No boundaries found");
    }
    
    Ok(())
}

Modifying Elements

Change all elements on layer 1 to layer 2:

use laykit::{GDSIIFile, GDSElement};

fn remap_layer(gds: &mut GDSIIFile, from_layer: i16, to_layer: i16) {
    for structure in &mut gds.structures {
        for element in &mut structure.elements {
            match element {
                GDSElement::Boundary(b) if b.layer == from_layer => {
                    b.layer = to_layer;
                }
                GDSElement::Path(p) if p.layer == from_layer => {
                    p.layer = to_layer;
                }
                GDSElement::Text(t) if t.layer == from_layer => {
                    t.layer = to_layer;
                }
                _ => {}
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::read_from_file("input.gds")?;
    
    remap_layer(&mut gds, 1, 2);
    
    gds.write_to_file("remapped.gds")?;
    println!("✅ Remapped layer 1 to layer 2");
    
    Ok(())
}

Extracting Text Labels

Get all text labels from a design:

use laykit::{GDSIIFile, GDSElement};

fn extract_text_labels(gds: &GDSIIFile) -> Vec<String> {
    let mut labels = Vec::new();
    
    for structure in &gds.structures {
        for element in &structure.elements {
            if let GDSElement::Text(t) = element {
                labels.push(t.string.clone());
            }
        }
    }
    
    labels
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    let labels = extract_text_labels(&gds);
    
    println!("Found {} text labels:", labels.len());
    for (i, label) in labels.iter().enumerate() {
        println!("  {}. {}", i + 1, label);
    }
    
    Ok(())
}

Scaling Coordinates

Scale all coordinates by a factor:

use laykit::{GDSIIFile, GDSElement};

fn scale_design(gds: &mut GDSIIFile, factor: f64) {
    for structure in &mut gds.structures {
        for element in &mut structure.elements {
            match element {
                GDSElement::Boundary(b) => {
                    for (x, y) in &mut b.xy {
                        *x = (*x as f64 * factor) as i32;
                        *y = (*y as f64 * factor) as i32;
                    }
                }
                GDSElement::Path(p) => {
                    for (x, y) in &mut p.xy {
                        *x = (*x as f64 * factor) as i32;
                        *y = (*y as f64 * factor) as i32;
                    }
                    if let Some(width) = &mut p.width {
                        *width = (*width as f64 * factor) as i32;
                    }
                }
                GDSElement::Text(t) => {
                    t.xy.0 = (t.xy.0 as f64 * factor) as i32;
                    t.xy.1 = (t.xy.1 as f64 * factor) as i32;
                }
                _ => {}
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::read_from_file("input.gds")?;
    
    scale_design(&mut gds, 2.0); // 2x scaling
    
    gds.write_to_file("scaled.gds")?;
    println!("✅ Design scaled by 2x");
    
    Ok(())
}

Filtering Structures

Keep only structures matching a pattern:

use laykit::GDSIIFile;

fn filter_structures(gds: &mut GDSIIFile, pattern: &str) {
    gds.structures.retain(|s| s.name.contains(pattern));
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::read_from_file("input.gds")?;
    
    println!("Before: {} structures", gds.structures.len());
    
    filter_structures(&mut gds, "TOP");
    
    println!("After: {} structures", gds.structures.len());
    
    gds.write_to_file("filtered.gds")?;
    println!("✅ Filtered structures");
    
    Ok(())
}

Merging Files

Combine multiple GDSII files:

use laykit::GDSIIFile;

fn merge_files(files: Vec<&str>) -> Result<GDSIIFile, Box<dyn std::error::Error>> {
    let mut merged = GDSIIFile::new("MERGED".to_string());
    merged.units = (1e-6, 1e-9);
    
    for file_path in files {
        let gds = GDSIIFile::read_from_file(file_path)?;
        
        // Copy units from first file
        if merged.structures.is_empty() {
            merged.units = gds.units;
        }
        
        // Add all structures
        for structure in gds.structures {
            merged.structures.push(structure);
        }
    }
    
    Ok(merged)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let merged = merge_files(vec!["file1.gds", "file2.gds", "file3.gds"])?;
    
    println!("Merged {} structures", merged.structures.len());
    
    merged.write_to_file("merged.gds")?;
    println!("✅ Files merged");
    
    Ok(())
}

Hierarchical Designs

Learn how to create and work with hierarchical designs using cell references.

Basic Cell Reference

Create a design with one cell referencing another:

use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, 
             Boundary, StructRef};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("HIERARCHICAL".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create subcell
    let mut subcell = GDSStructure {
        name: "SUBCELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Add a rectangle to subcell
    subcell.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (1000, 0), (1000, 1000), (0, 1000), (0, 0)],
        properties: Vec::new(),
    }));
    
    // Create top cell
    let mut topcell = GDSStructure {
        name: "TOPCELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Reference subcell
    topcell.elements.push(GDSElement::StructRef(StructRef {
        sname: "SUBCELL".to_string(),
        xy: (2000, 2000),
        strans: None,
        properties: Vec::new(),
    }));
    
    gds.structures.push(subcell);
    gds.structures.push(topcell);
    
    gds.write_to_file("hierarchical.gds")?;
    println!("✅ Created hierarchical design");
    
    Ok(())
}

Multiple Instances

Place multiple instances of the same cell:

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, 
             Boundary, StructRef};

fn create_repeated_instances() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("MULTI_INSTANCE".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create unit cell
    let mut unit = GDSStructure {
        name: "UNIT".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    unit.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (500, 0), (500, 500), (0, 500), (0, 0)],
        properties: Vec::new(),
    }));
    
    // Create top cell with multiple instances
    let mut top = GDSStructure {
        name: "TOP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Create a 3x3 grid manually
    for row in 0..3 {
        for col in 0..3 {
            top.elements.push(GDSElement::StructRef(StructRef {
                sname: "UNIT".to_string(),
                xy: (col * 1000, row * 1000),
                strans: None,
                properties: Vec::new(),
            }));
        }
    }
    
    gds.structures.push(unit);
    gds.structures.push(top);
    
    gds.write_to_file("multi_instance.gds")?;
    println!("✅ Created 3×3 array of instances");
    
    Ok(())
}
}

Array References

Use ArrayRef for efficient array representation:

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, 
             Boundary, ArrayRef};

fn create_array() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("ARRAY".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create unit cell
    let mut unit = GDSStructure {
        name: "UNIT".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    unit.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (500, 0), (500, 500), (0, 500), (0, 0)],
        properties: Vec::new(),
    }));
    
    // Create top cell with array reference
    let mut top = GDSStructure {
        name: "TOP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // 10×5 array with 1000nm spacing
    top.elements.push(GDSElement::ArrayRef(ArrayRef {
        sname: "UNIT".to_string(),
        columns: 10,
        rows: 5,
        xy: [
            (0, 0),        // Reference point
            (10000, 0),    // Column extent (10 * 1000)
            (0, 5000),     // Row extent (5 * 1000)
        ],
        strans: None,
        properties: Vec::new(),
    }));
    
    gds.structures.push(unit);
    gds.structures.push(top);
    
    gds.write_to_file("array.gds")?;
    println!("✅ Created 10×5 array");
    
    Ok(())
}
}

Transformations

Apply transformations to cell instances:

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, 
             Boundary, StructRef, STrans};

fn create_transformed_instances() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("TRANSFORMED".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create base cell
    let mut base = GDSStructure {
        name: "BASE".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    base.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (1000, 0), (1000, 500), (0, 500), (0, 0)],
        properties: Vec::new(),
    }));
    
    let mut top = GDSStructure {
        name: "TOP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Original instance
    top.elements.push(GDSElement::StructRef(StructRef {
        sname: "BASE".to_string(),
        xy: (0, 0),
        strans: None,
        properties: Vec::new(),
    }));
    
    // Rotated 90 degrees
    top.elements.push(GDSElement::StructRef(StructRef {
        sname: "BASE".to_string(),
        xy: (3000, 0),
        strans: Some(STrans {
            reflect_x: false,
            absolute_mag: false,
            absolute_angle: false,
            magnification: None,
            angle: Some(90.0),
        }),
        properties: Vec::new(),
    }));
    
    // Mirrored
    top.elements.push(GDSElement::StructRef(StructRef {
        sname: "BASE".to_string(),
        xy: (6000, 0),
        strans: Some(STrans {
            reflect_x: true,
            absolute_mag: false,
            absolute_angle: false,
            magnification: None,
            angle: None,
        }),
        properties: Vec::new(),
    }));
    
    // Scaled 2x
    top.elements.push(GDSElement::StructRef(StructRef {
        sname: "BASE".to_string(),
        xy: (9000, 0),
        strans: Some(STrans {
            reflect_x: false,
            absolute_mag: false,
            absolute_angle: false,
            magnification: Some(2.0),
            angle: None,
        }),
        properties: Vec::new(),
    }));
    
    gds.structures.push(base);
    gds.structures.push(top);
    
    gds.write_to_file("transformed.gds")?;
    println!("✅ Created transformed instances");
    
    Ok(())
}
}

Multi-Level Hierarchy

Create a 3-level hierarchy:

#![allow(unused)]
fn main() {
use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement, 
             Boundary, StructRef};

fn create_multilevel() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("MULTILEVEL".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Level 1: Basic shape
    let mut l1_cell = GDSStructure {
        name: "L1_BASIC".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    l1_cell.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)],
        properties: Vec::new(),
    }));
    
    // Level 2: Group of L1 cells
    let mut l2_cell = GDSStructure {
        name: "L2_GROUP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    for i in 0..4 {
        l2_cell.elements.push(GDSElement::StructRef(StructRef {
            sname: "L1_BASIC".to_string(),
            xy: (i * 200, 0),
            strans: None,
            properties: Vec::new(),
        }));
    }
    
    // Level 3: Top level with L2 cells
    let mut l3_cell = GDSStructure {
        name: "L3_TOP".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    for i in 0..3 {
        l3_cell.elements.push(GDSElement::StructRef(StructRef {
            sname: "L2_GROUP".to_string(),
            xy: (0, i * 500),
            strans: None,
            properties: Vec::new(),
        }));
    }
    
    gds.structures.push(l1_cell);
    gds.structures.push(l2_cell);
    gds.structures.push(l3_cell);
    
    gds.write_to_file("multilevel.gds")?;
    println!("✅ Created 3-level hierarchy");
    
    Ok(())
}
}

Finding Cell Dependencies

Analyze cell reference relationships:

use laykit::{GDSIIFile, GDSElement};
use std::collections::{HashMap, HashSet};

fn analyze_hierarchy(gds: &GDSIIFile) {
    let mut references: HashMap<String, HashSet<String>> = HashMap::new();
    
    // Build reference map
    for structure in &gds.structures {
        let mut refs = HashSet::new();
        
        for element in &structure.elements {
            match element {
                GDSElement::StructRef(sref) => {
                    refs.insert(sref.sname.clone());
                }
                GDSElement::ArrayRef(aref) => {
                    refs.insert(aref.sname.clone());
                }
                _ => {}
            }
        }
        
        references.insert(structure.name.clone(), refs);
    }
    
    // Print hierarchy
    println!("Cell hierarchy:");
    for (cell, refs) in &references {
        if !refs.is_empty() {
            println!("  {} references:", cell);
            for ref_name in refs {
                println!("    - {}", ref_name);
            }
        }
    }
    
    // Find top cells (not referenced by others)
    let all_refs: HashSet<_> = references.values()
        .flat_map(|refs| refs.iter().cloned())
        .collect();
    
    let top_cells: Vec<_> = references.keys()
        .filter(|name| !all_refs.contains(*name))
        .collect();
    
    println!("\nTop-level cells:");
    for cell in top_cells {
        println!("  - {}", cell);
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    analyze_hierarchy(&gds);
    Ok(())
}

Flattening Hierarchy

Flatten a hierarchical design to a single level:

#![allow(unused)]
fn main() {
// Note: This is a simplified example
// Full flattening requires resolving all transformations

use laykit::{GDSIIFile, GDSStructure, GDSTime, GDSElement};

fn flatten_simple(gds: &GDSIIFile, top_cell_name: &str) 
    -> Result<GDSStructure, Box<dyn std::error::Error>> 
{
    let mut flattened = GDSStructure {
        name: format!("{}_FLAT", top_cell_name),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Find top cell
    let top_cell = gds.structures.iter()
        .find(|s| s.name == top_cell_name)
        .ok_or("Top cell not found")?;
    
    // Copy non-reference elements
    for element in &top_cell.elements {
        match element {
            GDSElement::Boundary(_) | 
            GDSElement::Path(_) | 
            GDSElement::Text(_) => {
                flattened.elements.push(element.clone());
            }
            _ => {
                // References would need transformation and recursion
                println!("Warning: Skipping reference (not implemented)");
            }
        }
    }
    
    Ok(flattened)
}
}

Complete Examples

Full working examples demonstrating LayKit features.

Basic GDSII File Creation

Complete program to create a GDSII file with multiple elements:

use laykit::{Boundary, GDSElement, GDSIIFile, GDSStructure, GDSTime, GPath, GText};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create library
    let mut gds = GDSIIFile::new("DEMO_LIB".to_string());
    gds.units = (1e-6, 1e-9); // 1µm, 1nm
    
    // Create structure
    let mut structure = GDSStructure {
        name: "TOP_CELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Add rectangle
    structure.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![
            (0, 0),
            (10000, 0),
            (10000, 5000),
            (0, 5000),
            (0, 0),
        ],
        properties: Vec::new(),
    }));
    
    // Add path
    structure.elements.push(GDSElement::Path(GPath {
        layer: 2,
        datatype: 0,
        pathtype: 0,
        width: Some(100),
        xy: vec![(0, 0), (5000, 0), (5000, 5000)],
        properties: Vec::new(),
    }));
    
    // Add text
    structure.elements.push(GDSElement::Text(GText {
        layer: 3,
        texttype: 0,
        string: "DEMO".to_string(),
        xy: (2500, 2500),
        strans: None,
        properties: Vec::new(),
    }));
    
    gds.structures.push(structure);
    gds.write_to_file("demo.gds")?;
    
    println!("✅ Created demo.gds");
    Ok(())
}

Format Conversion

Convert between GDSII and OASIS:

use laykit::{GDSIIFile, OASISFile, converter};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read GDSII
    println!("Reading GDSII file...");
    let gds = GDSIIFile::read_from_file("input.gds")?;
    println!("  Library: {}", gds.library_name);
    println!("  Structures: {}", gds.structures.len());
    
    // Convert to OASIS
    println!("Converting to OASIS...");
    let oasis = converter::gdsii_to_oasis(&gds)?;
    oasis.write_to_file("output.oas")?;
    println!("  ✅ Written output.oas");
    
    // Read OASIS
    println!("Reading OASIS file...");
    let oasis = OASISFile::read_from_file("output.oas")?;
    println!("  Cells: {}", oasis.cells.len());
    
    // Convert back to GDSII
    println!("Converting to GDSII...");
    let gds2 = converter::oasis_to_gdsii(&oasis)?;
    gds2.write_to_file("roundtrip.gds")?;
    println!("  ✅ Written roundtrip.gds");
    
    println!("\n✅ All conversions completed!");
    Ok(())
}

Processing All Elements

Iterate through and analyze all elements:

use laykit::{GDSIIFile, GDSElement};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let gds = GDSIIFile::read_from_file("design.gds")?;
    
    let mut stats = Statistics::default();
    
    for structure in &gds.structures {
        println!("\n📦 Structure: {}", structure.name);
        
        for element in &structure.elements {
            stats.total_elements += 1;
            
            match element {
                GDSElement::Boundary(b) => {
                    stats.boundaries += 1;
                    println!("  ▢ Boundary: layer {}, {} vertices", 
                        b.layer, b.xy.len());
                }
                GDSElement::Path(p) => {
                    stats.paths += 1;
                    println!("  ─ Path: layer {}, width {:?}", 
                        p.layer, p.width);
                }
                GDSElement::Text(t) => {
                    stats.texts += 1;
                    println!("  T Text: \"{}\"", t.string);
                }
                GDSElement::StructRef(s) => {
                    stats.refs += 1;
                    println!("  → Reference: {}", s.sname);
                }
                GDSElement::ArrayRef(a) => {
                    stats.arrays += 1;
                    println!("  ⊞ Array: {} [{}×{}]", 
                        a.sname, a.columns, a.rows);
                }
                GDSElement::Node(n) => {
                    stats.nodes += 1;
                    println!("  ◉ Node: layer {}", n.layer);
                }
                GDSElement::Box(b) => {
                    stats.boxes += 1;
                    println!("  □ Box: layer {}", b.layer);
                }
            }
        }
    }
    
    println!("\n📊 Statistics:");
    println!("  Total elements: {}", stats.total_elements);
    println!("  Boundaries: {}", stats.boundaries);
    println!("  Paths: {}", stats.paths);
    println!("  Texts: {}", stats.texts);
    println!("  References: {}", stats.refs);
    println!("  Arrays: {}", stats.arrays);
    println!("  Nodes: {}", stats.nodes);
    println!("  Boxes: {}", stats.boxes);
    
    Ok(())
}

#[derive(Default)]
struct Statistics {
    total_elements: usize,
    boundaries: usize,
    paths: usize,
    texts: usize,
    refs: usize,
    arrays: usize,
    nodes: usize,
    boxes: usize,
}

Hierarchical Design

Create a design with cell references:

use laykit::{Boundary, GDSElement, GDSIIFile, GDSStructure, 
             GDSTime, StructRef};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut gds = GDSIIFile::new("HIERARCHICAL".to_string());
    gds.units = (1e-6, 1e-9);
    
    // Create subcell
    let mut subcell = GDSStructure {
        name: "SUBCELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    subcell.elements.push(GDSElement::Boundary(Boundary {
        layer: 1,
        datatype: 0,
        xy: vec![(0, 0), (1000, 0), (1000, 1000), (0, 1000), (0, 0)],
        properties: Vec::new(),
    }));
    
    // Create top cell
    let mut topcell = GDSStructure {
        name: "TOPCELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        elements: Vec::new(),
    };
    
    // Add multiple instances of subcell
    for i in 0..3 {
        topcell.elements.push(GDSElement::StructRef(StructRef {
            sname: "SUBCELL".to_string(),
            xy: (i * 2000, 0),
            strans: None,
            properties: Vec::new(),
        }));
    }
    
    gds.structures.push(subcell);
    gds.structures.push(topcell);
    gds.write_to_file("hierarchical.gds")?;
    
    println!("✅ Created hierarchical.gds");
    Ok(())
}

Running Examples

The repository includes working examples:

# Clone repository
git clone https://github.com/giridharsalana/laykit.git
cd laykit

# Run basic usage example
cargo run --example basic_usage

# Run GDSII-only example
cargo run --example gdsii_only

# Run OASIS-only example
cargo run --example oasis_only

API Reference

Module Overview

laykit
├── gdsii           — GDSII format (read/write, all element types)
├── oasis           — OASIS format (read/write, all element types)
├── converter       — Bidirectional GDSII ↔ OASIS conversion
├── geometry        — Bounding box, transforms, area, point-in-polygon
├── boolean_ops     — Union, intersection, difference, XOR, slice, offset
├── flexpath        — FlexPath with joins/caps; RobustPath
├── curve           — Arc, Bezier, ellipse, spline, polygon, star, spiral
├── topology        — Cell hierarchy: flatten, order, merge, validate
├── streaming       — Streaming GDSII parser for large files
├── aref_expansion  — AREF → individual SREF expansion
├── properties      — Property builders and managers
└── format_detection — File format detection by magic bytes

gdsii

GDSIIFile

MemberTypeDescription
versioni16Format version
library_nameStringLibrary name
units(f64, f64)(user_unit, database_unit) in meters
structuresVec<GDSStructure>All cells
MethodDescription
new(name)Create empty file
read_from_file(path)Read .gds file
write_to_file(path)Write .gds file
read(reader)Read from any Read
write(writer)Write to any Write

GDSStructure

FieldTypeDescription
nameStringCell name
creation_timeGDSTimeCreation timestamp
modification_timeGDSTimeModification timestamp
elementsVec<GDSElement>Elements

GDSElement

#![allow(unused)]
fn main() {
pub enum GDSElement {
    Boundary(Boundary),   // polygon
    Path(GPath),          // wire/trace
    Text(GText),          // text label
    StructRef(StructRef), // cell instance (SREF)
    ArrayRef(ArrayRef),   // cell array (AREF)
    Node(Node),           // net topology
    Box(GDSBox),          // box element
}
}

GDSTime

#![allow(unused)]
fn main() {
GDSTime::now() -> Self
// fields: year, month, day, hour, minute, second: i16
}

oasis

OASISFile

MethodDescription
new()Create empty file
read_from_file(path)Read .oas file
write_to_file(path)Write .oas file

OASISElement

#![allow(unused)]
fn main() {
pub enum OASISElement {
    Rectangle(Rectangle),
    Polygon(Polygon),
    Path(OPath),
    Trapezoid(Trapezoid),
    CTrapezoid(CTrapezoid),
    Circle(Circle),
    Text(OText),
    Placement(Placement),
}
}

converter

#![allow(unused)]
fn main() {
converter::gdsii_to_oasis(gds: &GDSIIFile) -> Result<OASISFile, _>
converter::oasis_to_gdsii(oasis: &OASISFile) -> Result<GDSIIFile, _>
}

geometry

#![allow(unused)]
fn main() {
// Bounding box
bounding_box(points: &[(f64, f64)]) -> Option<BoundingBox>
bounding_box_i32(points: &[(i32, i32)]) -> Option<BoundingBox>
gds_element_bounding_box(element: &GDSElement) -> Option<BoundingBox>
structure_bounding_box(structure: &GDSStructure) -> Option<BoundingBox>
library_bounding_box(library: &GDSIIFile) -> Option<BoundingBox>
oasis_element_bounding_box(element: &OASISElement) -> Option<BoundingBox>

// BoundingBox methods
bb.width() / bb.height() / bb.area() / bb.center()
bb.union(other) / bb.intersect(other)
bb.contains_point(x, y) / bb.expand(margin)

// Polygon metrics
polygon_area(points) -> f64
polygon_signed_area(points) -> f64
polygon_perimeter(points) -> f64
polygon_centroid(points) -> (f64, f64)

// Point queries
point_in_polygon(pt, polygon) -> bool
point_in_any_polygon(pt, polygons) -> bool
inside(points, polygons) -> Vec<bool>

// Transforms
translate(points, dx, dy) -> Vec<(f64, f64)>
rotate(points, angle, cx, cy) -> Vec<(f64, f64)>
scale(points, sx, sy, cx, cy) -> Vec<(f64, f64)>
mirror_x(points, axis_y) -> Vec<(f64, f64)>
mirror_y(points, axis_x) -> Vec<(f64, f64)>
affine_transform(points, matrix) -> Vec<(f64, f64)>

// Utilities
is_counter_clockwise(points) -> bool
ensure_counter_clockwise(points) -> Vec<(f64, f64)>
ensure_clockwise(points) -> Vec<(f64, f64)>
close_polygon(points) -> Vec<(f64, f64)>
remove_duplicates(points) -> Vec<(f64, f64)>
distance(a, b) -> f64

// Advanced
fillet(points, radius, points_per_arc) -> Vec<(f64, f64)>
fracture_to_rectangles(points) -> Vec<Vec<(f64, f64)>>
}

boolean_ops

#![allow(unused)]
fn main() {
// High-level boolean on sets of polygons
boolean(a, b, op: BooleanOp) -> Vec<Vec<(f64, f64)>>
// BooleanOp: Or | And | Not | Xor

// Offset (expand/shrink)
offset(polygons, distance: f64, tolerance: f64) -> Vec<Vec<(f64, f64)>>

// Slice along axis
slice(polygons, position: f64, axis: Axis) -> (Vec<Vec<(f64, f64)>>, Vec<Vec<(f64, f64)>>)
// Axis: X | Y

// Convex hull
convex_hull(points: &[(f64, f64)]) -> Vec<(f64, f64)>
}

flexpath

#![allow(unused)]
fn main() {
// FlexPath — stroke-style path converted to polygon
let mut path = FlexPath::new(start, width, layer, datatype);
path.segment(end, width, offset, relative);
path.arc(radius, initial_angle, final_angle, width);
path.bezier(ctrl1, ctrl2, end, width, steps);

path.end_caps = (EndCap::Flush, EndCap::Round);
path.join = Join::Miter;

path.to_polygon() -> Option<Vec<(f64, f64)>>
path.bounding_box() -> Option<BoundingBox>
path.length() -> f64

// EndCap: Flush | HalfWidth | Extended(f64) | Round
// Join:   Natural | Miter | Bevel | Round
}

curve

#![allow(unused)]
fn main() {
// Curve builder
let mut c = Curve::new(start);
c.line(end, relative);
c.arc(radius, initial_angle, final_angle);
c.bezier2(ctrl, end);          // quadratic
c.bezier3(ctrl1, ctrl2, end);  // cubic
c.smooth_bezier(ctrl2, end);   // smooth cubic
c.ellipse_arc(rx, ry, angle, large_arc, sweep, end);
c.interpolate(points, tension);
c.close();
c.get_points() -> &[(f64, f64)]

// Standalone primitives
ellipse(center, rx, ry, angle, tolerance) -> Vec<(f64, f64)>
regular_polygon(center, radius, sides, angle) -> Vec<(f64, f64)>
rounded_rectangle(corner, width, height, radius, tolerance) -> Vec<(f64, f64)>
star(center, r_inner, r_outer, points, angle) -> Vec<(f64, f64)>
spiral(center, r_start, r_end, turns, tolerance) -> Vec<(f64, f64)>
}

topology

#![allow(unused)]
fn main() {
top_level_cells(library) -> Vec<&GDSStructure>
direct_references(structure) -> Vec<String>
cell_dependencies(name, library) -> HashSet<String>
dependency_order(library) -> Vec<usize>
detect_cycles(library) -> Vec<Vec<String>>
validate_hierarchy(library) -> Result<(), Vec<String>>

flatten_structure(name, library, max_depth) -> Vec<GDSElement>
merge_library(target, source) -> usize           // skip duplicates
merge_library_overwrite(target, source) -> usize // overwrite duplicates

filter_by_layer(structure, layer) -> Vec<&GDSElement>
element_layer(element) -> Option<i16>
layers_in_structure(structure) -> Vec<i16>
layers_in_library(library) -> Vec<i16>
total_element_count(library) -> usize
}

streaming

#![allow(unused)]
fn main() {
let mut reader = StreamingGDSIIReader::new(file);
reader.read_with_callback(&mut my_callback)?;

// Implement the callback trait:
trait GDSIIStreamCallback {
    fn on_structure_start(&mut self, name: &str);
    fn on_element(&mut self, element: &GDSElement);
    fn on_structure_end(&mut self);
}

// Built-in collectors:
StructureNameCollector   // collects all cell names
StatisticsCollector      // counts elements per structure
}

Error Handling

All I/O operations return Result<T, Box<dyn std::error::Error>>:

#![allow(unused)]
fn main() {
match GDSIIFile::read_from_file("design.gds") {
    Ok(gds)  => println!("{} structures", gds.structures.len()),
    Err(e)   => eprintln!("Error: {}", e),
}
}

Technical Details

Deep dive into the technical implementation of LayKit.

Binary Format Specifications

GDSII Binary Format

The GDSII Stream Format uses a record-based binary structure:

Record Structure

+----------------+----------------+----------------+----------------+
|  Record Length (2 bytes)        |  Record Type   |   Data Type    |
+----------------+----------------+----------------+----------------+
|                        Record Data (variable)                     |
+------------------------------------------------------------------+

Fields:

  • Length (2 bytes, big-endian): Total record size including header
  • Record Type (1 byte): Identifies the record (e.g., BOUNDARY, PATH)
  • Data Type (1 byte): Data format (integer, real, string, etc.)
  • Data (variable): Actual data content

Important Record Types

Record TypeHexDescription
HEADER0x00File version
BGNLIB0x01Begin library
LIBNAME0x02Library name
UNITS0x03User and database units
ENDLIB0x04End library
BGNSTR0x05Begin structure
STRNAME0x06Structure name
ENDSTR0x07End structure
BOUNDARY0x08Polygon
PATH0x09Wire/trace
SREF0x0AStructure reference
AREF0x0BArray reference
TEXT0x0CText label
LAYER0x0DLayer number
DATATYPE0x0EDatatype number
XY0x10Coordinates

Data Types

Data TypeValueDescription
NO_DATA0x00No data
BIT_ARRAY0x01Bit array
INT20x022-byte signed integer
INT40x034-byte signed integer
REAL80x058-byte real (custom format)
ASCII0x06ASCII string

GDSII Real8 Format

Custom 8-byte floating point format:

 Bit:  63    62-56         55-0
      +----+--------+------------------+
      | S  |  Exp   |     Mantissa     |
      +----+--------+------------------+
       Sign 7 bits      56 bits

Formula:

value = (-1)^S × mantissa × 16^(exponent - 64)

Example Implementation:

#![allow(unused)]
fn main() {
fn decode_real8(bytes: [u8; 8]) -> f64 {
    let sign = if bytes[0] & 0x80 != 0 { -1.0 } else { 1.0 };
    let exponent = (bytes[0] & 0x7F) as i32 - 64;
    
    let mut mantissa = 0u64;
    for i in 1..8 {
        mantissa = (mantissa << 8) | bytes[i] as u64;
    }
    
    let mantissa_f = mantissa as f64 / (1u64 << 56) as f64;
    sign * mantissa_f * 16.0_f64.powi(exponent)
}
}

OASIS Binary Format

OASIS uses a more compact, modern binary format:

File Structure

Magic String: "%SEMI-OASIS\r\n" (13 bytes)
START Record
  - Version
  - Unit
  - Offset table flag
Name Tables
  - CELLNAME records
  - TEXTSTRING records
  - PROPNAME records
Cell Records
  - CELL record (begin)
  - Element records
  - CELL record (end)
END Record
  - Validation information
  - Padding table

Variable-Length Integer Encoding

Unsigned integers (0-127 in single byte):

Value < 128:     0xxxxxxx
Value >= 128:    1xxxxxxx 0yyyyyyy
Value >= 16384:  1xxxxxxx 1yyyyyyy 0zzzzzzz ...

Decoding algorithm:

#![allow(unused)]
fn main() {
fn read_unsigned_integer<R: Read>(reader: &mut R) -> Result<u64, Error> {
    let mut value = 0u64;
    let mut shift = 0;
    
    loop {
        let byte = read_byte(reader)?;
        value |= ((byte & 0x7F) as u64) << shift;
        
        if byte & 0x80 == 0 {
            break;
        }
        shift += 7;
    }
    
    Ok(value)
}
}

Zigzag Encoding (Signed Integers)

Maps signed integers to unsigned:

 0 →  0
-1 →  1
 1 →  2
-2 →  3
 2 →  4
-3 →  5

Formula:

#![allow(unused)]
fn main() {
fn encode_signed(n: i64) -> u64 {
    ((n << 1) ^ (n >> 63)) as u64
}

fn decode_signed(n: u64) -> i64 {
    ((n >> 1) as i64) ^ (-((n & 1) as i64))
}
}

Real Number Encoding

OASIS supports 8 types of real numbers (0-7):

TypeDescriptionExample
0Positive integer5
1Negative integer-5
2Positive reciprocal1/5
3Negative reciprocal-1/5
4Positive ratio3/5
5Negative ratio-3/5
6Float32 (IEEE 754)-
7Float64 (IEEE 754)-

LayKit primarily uses type 7 (Float64) for maximum precision.

Memory Management

GDSII File in Memory

Typical memory layout:

#![allow(unused)]
fn main() {
GDSIIFile (~100 bytes overhead)
├── version: i16                    (2 bytes)
├── library_name: String            (~50 bytes + string length)
├── units: (f64, f64)              (16 bytes)
└── structures: Vec<GDSStructure>  (24 bytes + contents)
    └── For each structure:
        ├── name: String           (~50 bytes + length)
        ├── times: 2 × GDSTime    (24 bytes)
        └── elements: Vec          (24 bytes + N × element_size)
}

Element sizes:

  • Boundary: ~80 bytes + (N vertices × 8 bytes)
  • Path: ~80 bytes + (N points × 8 bytes)
  • Text: ~100 bytes + string length
  • StructRef: ~100 bytes
  • ArrayRef: ~120 bytes

Example calculation:

File with:
- 100 structures
- Avg 1000 elements per structure
- Avg 5 vertices per boundary

Memory ≈ 100 + (100 × 200) + (100,000 × 120) + (500,000 × 8)
       ≈ 16 MB

OASIS File in Memory

Generally more compact:

#![allow(unused)]
fn main() {
OASISFile (~100 bytes overhead)
├── version: String                (~50 bytes)
├── unit: f64                      (8 bytes)
├── names: NameTable              (~200 bytes + strings)
│   ├── cell_names: HashMap        (varies)
│   ├── text_strings: HashMap      (varies)
│   └── prop_names: HashMap        (varies)
└── cells: Vec<OASISCell>         (24 bytes + contents)
}

Performance Characteristics

Read Performance

GDSII:

  • O(n) where n = file size
  • ~20-30 MB/s on modern hardware
  • Bottleneck: System calls, big-endian conversion

OASIS:

  • O(n) where n = file size
  • ~15-25 MB/s on modern hardware
  • Bottleneck: Variable-length integer decoding

Write Performance

GDSII:

  • O(n) where n = total elements
  • ~25-35 MB/s on modern hardware
  • Buffered I/O helps significantly

OASIS:

  • O(n) where n = total elements
  • ~20-30 MB/s on modern hardware
  • Variable-length encoding overhead

Memory Usage

File SizeMemory UsageNotes
1 MB~50 MBIncludes data structures
10 MB~300 MB30× expansion typical
100 MB~2.5 GBMay need 64-bit system
1 GB~20 GBConsider streaming

Coordinate Systems

GDSII Coordinates

  • Type: 32-bit signed integer (i32)
  • Range: -2,147,483,648 to 2,147,483,647
  • Units: Defined by database unit in file
  • Typical: 1nm per database unit

Example:

#![allow(unused)]
fn main() {
// Units: (1e-6, 1e-9) = 1µm user, 1nm database
// Coordinate 1000 = 1000 database units = 1µm = 0.001mm
}

OASIS Coordinates

  • Type: 64-bit signed integer (i64)
  • Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
  • Units: Defined by unit field (in meters)
  • Typical: 1nm (1e-9 meters)

Delta Encoding: OASIS often uses relative coordinates for compactness:

#![allow(unused)]
fn main() {
// Absolute: (0,0), (1000,0), (1000,1000), (0,1000)
// Delta:    (0,0), (1000,0), (0,1000), (-1000,0)
// Saves space in compressed format
}

Error Handling

LayKit uses Rust's Result type throughout:

#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
}

Common error scenarios:

  • File not found → io::Error
  • Invalid format → Custom error message
  • Corrupted data → Parse error
  • Out of memory → Allocation failure

Thread Safety

Current implementation:

  • ✅ Read-only operations are thread-safe
  • ✅ Multiple readers can work simultaneously
  • ⚠️ Writes require exclusive access
  • ⚠️ No built-in synchronization

Usage pattern:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

let gds = Arc::new(GDSIIFile::read_from_file("design.gds")?);

let handles: Vec<_> = (0..4).map(|i| {
    let gds_clone = Arc::clone(&gds);
    thread::spawn(move || {
        // Read-only analysis in parallel
        analyze_structures(&gds_clone, i);
    })
}).collect();

for handle in handles {
    handle.join().unwrap();
}
}

Future Optimizations

Potential improvements:

  1. Memory-mapped I/O - For very large files
  2. SIMD operations - For coordinate transformations
  3. Parallel parsing - Using rayon
  4. Streaming API - Process files without loading entirely
  5. Zero-copy parsing - Reduce allocations
  6. Custom allocator - Arena allocation for elements

Performance Guide

Tips and best practices for optimal performance with LayKit.

Benchmarks

Performance measurements on standard hardware:

Test System:

  • CPU: Intel Core i7 / AMD Ryzen 7
  • RAM: 16GB DDR4
  • Storage: NVMe SSD
  • OS: Linux/WSL2

Read Performance

OperationFile SizeTimeThroughput
GDSII Read1 MB~50 ms~20 MB/s
GDSII Read10 MB~400 ms~25 MB/s
GDSII Read100 MB~4 sec~25 MB/s
OASIS Read1 MB~60 ms~17 MB/s
OASIS Read10 MB~500 ms~20 MB/s
OASIS Read100 MB~5 sec~20 MB/s

Write Performance

OperationFile SizeTimeThroughput
GDSII Write1 MB~40 ms~25 MB/s
GDSII Write10 MB~350 ms~29 MB/s
GDSII Write100 MB~3.5 sec~29 MB/s
OASIS Write1 MB~50 ms~20 MB/s
OASIS Write10 MB~450 ms~22 MB/s
OASIS Write100 MB~4.5 sec~22 MB/s

Conversion Performance

ConversionInput SizeTimeRate
GDSII → OASIS10 MB~600 ms~17 MB/s
OASIS → GDSII10 MB~550 ms~18 MB/s

Memory Usage

File Size vs Memory

Typical memory usage patterns:

Memory ≈ File_Size × 20-50 (depending on complexity)

Examples:

File SizeElementsMemory UsageNotes
1 MB1K~40 MBSimple design
10 MB50K~300 MBModerate complexity
100 MB500K~2.5 GBComplex design
1 GB5M~20 GBVery large design

Reducing Memory Usage

1. Process in Chunks

#![allow(unused)]
fn main() {
// Instead of loading entire file
let gds = GDSIIFile::read_from_file("huge.gds")?;

// Consider processing structure by structure
// (requires custom implementation)
}

2. Drop Unused Data

#![allow(unused)]
fn main() {
let mut gds = GDSIIFile::read_from_file("design.gds")?;

// Remove unused structures
gds.structures.retain(|s| needed_structures.contains(&s.name));

// Shrink to fit
gds.structures.shrink_to_fit();
}

3. Use OASIS for Large Files

#![allow(unused)]
fn main() {
// OASIS files are typically 2-5× smaller
let gds = GDSIIFile::read_from_file("large.gds")?;
let oasis = converter::gdsii_to_oasis(&gds)?;
oasis.write_to_file("large.oas")?;  // Much smaller file
}

Optimization Tips

1. Use Release Mode

Always use --release for production:

# Debug mode (slow, ~10-20× slower)
cargo run --example basic_usage

# Release mode (fast, optimized)
cargo run --release --example basic_usage

Performance difference:

  • Debug: ~2 MB/s
  • Release: ~25 MB/s

2. Batch Operations

Process multiple files together:

#![allow(unused)]
fn main() {
use rayon::prelude::*;

fn batch_convert(files: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
    files.par_iter().try_for_each(|file| {
        let gds = GDSIIFile::read_from_file(file)?;
        let oasis = converter::gdsii_to_oasis(&gds)?;
        let output = file.replace(".gds", ".oas");
        oasis.write_to_file(&output)?;
        Ok(())
    })
}
}

3. Reuse Allocations

When processing multiple files:

#![allow(unused)]
fn main() {
// Less efficient: Create new for each file
for file in files {
    let gds = GDSIIFile::read_from_file(file)?;
    process(&gds);
}

// More efficient: Reuse buffer
let mut buffer = Vec::new();
for file in files {
    buffer.clear();
    let mut file = File::open(file)?;
    file.read_to_end(&mut buffer)?;
    // Parse from buffer
}
}

4. Filter Early

Remove unnecessary data as early as possible:

#![allow(unused)]
fn main() {
let mut gds = GDSIIFile::read_from_file("design.gds")?;

// Filter out unwanted layers immediately
for structure in &mut gds.structures {
    structure.elements.retain(|elem| {
        match elem {
            GDSElement::Boundary(b) => b.layer <= 10,
            GDSElement::Path(p) => p.layer <= 10,
            _ => true,
        }
    });
}
}

5. Use Buffered I/O

For custom reading/writing:

#![allow(unused)]
fn main() {
use std::io::{BufReader, BufWriter};
use std::fs::File;

// Buffered reading (faster)
let file = File::open("design.gds")?;
let mut reader = BufReader::with_capacity(1024 * 1024, file); // 1MB buffer
let gds = GDSIIFile::read(&mut reader)?;

// Buffered writing (faster)
let file = File::create("output.gds")?;
let mut writer = BufWriter::with_capacity(1024 * 1024, file); // 1MB buffer
gds.write(&mut writer)?;
}

Scalability Guidelines

Small Files (< 10 MB)

No special handling needed

  • Load entire file into memory
  • Process as needed
  • Fast operations
#![allow(unused)]
fn main() {
let gds = GDSIIFile::read_from_file("small.gds")?;
// Process freely
}

Medium Files (10-100 MB)

Generally works well

  • Monitor memory usage
  • Consider filtering unused data
  • Use release builds
#![allow(unused)]
fn main() {
let mut gds = GDSIIFile::read_from_file("medium.gds")?;
gds.structures.retain(|s| interesting_cells.contains(&s.name));
}

Large Files (100 MB - 1 GB)

⚠️ Requires care

  • Ensure sufficient RAM (>4GB)
  • Filter aggressively
  • Consider OASIS format
  • Monitor system resources
#![allow(unused)]
fn main() {
// Convert to OASIS first for smaller size
let gds = GDSIIFile::read_from_file("large.gds")?;
let oasis = converter::gdsii_to_oasis(&gds)?;
oasis.write_to_file("large.oas")?;
drop(gds); // Free memory

// Work with smaller OASIS file
let oasis = OASISFile::read_from_file("large.oas")?;
}

Very Large Files (> 1 GB)

Current limitations

  • May exhaust memory
  • Consider splitting files
  • Future: Streaming API needed

Workarounds:

#![allow(unused)]
fn main() {
// 1. Split file by structures (external tool)
// 2. Process each part separately
// 3. Merge results
}

Profiling

Use Rust profiling tools to identify bottlenecks:

Using Cargo Flamegraph

# Install
cargo install flamegraph

# Profile your program
cargo flamegraph --example basic_usage

# Open flamegraph.svg in browser

Using perf (Linux)

# Build with symbols
cargo build --release

# Profile
perf record --call-graph dwarf ./target/release/examples/basic_usage

# Analyze
perf report

Memory Profiling

# Install valgrind
sudo apt install valgrind

# Profile memory
valgrind --tool=massif ./target/release/examples/basic_usage

# Visualize
ms_print massif.out.*

Real-World Performance

Example: Converting 100 Files

#![allow(unused)]
fn main() {
use rayon::prelude::*;
use std::time::Instant;

fn batch_convert_parallel(files: &[&str]) {
    let start = Instant::now();
    
    let results: Vec<_> = files.par_iter()
        .map(|file| {
            let gds = GDSIIFile::read_from_file(file)?;
            let oasis = converter::gdsii_to_oasis(&gds)?;
            let output = file.replace(".gds", ".oas");
            oasis.write_to_file(&output)?;
            Ok::<_, Box<dyn std::error::Error>>(())
        })
        .collect();
    
    let duration = start.elapsed();
    let success = results.iter().filter(|r| r.is_ok()).count();
    
    println!("Converted {}/{} files in {:?}", 
        success, files.len(), duration);
    println!("Average: {:.2} files/sec", 
        files.len() as f64 / duration.as_secs_f64());
}
}

Typical results:

  • Sequential: ~2-3 files/sec
  • Parallel (4 cores): ~8-10 files/sec
  • Parallel (8 cores): ~12-15 files/sec

Performance Checklist

Before deploying:

  • ✅ Use --release builds
  • ✅ Profile with representative data
  • ✅ Test with your largest expected files
  • ✅ Monitor memory usage
  • ✅ Consider parallel processing
  • ✅ Use appropriate buffer sizes
  • ✅ Filter unnecessary data early
  • ✅ Use OASIS for large designs
  • ✅ Have sufficient RAM (2× file size minimum)
  • ✅ Use SSD for large files

When to Optimize

"Premature optimization is the root of all evil" - Donald Knuth

Optimize when:

  1. ✅ You have performance problems
  2. ✅ You've profiled and found bottlenecks
  3. ✅ You have specific performance requirements
  4. ✅ You're processing large files regularly

Don't optimize when:

  1. ❌ Current performance is acceptable
  2. ❌ You haven't measured anything
  3. ❌ You're just starting development
  4. ❌ It would make code much more complex

Testing

Running Tests

# Full suite
cargo test

# With printed output
cargo test -- --nocapture

# Single test
cargo test test_gdsii_round_trip

# By module
cargo test geometry
cargo test boolean_ops
cargo test flexpath
cargo test topology

# Doc tests only
cargo test --doc

Test Counts

SuiteTests
Unit tests (all modules)115
Integration tests41
Doc tests8
Total164

All 164 pass. Zero failures. Zero external dependencies.

Coverage by Module

ModuleTests
gdsii7
oasis11
converter3
geometry22
boolean_ops12
flexpath10
curve9
topology13
streaming8
aref_expansion6
properties4
format_detection6
Integration (cross-module)41
Doc tests8

Writing Tests

#![allow(unused)]
fn main() {
#[test]
fn test_my_feature() {
    let mut gds = GDSIIFile::new("TEST".to_string());
    gds.structures.push(GDSStructure {
        name: "CELL".to_string(),
        creation_time: GDSTime::now(),
        modification_time: GDSTime::now(),
        strclass: None,
        elements: Vec::new(),
    });
    assert_eq!(gds.structures.len(), 1);
}
}

For file I/O tests, clean up after yourself:

#![allow(unused)]
fn main() {
#[test]
fn test_round_trip() -> Result<(), Box<dyn std::error::Error>> {
    let path = "test_rt.gds";
    let gds = make_test_file();
    gds.write_to_file(path)?;
    let read = GDSIIFile::read_from_file(path)?;
    std::fs::remove_file(path)?;
    assert_eq!(gds.library_name, read.library_name);
    Ok(())
}
}

Contributing to LayKit

Thank you for your interest in contributing to LayKit! This guide will help you get started.

Getting Started

Prerequisites

  • Rust 1.70 or later
  • Git
  • A GitHub account
  • Familiarity with Rust and Git workflows

Setting Up Development Environment

  1. Fork the repository

    # Fork on GitHub, then clone your fork
    git clone https://github.com/YOUR_USERNAME/laykit.git
    cd laykit
    
  2. Add upstream remote

    git remote add upstream https://github.com/giridharsalana/laykit.git
    
  3. Install Rust (if needed)

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  4. Build and test

    cargo build
    cargo test
    cargo clippy
    cargo fmt --check
    

Development Workflow

1. Create a Branch

git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description

Branch naming conventions:

  • feature/ - New features
  • fix/ - Bug fixes
  • docs/ - Documentation changes
  • refactor/ - Code refactoring
  • test/ - Test additions/changes

2. Make Changes

Follow the coding standards and write tests for your changes.

3. Test Your Changes

# Run tests
cargo test

# Run tests with output
cargo test -- --nocapture

# Check formatting
cargo fmt --check

# Run clippy
cargo clippy -- -D warnings

# Build documentation
cargo doc --open

4. Commit Changes

git add .
git commit -m "feat: add new feature description"

Commit message format:

type: brief description

Longer description if needed.

Fixes #issue_number

Types:

  • feat: - New feature
  • fix: - Bug fix
  • docs: - Documentation
  • test: - Tests
  • refactor: - Code refactoring
  • perf: - Performance improvement
  • chore: - Maintenance

5. Push and Create PR

git push origin feature/your-feature-name

Then create a Pull Request on GitHub.

Code Style

Rust Style

Follow Rust standard style using rustfmt:

cargo fmt

Key points:

  • Use 4-space indentation
  • Line length: 100 characters (preferred)
  • Use trailing commas in multi-line constructs
  • Organize imports: std, external crates, internal modules

Example:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;

use laykit::{GDSIIFile, OASISFile};

pub fn process_file(path: &str) -> Result<(), Box<dyn Error>> {
    let gds = GDSIIFile::read_from_file(path)?;
    // Process...
    Ok(())
}
}

Documentation

Document all public items:

#![allow(unused)]
fn main() {
/// Reads a GDSII file from the specified path.
///
/// # Arguments
///
/// * `path` - Path to the GDSII file
///
/// # Returns
///
/// Returns a `Result` containing the `GDSIIFile` or an error.
///
/// # Examples
///
/// ```
/// use laykit::GDSIIFile;
///
/// let gds = GDSIIFile::read_from_file("design.gds")?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn read_from_file(path: &str) -> Result<Self, Box<dyn Error>> {
    // Implementation...
}
}

Error Handling

Use Result types consistently:

#![allow(unused)]
fn main() {
// Good
pub fn process() -> Result<Output, Box<dyn Error>> {
    let data = read_file()?;
    Ok(process_data(data))
}

// Avoid unwrap in library code
// Bad
pub fn process() -> Output {
    let data = read_file().unwrap(); // Don't do this!
    process_data(data)
}
}

Testing

Writing Tests

Every new feature should have tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_feature() -> Result<(), Box<dyn Error>> {
        // Setup
        let input = create_test_input();
        
        // Execute
        let result = new_feature(input)?;
        
        // Verify
        assert_eq!(result.value, expected_value);
        
        Ok(())
    }
}
}

Test Coverage

Aim for:

  • ✅ 80%+ code coverage for new features
  • ✅ Tests for all public APIs
  • ✅ Tests for error conditions
  • ✅ Integration tests for complex features

Pull Request Guidelines

Before Submitting

Checklist:

  • ✅ Code compiles without warnings
  • ✅ All tests pass
  • cargo clippy passes
  • cargo fmt applied
  • ✅ Documentation updated
  • ✅ New tests added

PR Description

Include in your PR:

## Description
Brief description of changes

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing
How has this been tested?

## Checklist
- [ ] Tests pass locally
- [ ] Code follows project style
- [ ] Documentation updated

PR Review Process

  1. Automated checks run (CI/CD)
  2. Code review by maintainers
  3. Address feedback
  4. Approval and merge

Areas for Contribution

High Priority

  • Performance optimizations - SIMD, parallel processing
  • Streaming API - For very large files
  • Additional element types - Missing GDSII/OASIS features
  • CLI tool - Command-line interface
  • More tests - Edge cases, error conditions

Good First Issues

Look for issues labeled:

  • good first issue
  • help wanted
  • documentation

Documentation

  • Improve API documentation
  • Add more examples
  • Write tutorials
  • Fix typos and clarify instructions

Testing

  • Add edge case tests
  • Improve test coverage
  • Add benchmark tests
  • Add integration tests

Code Review

What We Look For

  • Correctness - Does it work?
  • Tests - Are there adequate tests?
  • Style - Follows project conventions?
  • Documentation - Well documented?
  • Performance - No obvious performance issues?
  • Safety - No unsafe code without justification?

Responding to Feedback

  • Be open to suggestions
  • Ask questions if unclear
  • Make requested changes
  • Push updates to the same branch

Release Process

(For maintainers)

  1. Update version in Cargo.toml
  2. Create git tag: git tag v0.x.y
  3. Push tag: git push origin v0.x.y
  4. CI builds and deploys automatically

Communication

Channels

  • GitHub Issues - Bug reports, feature requests
  • GitHub Discussions - Questions, ideas, general discussion
  • Pull Requests - Code contributions

Asking Questions

Before asking:

  1. Check existing issues
  2. Read the documentation
  3. Search discussions

When asking:

  • Provide context
  • Include code examples
  • Show what you've tried
  • Be specific about the problem

License

By contributing, you agree that your contributions will be licensed under the MIT License.

Code of Conduct

Our Standards

  • Be respectful and inclusive
  • Accept constructive criticism gracefully
  • Focus on what's best for the community
  • Show empathy towards others

Unacceptable Behavior

  • Harassment or discriminatory language
  • Trolling or insulting comments
  • Publishing others' private information
  • Other unprofessional conduct

Getting Help

If you need help:

  1. Check the documentation
  2. Search existing issues
  3. Ask in discussions
  4. Open a new issue

Recognition

Contributors will be:

  • Listed in release notes
  • Credited in documentation
  • Mentioned in the project README

Thank you for contributing to LayKit! 🎉

Project Structure

Overview of the LayKit codebase organization.

Directory Structure

laykit/
├── .github/
│   └── workflows/
│       └── ci.yml              # GitHub Actions CI/CD
├── book/
│   ├── src/                    # mdBook documentation source
│   └── theme/                  # Custom CSS/JS for docs
├── examples/
│   ├── basic_usage.rs          # Basic usage example
│   ├── gdsii_only.rs           # GDSII-focused example
│   └── oasis_only.rs           # OASIS-focused example
├── src/
│   ├── lib.rs                  # Library entry point & public API
│   ├── gdsii.rs                # GDSII read/write
│   ├── oasis.rs                # OASIS read/write
│   ├── converter.rs            # Format conversion
│   ├── geometry.rs             # Geometric operations
│   ├── boolean_ops.rs          # Boolean polygon operations
│   ├── flexpath.rs             # Flexible path generation
│   ├── curve.rs                # Curve primitives
│   ├── topology.rs             # Cell hierarchy utilities
│   ├── streaming.rs            # Streaming parser
│   ├── aref_expansion.rs       # AREF expansion
│   ├── properties.rs           # Property utilities
│   ├── format_detection.rs     # Magic-byte format detection
│   └── bin/
│       └── laykit.rs           # CLI tool
├── tests/
│   ├── tests.rs                # Integration tests
│   ├── gdstk_validation.py     # Cross-validation against reference
│   └── run_all_tests.sh        # Test runner script
├── target/                     # Build artifacts (gitignored)
├── book.toml                   # mdBook configuration
├── Cargo.toml                  # Project manifest
├── Cargo.lock                  # Dependency lock file
├── LICENSE                     # MIT License
├── README.md                   # Project README
└── .gitignore                  # Git ignore rules

Source Files

lib.rs (~90 lines)

Entry point and public API exports.

Contents:

  • Module declarations
  • Public re-exports
  • Top-level documentation

Example:

#![allow(unused)]
fn main() {
//! LayKit - GDSII and OASIS library
pub mod gdsii;
pub mod oasis;
pub mod converter;

// Re-export main types
pub use gdsii::*;
pub use oasis::*;
}

gdsii.rs (~1,000 lines)

Complete GDSII format implementation.

Major components:

  • GDSIIFile - Main file structure
  • GDSStructure - Cell/structure definition
  • GDSElement - Element enum
  • Element types: Boundary, Path, Text, etc.
  • STrans - Transformation data
  • GDSTime - Timestamp handling
  • Binary I/O functions
  • Real8 encoding/decoding

Structure:

#![allow(unused)]
fn main() {
// Data structures (~300 lines)
pub struct GDSIIFile { ... }
pub struct GDSStructure { ... }
pub enum GDSElement { ... }

// Implementation (~400 lines)
impl GDSIIFile {
    pub fn read_from_file(...) { ... }
    pub fn write_to_file(...) { ... }
    pub fn read<R: Read>(...) { ... }
    pub fn write<W: Write>(...) { ... }
}

// Helper functions (~300 lines)
fn read_record(...) { ... }
fn write_record(...) { ... }
fn decode_real8(...) { ... }
fn encode_real8(...) { ... }
}

oasis.rs (~950 lines)

Complete OASIS format implementation.

Major components:

  • OASISFile - Main file structure
  • OASISCell - Cell definition
  • OASISElement - Element enum
  • Element types: Rectangle, Polygon, Path, etc.
  • NameTable - String compression
  • Repetition - Array patterns
  • Variable-length integer encoding
  • Zigzag encoding for signed integers

Structure:

#![allow(unused)]
fn main() {
// Data structures (~350 lines)
pub struct OASISFile { ... }
pub struct OASISCell { ... }
pub enum OASISElement { ... }

// Implementation (~350 lines)
impl OASISFile {
    pub fn read_from_file(...) { ... }
    pub fn write_to_file(...) { ... }
}

// Helper functions (~250 lines)
fn read_unsigned_integer(...) { ... }
fn read_signed_integer(...) { ... }
fn write_unsigned_integer(...) { ... }
fn zigzag_encode(...) { ... }
fn zigzag_decode(...) { ... }
}

converter.rs (~300 lines)

Format conversion utilities.

Functions:

  • gdsii_to_oasis() - GDSII to OASIS conversion
  • oasis_to_gdsii() - OASIS to GDSII conversion
  • is_rectangle() - Rectangle detection helper

Structure:

#![allow(unused)]
fn main() {
// Main conversion functions (~250 lines)
pub fn gdsii_to_oasis(gds: &GDSIIFile) 
    -> Result<OASISFile, Box<dyn Error>> { ... }

pub fn oasis_to_gdsii(oasis: &OASISFile) 
    -> Result<GDSIIFile, Box<dyn Error>> { ... }

// Helper functions (~50 lines)
pub fn is_rectangle(points: &[(i32, i32)]) 
    -> Option<(i32, i32, i32, i32)> { ... }
}

Examples

basic_usage.rs (~150 lines)

Demonstrates core functionality:

  • Creating GDSII files
  • Creating OASIS files
  • Format conversion
  • Reading and displaying information

gdsii_only.rs (~200 lines)

Comprehensive GDSII example:

  • All element types
  • Hierarchical design
  • Transformations
  • Properties
  • Arrays

oasis_only.rs (~150 lines)

OASIS-specific features:

  • Rectangle primitives
  • Trapezoids
  • Circles
  • Name tables
  • Repetitions

Tests

tests.rs (~600 lines)

Comprehensive test suite:

Test categories:

#![allow(unused)]
fn main() {
// GDSII tests (7 tests, ~200 lines)
#[test] fn test_gdsii_create_and_write() { ... }
#[test] fn test_gdsii_round_trip() { ... }
// ...

// OASIS tests (11 tests, ~300 lines)
#[test] fn test_oasis_create_simple() { ... }
#[test] fn test_oasis_round_trip_rectangles() { ... }
// ...

// Converter tests (3 tests, ~100 lines)
#[test] fn test_gdsii_to_oasis_conversion() { ... }
#[test] fn test_oasis_to_gdsii_conversion() { ... }
#[test] fn test_rectangle_detection() { ... }
}

Documentation

API Documentation

Generated from source code comments:

  • Module-level documentation
  • Struct/enum documentation
  • Function documentation
  • Example code

Location: target/doc/laykit/

User Guide (mdBook)

Located in book/src/:

  • Introduction and getting started
  • Format-specific guides
  • Conversion guide
  • Examples and tutorials
  • API reference
  • Technical details

Build output: book/build/

Build Artifacts

target/ Directory

target/
├── debug/              # Debug builds
│   ├── deps/          # Dependencies
│   ├── examples/      # Example binaries
│   └── incremental/   # Incremental compilation data
├── release/           # Release builds (optimized)
│   ├── deps/
│   └── examples/
└── doc/              # Generated API documentation
    └── laykit/

Configuration Files

Cargo.toml

Project manifest:

[package]
name = "laykit"
edition = "2024"
authors = ["Giridhar Salana <giridharsalana@gmail.com>"]
description = "Production-ready Rust library for reading, writing, and manipulating GDSII and OASIS IC layout files"
repository = "https://github.com/giridharsalana/laykit"
documentation = "https://giridharsalana.github.io/laykit"
homepage = "https://giridharsalana.github.io/laykit"
license = "MIT"
keywords = ["gdsii", "oasis", "ic-layout", "vlsi", "eda"]
categories = ["parser-implementations", "encoding"]

[dependencies]
# Zero dependencies!

book.toml

mdBook configuration:

[book]
title = "LayKit Documentation"
authors = ["Giridhar Salana"]
description = "Production-ready Rust library for GDSII and OASIS"
language = "en"
src = "book/src"

[build]
build-dir = "book/build"

[output.html]
default-theme = "rust"
git-repository-url = "https://github.com/giridharsalana/laykit"

.gitignore

Ignored files:

/target/              # Build artifacts
**/*.rs.bk           # Rust backups
*.gds                # Test GDSII files
*.oas                # Test OASIS files
/book/build/         # mdBook output
.vscode/             # Editor config
.DS_Store            # macOS files

Module Organization

Public API

What users interact with:

#![allow(unused)]
fn main() {
laykit
├── GDSIIFile          // Main GDSII type
├── GDSStructure       // GDSII structures
├── GDSElement         // GDSII elements
├── OASISFile          // Main OASIS type
├── OASISCell          // OASIS cells
├── OASISElement       // OASIS elements
└── converter          // Conversion functions
    ├── gdsii_to_oasis
    └── oasis_to_gdsii
}

Internal Organization

How code is structured:

src/
├── lib.rs                    # Public API surface
├── gdsii.rs                  # Self-contained module
│   ├── Data structures
│   ├── I/O implementation
│   └── Helper functions
├── oasis.rs                  # Self-contained module
│   ├── Data structures
│   ├── I/O implementation
│   └── Helper functions
└── converter.rs              # Uses both modules
    └── Conversion logic

Statistics

Code Metrics

MetricValue
Total source lines~2,950
GDSII module~1,000
OASIS module~950
Converter module~300
Tests~600
Examples~500
Documentation~5,000+

Dependencies

  • Runtime: 0 (zero!)
  • Development: Standard Rust toolchain only
  • Optional: mdBook for documentation

Build Process

Development Build

cargo build
# Output: target/debug/

Release Build

cargo build --release
# Output: target/release/

Documentation Build

# API docs
cargo doc

# User guide
mdbook build

Running Examples

cargo run --example basic_usage
cargo run --release --example gdsii_only

CI/CD Pipeline

GitHub Actions workflow:

  1. Test - Run all tests
  2. Clippy - Lint checking
  3. Format - Style checking
  4. Build - Release builds (on tags)
  5. Docs - Generate and deploy documentation
  6. Release - Create GitHub release

Adding New Features

Checklist

When adding a new feature:

  1. ✅ Add implementation in appropriate module
  2. ✅ Add public API in lib.rs
  3. ✅ Add tests in tests/tests.rs
  4. ✅ Add example in examples/
  5. ✅ Update documentation
  6. ✅ Update CHANGELOG.md
  7. ✅ Run all checks (cargo test, cargo clippy, cargo fmt)

Example: Adding a New Element Type

  1. Define structure (e.g., in gdsii.rs)
  2. Add to enum (GDSElement)
  3. Implement I/O (read/write functions)
  4. Add conversion (in converter.rs)
  5. Write tests
  6. Document

Finding Code

  • GDSII featuressrc/gdsii.rs
  • OASIS featuressrc/oasis.rs
  • Conversionssrc/converter.rs
  • Teststests/tests.rs
  • Examplesexamples/

Understanding Flow

  1. Start with examples/ to see usage
  2. Check src/lib.rs for public API
  3. Dive into implementation files
  4. Refer to tests for edge cases

Roadmap

Current Release ✅

  • ✅ GDSII read/write (all 7 element types)
  • ✅ OASIS read/write (all element types)
  • ✅ Bidirectional format conversion
  • ✅ Geometry: bounding box, transforms, area, point-in-polygon, fillet, fracture
  • ✅ Boolean operations: union, intersection, difference, XOR, slice, offset, convex hull
  • ✅ FlexPath with miter/bevel/round joins and configurable end caps
  • ✅ Curve primitives: arc, Bezier, ellipse, spline, regular polygon, star, spiral
  • ✅ Cell topology: flatten, dependency order, cycle detection, library merge
  • ✅ Streaming parser for large files
  • ✅ AREF expansion
  • ✅ Property management
  • ✅ CLI tool (convert, info, validate)
  • ✅ File format detection by magic bytes
  • ✅ 164 tests, zero external dependencies

Future

  • Python bindings via PyO3 — drop-in replacement for gdstk
  • WebAssembly — browser-based layout tools
  • SIMD acceleration — vectorised coordinate operations
  • Parallel parsing — multi-threaded structure processing via Rayon (optional feature flag)
  • Design Rule Checking — basic geometric DRC (min width, spacing, overlap)
  • Layer mapping — transform layer numbers during conversion
  • Partial file reading — read a single named cell without parsing the whole file
  • GUI viewer — simple pan/zoom layout visualisation (egui)