diff --git a/examples/release/Cargo.toml b/examples/release/Cargo.toml index 066358a1..70fce75d 100644 --- a/examples/release/Cargo.toml +++ b/examples/release/Cargo.toml @@ -3,6 +3,10 @@ resolver = "2" members = [ + "colbuf", "sky130_inverter", "spice_vdivider", -] + "substrate_api_examples", + "vdivider", + "via" +] \ No newline at end of file diff --git a/examples/release/sky130_inverter/src/tb/open.rs b/examples/release/sky130_inverter/src/tb/open.rs new file mode 100644 index 00000000..229b9d3c --- /dev/null +++ b/examples/release/sky130_inverter/src/tb/open.rs @@ -0,0 +1,390 @@ +// begin-code-snippet imports +use crate::Inverter; +use crate::InverterIoKind; +use crate::SKY130_MAGIC_TECH_FILE; +use crate::SKY130_NETGEN_SETUP_FILE; + +use magic_netgen::Pex; +use ngspice::blocks::{Pulse, Vsource}; +use ngspice::Ngspice; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal_macros::dec; +use sky130::corner::Sky130Corner; +use sky130::layout::to_gds; +use sky130::Sky130OpenSchema; +use spice::Spice; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use substrate::block::Block; +use substrate::context::Context; +use substrate::error::Result; +use substrate::schematic::{CellBuilder, ConvertSchema, Schematic}; +use substrate::simulation::waveform::{EdgeDir, TimeWaveform}; +use substrate::simulation::Pvt; +use substrate::types::schematic::{IoNodeBundle, Node}; +use substrate::types::{Signal, TestbenchIo}; +// end-code-snippet imports + +#[allow(dead_code)] +mod schematic_only_tb { + use super::*; + + // begin-code-snippet schematic-tb + // begin-code-snippet struct-and-impl + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Block)] + #[substrate(io = "TestbenchIo")] + pub struct InverterTb { + pvt: Pvt, + dut: Inverter, + } + + impl InverterTb { + #[inline] + pub fn new(pvt: Pvt, dut: Inverter) -> Self { + Self { pvt, dut } + } + } + // end-code-snippet struct-and-impl + + // begin-code-snippet schematic + impl Schematic for InverterTb { + type Schema = Ngspice; + type NestedData = Node; + fn schematic( + &self, + io: &IoNodeBundle, + cell: &mut CellBuilder<::Schema>, + ) -> Result { + let inv = cell + .sub_builder::() + .instantiate(ConvertSchema::new(self.dut)); + + let vdd = cell.signal("vdd", Signal); + let dout = cell.signal("dout", Signal); + let vddsrc = cell.instantiate(Vsource::dc(self.pvt.voltage)); + cell.connect(vddsrc.io().p, vdd); + cell.connect(vddsrc.io().n, io.vss); + + let vin = cell.instantiate(Vsource::pulse(Pulse { + val0: 0.into(), + val1: self.pvt.voltage, + delay: Some(dec!(0.1e-9)), + width: Some(dec!(1e-9)), + fall: Some(dec!(1e-12)), + rise: Some(dec!(1e-12)), + period: None, + num_pulses: Some(dec!(1)), + })); + cell.connect(inv.io().din, vin.io().p); + cell.connect(vin.io().n, io.vss); + + cell.connect(inv.io().vdd, vdd); + cell.connect(inv.io().vss, io.vss); + cell.connect(inv.io().dout, dout); + + Ok(dout) + } + } + // end-code-snippet schematic + // end-code-snippet schematic-tb + + // begin-code-snippet schematic-design-script + /// Designs an inverter for balanced pull-up and pull-down times. + /// + /// The NMOS width is kept constant; the PMOS width is swept over + /// the given range. + pub struct InverterDesign { + /// The fixed NMOS width. + pub nw: i64, + /// The set of PMOS widths to sweep. + pub pw: Vec, + } + + impl InverterDesign { + pub fn run(&self, ctx: &mut Context, work_dir: impl AsRef) -> Inverter { + let work_dir = work_dir.as_ref(); + let pvt = Pvt::new(Sky130Corner::Tt, dec!(1.8), dec!(25)); + + let mut opt = None; + for pw in self.pw.iter().copied() { + let dut = Inverter { nw: self.nw, pw }; + let tb = InverterTb::new(pvt, dut); + let sim_dir = work_dir.join(format!("pw{pw}")); + let sim = ctx + .get_sim_controller(tb, sim_dir) + .expect("failed to create sim controller"); + let mut opts = ngspice::Options::default(); + sim.set_option(pvt.corner, &mut opts); + let output = sim + .simulate( + opts, + ngspice::tran::Tran { + stop: dec!(2e-9), + step: dec!(1e-11), + ..Default::default() + }, + ) + .expect("failed to run simulation"); + + let vout = output.as_ref(); + let mut trans = vout.transitions( + 0.2 * pvt.voltage.to_f64().unwrap(), + 0.8 * pvt.voltage.to_f64().unwrap(), + ); + // The input waveform has a low -> high, then a high -> low transition. + // So the first transition of the inverter output is high -> low. + // The duration of this transition is the inverter fall time. + let falling_transition = trans.next().unwrap(); + assert_eq!(falling_transition.dir(), EdgeDir::Falling); + let tf = falling_transition.duration(); + let rising_transition = trans.next().unwrap(); + assert_eq!(rising_transition.dir(), EdgeDir::Rising); + let tr = rising_transition.duration(); + + println!("Simulating with pw = {pw} gave tf = {}, tr = {}", tf, tr); + let diff = (tr - tf).abs(); + if let Some((pdiff, _)) = opt { + if diff < pdiff { + opt = Some((diff, dut)); + } + } else { + opt = Some((diff, dut)); + } + } + + opt.unwrap().1 + } + } + // end-code-snippet schematic-design-script + + // begin-code-snippet schematic-tests + #[cfg(test)] + mod tests { + use crate::sky130_open_ctx; + + use super::*; + + #[test] + pub fn design_inverter_open() { + let work_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/design_inverter_open"); + let mut ctx = sky130_open_ctx(); + let script = InverterDesign { + nw: 1_200, + pw: (3_000..=5_000).step_by(400).collect(), + }; + let inv = script.run(&mut ctx, work_dir); + println!("Designed inverter:\n{:#?}", inv); + } + } + // end-code-snippet schematic-tests +} + +// begin-code-snippet pex-tb +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum InverterDut { + Schematic(Inverter), + Extracted(Pex, Spice>>), +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Block)] +#[substrate(io = "TestbenchIo")] +pub struct InverterTb { + pvt: Pvt, + dut: InverterDut, +} + +impl InverterTb { + #[inline] + pub fn new(pvt: Pvt, dut: impl Into) -> Self { + Self { + pvt, + dut: dut.into(), + } + } +} + +impl Schematic for InverterTb { + type Schema = Ngspice; + type NestedData = Node; + fn schematic( + &self, + io: &IoNodeBundle, + cell: &mut CellBuilder<::Schema>, + ) -> Result { + let invio = cell.signal( + "dut", + InverterIoKind { + vdd: Signal, + vss: Signal, + din: Signal, + dout: Signal, + }, + ); + + match self.dut.clone() { + InverterDut::Schematic(inv) => { + cell.sub_builder::() + .instantiate_connected_named(ConvertSchema::new(inv), &invio, "inverter"); + } + InverterDut::Extracted(inv) => { + cell.sub_builder::() + .instantiate_connected_named(inv, &invio, "inverter"); + } + }; + + let vdd = cell.signal("vdd", Signal); + let dout = cell.signal("dout", Signal); + let vddsrc = cell.instantiate(Vsource::dc(self.pvt.voltage)); + cell.connect(vddsrc.io().p, vdd); + cell.connect(vddsrc.io().n, io.vss); + + let vin = cell.instantiate(Vsource::pulse(Pulse { + val0: 0.into(), + val1: self.pvt.voltage, + delay: Some(dec!(0.1e-9)), + width: Some(dec!(1e-9)), + fall: Some(dec!(1e-12)), + rise: Some(dec!(1e-12)), + period: None, + num_pulses: Some(dec!(1)), + })); + cell.connect(invio.din, vin.io().p); + cell.connect(vin.io().n, io.vss); + + cell.connect(invio.vdd, vdd); + cell.connect(invio.vss, io.vss); + cell.connect(invio.dout, dout); + + Ok(dout) + } +} +// end-code-snippet pex-tb + +// begin-code-snippet design-extracted +/// Designs an inverter for balanced pull-up and pull-down times. +/// +/// The NMOS width is kept constant; the PMOS width is swept over +/// the given range. +pub struct InverterDesign { + /// The fixed NMOS width. + pub nw: i64, + /// The set of PMOS widths to sweep. + pub pw: Vec, + /// Whether or not to run extracted simulations. + pub extracted: bool, +} + +impl InverterDesign { + pub fn run(&self, ctx: &mut Context, work_dir: impl AsRef) -> Inverter { + let work_dir = work_dir.as_ref(); + let pvt = Pvt::new(Sky130Corner::Tt, dec!(1.8), dec!(25)); + + let mut opt = None; + for pw in self.pw.iter().copied() { + let dut = Inverter { nw: self.nw, pw }; + let inverter = if self.extracted { + let work_dir = work_dir.join(format!("pw{pw}")); + let layout_path = work_dir.join("layout.gds"); + ctx.write_layout(dut, to_gds, &layout_path) + .expect("failed to write layout"); + InverterDut::Extracted(Pex { + schematic: Arc::new(ConvertSchema::new(ConvertSchema::new(dut))), + gds_path: work_dir.join("layout.gds"), + layout_cell_name: dut.name(), + work_dir, + magic_tech_file_path: PathBuf::from(SKY130_MAGIC_TECH_FILE), + netgen_setup_file_path: PathBuf::from(SKY130_NETGEN_SETUP_FILE), + }) + } else { + InverterDut::Schematic(dut) + }; + let tb = InverterTb::new(pvt, inverter); + let sim_dir = work_dir.join(format!("pw{pw}")); + let sim = ctx + .get_sim_controller(tb, sim_dir) + .expect("failed to create sim controller"); + let mut opts = ngspice::Options::default(); + sim.set_option(pvt.corner, &mut opts); + let output = sim + .simulate( + opts, + ngspice::tran::Tran { + stop: dec!(2e-9), + step: dec!(1e-11), + ..Default::default() + }, + ) + .expect("failed to run simulation"); + + let vout = output.as_ref(); + let mut trans = vout.transitions( + 0.2 * pvt.voltage.to_f64().unwrap(), + 0.8 * pvt.voltage.to_f64().unwrap(), + ); + // The input waveform has a low -> high, then a high -> low transition. + // So the first transition of the inverter output is high -> low. + // The duration of this transition is the inverter fall time. + let falling_transition = trans.next().unwrap(); + assert_eq!(falling_transition.dir(), EdgeDir::Falling); + let tf = falling_transition.duration(); + let rising_transition = trans.next().unwrap(); + assert_eq!(rising_transition.dir(), EdgeDir::Rising); + let tr = rising_transition.duration(); + + println!("Simulating with pw = {pw} gave tf = {}, tr = {}", tf, tr); + let diff = (tr - tf).abs(); + if let Some((pdiff, _)) = opt { + if diff < pdiff { + opt = Some((diff, dut)); + } + } else { + opt = Some((diff, dut)); + } + } + + opt.unwrap().1 + } +} +// end-code-snippet design-extracted + +// begin-code-snippet tests-extracted +#[cfg(test)] +mod tests { + use crate::sky130_open_ctx; + + use super::*; + + #[test] + pub fn design_inverter_extracted_open() { + let work_dir = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/design_inverter_extracted_open" + ); + let mut ctx = sky130_open_ctx(); + let script = InverterDesign { + nw: 1_200, + pw: (3_000..=5_000).step_by(400).collect(), + extracted: true, + }; + let inv = script.run(&mut ctx, work_dir); + println!("Designed inverter:\n{:#?}", inv); + } + + #[test] + pub fn design_inverter_schematic_open() { + let work_dir = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/design_inverter_schematic_open" + ); + let mut ctx = sky130_open_ctx(); + let script = InverterDesign { + nw: 1_200, + pw: (3_000..=5_000).step_by(400).collect(), + extracted: false, + }; + let inv = script.run(&mut ctx, work_dir); + println!("Designed inverter:\n{:#?}", inv); + } +} +// end-code-snippet tests-extracted diff --git a/examples/release/vdivider/Cargo.toml b/examples/release/vdivider/Cargo.toml new file mode 100644 index 00000000..66d8a9f5 --- /dev/null +++ b/examples/release/vdivider/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "vdivider" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +arcstr = { version = "1", features = ["serde"] } +rust_decimal = "1" +rust_decimal_macros = "1" +serde = { version = "1.0.217", features = ["derive"] } +substrate = { version = "0.9.0", registry = "substrate" } +spectre = { version = "0.10.0", registry = "substrate" } + +[dev-dependencies] +approx = "0.5" diff --git a/examples/release/vdivider/src/lib.rs b/examples/release/vdivider/src/lib.rs new file mode 100644 index 00000000..8ac36b00 --- /dev/null +++ b/examples/release/vdivider/src/lib.rs @@ -0,0 +1,213 @@ +use arcstr::ArcStr; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use serde::{Deserialize, Serialize}; +use spectre::blocks::{Iprobe, Resistor, Vsource}; +use spectre::Spectre; +use substrate::block::Block; +use substrate::schematic::{CellBuilder, Instance, NestedData, Schematic}; +use substrate::types::{Array, InOut, Io, Output, PowerIo, Signal, TestbenchIo}; + +#[derive(Debug, Default, Clone, Io)] +pub struct VdividerIo { + pub pwr: PowerIo, + pub out: Output, +} + +#[derive(Debug, Default, Clone, Io)] +pub struct VdividerFlatIo { + pub vdd: InOut, + pub vss: InOut, + pub out: Output, +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct Vdivider { + pub r1: Resistor, + pub r2: Resistor, +} + +impl Vdivider { + #[inline] + pub fn new(r1: impl Into, r2: impl Into) -> Self { + Self { + r1: Resistor::new(r1), + r2: Resistor::new(r2), + } + } +} + +#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct VdividerArray { + pub vdividers: Vec, +} + +impl Block for Vdivider { + type Io = VdividerIo; + + fn name(&self) -> ArcStr { + arcstr::format!("vdivider_{}_{}", self.r1.value(), self.r2.value()) + } + + fn io(&self) -> Self::Io { + Default::default() + } +} + +#[derive(Debug, Clone, Io)] +pub struct VdividerArrayIo { + pub elements: Array, +} + +impl Block for VdividerArray { + type Io = VdividerArrayIo; + + fn name(&self) -> ArcStr { + arcstr::format!("vdivider_array_{}", self.vdividers.len()) + } + + fn io(&self) -> Self::Io { + VdividerArrayIo { + elements: Array::new(self.vdividers.len(), Default::default()), + } + } +} + +#[derive(NestedData)] +pub struct VdividerData { + r1: Instance, + r2: Instance, +} + +impl Schematic for Vdivider { + type Schema = Spectre; + type NestedData = VdividerData; + + fn schematic( + &self, + io: &substrate::types::schematic::IoNodeBundle, + cell: &mut CellBuilder<::Schema>, + ) -> substrate::error::Result { + let r1 = cell.instantiate(self.r1); + let r2 = cell.instantiate(self.r2); + + cell.connect(io.pwr.vdd, r1.io().p); + cell.connect(io.out, r1.io().n); + cell.connect(io.out, r2.io().p); + cell.connect(io.pwr.vss, r2.io().n); + Ok(VdividerData { r1, r2 }) + } +} + +impl Schematic for VdividerArray { + type Schema = Spectre; + type NestedData = Vec>; + + fn schematic( + &self, + io: &substrate::types::schematic::IoNodeBundle, + cell: &mut CellBuilder<::Schema>, + ) -> substrate::error::Result { + let mut vdividers = Vec::new(); + + for (i, vdivider) in self.vdividers.iter().enumerate() { + let vdiv = cell.instantiate(*vdivider); + + cell.connect(&vdiv.io().pwr, &io.elements[i]); + + vdividers.push(vdiv); + } + + Ok(vdividers) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize, Block)] +#[substrate(io = "TestbenchIo")] +pub struct VdividerTb; + +#[derive(NestedData)] +pub struct VdividerTbData { + iprobe: Instance, + dut: Instance, +} + +impl Schematic for VdividerTb { + type Schema = Spectre; + type NestedData = VdividerTbData; + + fn schematic( + &self, + io: &substrate::types::schematic::IoNodeBundle, + cell: &mut CellBuilder<::Schema>, + ) -> substrate::error::Result { + let vdd_a = cell.signal("vdd_a", Signal); + let vdd = cell.signal("vdd", Signal); + let out = cell.signal("out", Signal); + let dut = cell.instantiate(Vdivider { + r1: Resistor::new(20), + r2: Resistor::new(20), + }); + + cell.connect(dut.io().pwr.vdd, vdd); + cell.connect(dut.io().pwr.vss, io.vss); + cell.connect(dut.io().out, out); + + let iprobe = cell.instantiate(Iprobe); + cell.connect(iprobe.io().p, vdd_a); + cell.connect(iprobe.io().n, vdd); + + let vsource = cell.instantiate(Vsource::dc(dec!(1.8))); + cell.connect(vsource.io().p, vdd_a); + cell.connect(vsource.io().n, io.vss); + + Ok(VdividerTbData { iprobe, dut }) + } +} + +#[cfg(test)] +mod tests { + use approx::relative_eq; + use rust_decimal_macros::dec; + use spectre::{analysis::tran::Tran, ErrPreset}; + use substrate::{context::Context, simulation::waveform::TimeWaveform}; + + use super::*; + use std::path::PathBuf; + + pub const BUILD_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/build"); + + #[inline] + pub fn get_path(test_name: &str, file_name: &str) -> PathBuf { + PathBuf::from(BUILD_DIR).join(test_name).join(file_name) + } + + #[test] + fn vdivider_tran() { + let test_name = "spectre_vdivider_tran"; + let sim_dir = get_path(test_name, "sim/"); + let ctx = Context::builder().install(Spectre::default()).build(); + let sim = ctx.get_sim_controller(VdividerTb, sim_dir).unwrap(); + let output = sim + .simulate( + Default::default(), + Tran { + stop: dec!(1e-6), + errpreset: Some(ErrPreset::Conservative), + ..Default::default() + }, + ) + .unwrap(); + + for (actual, expected) in [ + (&output.iprobe.io().p.i, 1.8 / 40.), + (&output.dut.io().pwr.vdd.v, 1.8), + (&output.dut.io().out.v, 0.9), + ] { + assert!(actual.values().all(|pt| { + let val = pt.x(); + relative_eq!(val, expected) + })); + } + } +} diff --git a/examples/release/via/Cargo.toml b/examples/release/via/Cargo.toml new file mode 100644 index 00000000..25614409 --- /dev/null +++ b/examples/release/via/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "via" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +substrate = { version = "0.9.0", registry = "substrate" } +layir = { version = "0.1.0", registry = "substrate" } diff --git a/examples/release/via/src/lib.rs b/examples/release/via/src/lib.rs new file mode 100644 index 00000000..8b9a895f --- /dev/null +++ b/examples/release/via/src/lib.rs @@ -0,0 +1,163 @@ +use layir::Shape; +use substrate::types::codegen::PortGeometryBundle; +use substrate::types::layout::PortGeometry; +use substrate::types::ArrayBundle; +use substrate::{ + block::Block, + geometry::rect::Rect, + layout::{schema::Schema, Layout}, + types::{layout::PortGeometryBuilder, Array, InOut, Io, Signal}, +}; + +#[derive(Clone, Debug, Default, Io)] +pub struct ViaIo { + pub x: InOut, +} + +#[derive(Clone, Debug, Io)] +pub struct RectsIo { + pub x: InOut>, +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Block)] +#[substrate(io = "ViaIo")] +pub struct Via; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Layer { + MetBot, + Cut, + MetTop, +} + +impl Schema for Layer { + type Layer = Layer; +} + +impl Layout for Via { + type Data = (); + type Schema = Layer; + type Bundle = ViaIoView>; + fn layout( + &self, + cell: &mut substrate::layout::CellBuilder, + ) -> substrate::error::Result<(Self::Bundle, Self::Data)> { + cell.draw(Shape::new(Layer::MetTop, Rect::from_sides(0, 0, 100, 100)))?; + let cut = Shape::new(Layer::Cut, Rect::from_sides(40, 40, 60, 60)); + cell.draw(cut.clone())?; + cell.draw(Shape::new(Layer::MetBot, Rect::from_sides(20, 20, 80, 80)))?; + let mut x = PortGeometryBuilder::default(); + x.push(cut); + Ok((ViaIoView { x: x.build()? }, ())) + } +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub struct Rects { + n_io: usize, + n_drawn: usize, +} + +impl Block for Rects { + type Io = RectsIo; + + fn io(&self) -> Self::Io { + RectsIo { + x: InOut(Array::new(self.n_io, Signal)), + } + } + fn name(&self) -> substrate::arcstr::ArcStr { + substrate::arcstr::literal!("rects") + } +} + +impl Layout for Rects { + type Data = (); + type Schema = Layer; + type Bundle = RectsIoView>; + fn layout( + &self, + cell: &mut substrate::layout::CellBuilder, + ) -> substrate::error::Result<(Self::Bundle, Self::Data)> { + let ports = (0..self.n_drawn) + .map(|i| { + let i = i as i64; + let cut = Shape::new( + Layer::Cut, + Rect::from_sides(40 + 100 * i, 40, 60 + 100 * i, 60), + ); + cell.draw(cut.clone()).expect("failed to draw geometry"); + PortGeometry::new(cut) + }) + .collect(); + let x = ArrayBundle::new(Signal, ports); + Ok((RectsIoView { x }, ())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use layir::Element; + use substrate::context::Context; + + #[test] + fn export_via_layout() { + let ctx = Context::builder().build(); + let lib = ctx.export_layir(Via).expect("failed to export layout"); + assert_eq!(lib.layir.cells().count(), 1); + let cell = lib.layir.cells().next().unwrap().1; + assert_eq!(cell.name(), "via"); + assert_eq!(cell.elements().count(), 3); + assert_eq!(cell.ports().count(), 1); + let port = cell.ports().next().unwrap(); + let mut iter = port.1.elements(); + let x = iter.next().unwrap(); + assert_eq!( + *x, + Element::Shape(Shape::new(Layer::Cut, Rect::from_sides(40, 40, 60, 60))) + ); + } + + #[test] + fn export_rects_layout() { + let ctx = Context::builder().build(); + let lib = ctx + .export_layir(Rects { + n_io: 12, + n_drawn: 12, + }) + .expect("failed to export layout"); + assert_eq!(lib.layir.cells().count(), 1); + let cell = lib.layir.cells().next().unwrap().1; + assert_eq!(cell.name(), "rects"); + assert_eq!(cell.elements().count(), 12); + assert_eq!(cell.ports().count(), 12); + let mut ports = cell.ports(); + let port = ports.next().unwrap(); + let mut iter = port.1.elements(); + let x = iter.next().unwrap(); + assert_eq!( + *x, + Element::Shape(Shape::new(Layer::Cut, Rect::from_sides(40, 40, 60, 60))) + ); + let port = ports.next().unwrap(); + let mut iter = port.1.elements(); + let x = iter.next().unwrap(); + assert_eq!( + *x, + Element::Shape(Shape::new(Layer::Cut, Rect::from_sides(140, 40, 160, 60))) + ); + } + + #[test] + fn export_rects_layout_mismatched_io_length() { + let ctx = Context::builder().build(); + assert!(ctx + .export_layir(Rects { + n_io: 12, + n_drawn: 16, + }) + .is_err()); + } +}