Skip to main content

klayout_core/
layermap.rs

1//! Layer mapping file (.layermap / .map / .lyp shorthand).
2//!
3//! IP integration always goes through a layer-name translation step:
4//! foundry A's "M1" might be `(layer=10, datatype=0)` while foundry B
5//! ships the same metal as `(layer=68, datatype=20)`. A `LayerMap`
6//! captures the mapping so layouts can be re-keyed without manual
7//! editing.
8//!
9//! The format we read/write is the de-facto industry plaintext form:
10//!
11//! ```text
12//! # comment
13//! METAL1   drawing  10  0
14//! METAL1   pin      10  1
15//! METAL1   label    10  2
16//! VIA12    drawing  68  0
17//! ```
18//!
19//! Each entry is `<name> <purpose> <layer> <datatype>`. `purpose` is a
20//! free-form string (`drawing`, `pin`, `label`, `text`, `boundary`,
21//! …); convention varies by foundry. Unknown purposes are preserved.
22//!
23//! Calibre/SVRF style maps and KLayout `.lyp` XML files are not v1
24//! targets — those carry display metadata (color, fill) on top of the
25//! mapping and would warrant their own crates.
26
27use crate::layer::LayerInfo;
28use smol_str::SmolStr;
29use std::collections::HashMap;
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct LayerMapEntry {
33    pub name: SmolStr,
34    pub purpose: SmolStr,
35    pub layer: u16,
36    pub datatype: u16,
37}
38
39#[derive(Default, Clone, Debug)]
40pub struct LayerMapping {
41    pub entries: Vec<LayerMapEntry>,
42    by_key: HashMap<(SmolStr, SmolStr), usize>,
43    by_gds: HashMap<(u16, u16), usize>,
44}
45
46impl LayerMapping {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    pub fn insert(&mut self, entry: LayerMapEntry) {
52        let idx = self.entries.len();
53        self.by_key.insert((entry.name.clone(), entry.purpose.clone()), idx);
54        self.by_gds.insert((entry.layer, entry.datatype), idx);
55        self.entries.push(entry);
56    }
57
58    pub fn lookup_name(&self, name: &str, purpose: &str) -> Option<&LayerMapEntry> {
59        let key = (SmolStr::from(name), SmolStr::from(purpose));
60        self.by_key.get(&key).map(|&i| &self.entries[i])
61    }
62
63    pub fn lookup_gds(&self, layer: u16, datatype: u16) -> Option<&LayerMapEntry> {
64        self.by_gds.get(&(layer, datatype)).map(|&i| &self.entries[i])
65    }
66
67    /// Convert an entry to a [`LayerInfo`] using `name` as the layer
68    /// name. The `purpose` field is dropped — `LayerInfo` is keyed by
69    /// (layer, datatype) only.
70    pub fn to_layer_info(entry: &LayerMapEntry) -> LayerInfo {
71        LayerInfo::named(entry.name.clone(), entry.layer, entry.datatype)
72    }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum LayerMapError {
77    #[error("layermap parse error on line {line}: {msg}")]
78    Parse { line: usize, msg: String },
79}
80
81pub fn parse_layermap(text: &str) -> Result<LayerMapping, LayerMapError> {
82    let mut map = LayerMapping::new();
83    for (line_no, line) in text.lines().enumerate() {
84        let line_no = line_no + 1;
85        let trimmed = line.trim();
86        if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
87            continue;
88        }
89        // Split on whitespace; tolerate tabs.
90        let parts: Vec<&str> = trimmed.split_whitespace().collect();
91        if parts.len() < 4 {
92            return Err(LayerMapError::Parse {
93                line: line_no,
94                msg: format!("expected 4 fields, got {}", parts.len()),
95            });
96        }
97        let name = SmolStr::from(parts[0]);
98        let purpose = SmolStr::from(parts[1]);
99        let layer = parts[2].parse::<u16>().map_err(|_| LayerMapError::Parse {
100            line: line_no,
101            msg: format!("invalid layer number `{}`", parts[2]),
102        })?;
103        let datatype = parts[3].parse::<u16>().map_err(|_| LayerMapError::Parse {
104            line: line_no,
105            msg: format!("invalid datatype `{}`", parts[3]),
106        })?;
107        map.insert(LayerMapEntry {
108            name,
109            purpose,
110            layer,
111            datatype,
112        });
113    }
114    Ok(map)
115}
116
117pub fn write_layermap(map: &LayerMapping) -> String {
118    use std::fmt::Write as _;
119    let mut out = String::new();
120    let _ = writeln!(out, "# name  purpose  layer  datatype");
121    // Determine column widths so the output is aligned.
122    let name_w = map
123        .entries
124        .iter()
125        .map(|e| e.name.len())
126        .max()
127        .unwrap_or(0)
128        .max(4);
129    let purp_w = map
130        .entries
131        .iter()
132        .map(|e| e.purpose.len())
133        .max()
134        .unwrap_or(0)
135        .max(7);
136    for e in &map.entries {
137        let _ = writeln!(
138            out,
139            "{:<name_w$} {:<purp_w$} {:>3} {:>3}",
140            e.name.as_str(),
141            e.purpose.as_str(),
142            e.layer,
143            e.datatype,
144            name_w = name_w,
145            purp_w = purp_w,
146        );
147    }
148    out
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    const SAMPLE: &str = r#"
156# Foundry XYZ map
157METAL1   drawing  10  0
158METAL1   pin      10  1
159METAL1   label    10  2
160VIA12    drawing  68  0
161POLY     drawing   7  0
162"#;
163
164    #[test]
165    fn parses_basic_map() {
166        let map = parse_layermap(SAMPLE).unwrap();
167        assert_eq!(map.entries.len(), 5);
168        let m1 = map.lookup_name("METAL1", "drawing").unwrap();
169        assert_eq!(m1.layer, 10);
170        assert_eq!(m1.datatype, 0);
171    }
172
173    #[test]
174    fn lookup_by_gds_pair() {
175        let map = parse_layermap(SAMPLE).unwrap();
176        let v = map.lookup_gds(68, 0).unwrap();
177        assert_eq!(v.name.as_str(), "VIA12");
178    }
179
180    #[test]
181    fn comments_and_blank_lines_skipped() {
182        let txt = "# header\n\nA drawing 1 0\n# end\n";
183        let map = parse_layermap(txt).unwrap();
184        assert_eq!(map.entries.len(), 1);
185    }
186
187    #[test]
188    fn malformed_rejected() {
189        let bad = "BAD line\n";
190        assert!(parse_layermap(bad).is_err());
191        let bad2 = "X drawing notanumber 0\n";
192        assert!(parse_layermap(bad2).is_err());
193    }
194
195    #[test]
196    fn round_trip_via_string() {
197        let map1 = parse_layermap(SAMPLE).unwrap();
198        let text = write_layermap(&map1);
199        let map2 = parse_layermap(&text).unwrap();
200        assert_eq!(map2.entries.len(), map1.entries.len());
201        for e1 in &map1.entries {
202            let e2 = map2.lookup_name(&e1.name, &e1.purpose).unwrap();
203            assert_eq!(e1.layer, e2.layer);
204            assert_eq!(e1.datatype, e2.datatype);
205        }
206    }
207
208    #[test]
209    fn entry_to_layer_info() {
210        let entry = LayerMapEntry {
211            name: "METAL1".into(),
212            purpose: "drawing".into(),
213            layer: 10,
214            datatype: 0,
215        };
216        let info = LayerMapping::to_layer_info(&entry);
217        assert_eq!(info.name.as_str(), "METAL1");
218        assert_eq!(info.layer, 10);
219    }
220}