# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # DESCRIPTION: A simple layer processing framework # # Run as # # klayout -rm layer_proc.rbm # # Or copy the file to the KLayout installation directory. On Windows, # the file is found automatically, on Unix, the $KLAYOUT_PATH variable # must be set pointing to the installation folder of the script. # # This script will install a new menu entry in the "Tools" menu, # called "Processing Scripts". It allows to load a processing script # and will maintain a list of most recently used scrips in the menu # entries below the "Processing Script" menu entry. # # The file is basically a ruby script which is executed in the context # of the processing environment and the current layout and cell. # A simple script is composed of simple ruby statements. Each statement # specifies a layer operation. # # Layers are basically objects and the operations are methods. The "input" # function is used to create a layer initially from a database layer. # Various methods are provided that implement certain operations on layers. # # For example, the following script # # active = input("2/0") # poly = input("3/0") # gates = active.and(poly) # gates.output("10/0") # # computes the gates as the intersection region of active area and # poly. The active region is taken from layer 2, datatype 0 and the poly # region from 3, datatype 0. The output is written to layer 10, datatype 0. # The "and" operation is basically a method of the "active" layer # object which receives the second operand as the argument. # # The script can be written somewhat shorter: # # input("2/0").and(input("3/0")).output("10/0") # # or even shorter, since the "and" method is equivalent to the "*" # operator: # # (input("2/0") * input("3/0")).output("10/0") # # The following methods are implemented currently: # # input(s) - Create a layer as input from the layout database # (s is either "l/d", "l" or "n(l/d)" or "n", where # l is the layer, d the datatype and n the layer name. # # bbox - Create a layer with the bounding rectangle of the # current cell. # # l.and(u) or l*u - Boolean AND (intersection) between l and u. # # l.or(u) or l+u - Boolean OR (union) between l and u. # # l.xor(u) or l^u - Boolean XOR between l and u. # # l.not(u) or l-u - Boolean NOT (difference) between l and u. # # l.size(d) - Isotropic sizing by the distance d (can be negative). # # l.size(dx, dy) - Anisotropic sizing with a sizing of dx in x direction and # dy in y direction. dx and dy can be negative. # # l.bboxes - Expand all shapes into their bounding rectangles and # create a new layer with the boxes. # # l.round(ri, ro, n) - Round corners with inner and outer radius and number of # interpolation points per full circle. # # l.rsize(d) - Size all polygons individually (isotropic). # Unlike the "size" method, this method does not join the # polygon regions before the sizing is applied. This allows to # use "merge(1)" to select regions where the sized polygons # overlap. # # l.rsize(dx, dy) - Size all polygons individually (anisotropic). # # l.shift(dx, dy) - Shift the layer by the given displacement. # # l.rotate(a) - Rotate the layer by the given angle. # # l.mirror - Mirror the layer at the x axis. # # l.mag(m) - Magnify the layer.. # # l.merge - Merge the layer (join the polygon regions). # # l.merge(o) - Merge the layer (join the polygon regions) and select # areas with a certain minimum overlap count. # Values for o are: 0 (as normal merge), 1 (where more than # two polygons overlap), 2 (where more that three polygons # overlap) and so on. # # l.output(s) - Output the layer to the layout database # (s is either "l/d", "l" or "n(l/d)" or "n", where # l is the layer, d the datatype and n the layer name. # If the output layer already exists, it is deleted. # # All distances are given in micron units, all angles in degree. # ---------------------------------------------------------------- # A class representing a layer in the boolean processor class LayerProcessorLayer def initialize(p, layer, original) @p = p @layer = layer @original = original end def size(*args) @p.size(self, *args) end def and(*args) @p.and(self, *args) end def *(other) @p.and(self, other) end def or(*args) @p.or(self, *args) end def +(other) @p.or(self, other) end def not(*args) @p.not(self, *args) end def -(other) @p.not(self, other) end def xor(*args) @p.xor(self, *args) end def ^(other) @p.xor(self, other) end def bboxes @p.bboxes(self) end def merge(*args) @p.merge(self, *args) end def round(*args) @p.round(self, *args) end def rsize(*args) @p.rsize(self, *args) end def shift(dx, dy) @p.transform(self, RBA::CplxTrans.new(1, 0, false, RBA::DPoint.new(@p.to_dbu(dx), @p.to_dbu(dy)))) end def mirror @p.transform(self, RBA::CplxTrans.new(1, 0, true, RBA::DPoint.new(0, 0))) end def rotate(a) @p.transform(self, RBA::CplxTrans.new(1, a, false, RBA::DPoint.new(0, 0))) end def mag(m) @p.transform(self, RBA::CplxTrans.new(m, 0, false, RBA::DPoint.new(0, 0))) end def output(lps) @p.output(lps, self) end def layer_index @layer end def original @original end private @p @layer @original end # ---------------------------------------------------------------- # The basic boolean processor class class LayerProcessor def initialize(layout, cell) @layout = layout @cell = cell @proc = RBA::ShapeProcessor.new @temp_layers = [] end # Locate a layer for input. # The layer is specified by a layer specification string which identifies the layer by # a layer and datatype for example. If the layer does not exist, it is created. # This method returns a layer object which can be used as input for other functions. # The layer specification string is either "l", "l/d", "n(l/d)" or "n". def input(lps) layer_index = nil lp = string_to_layerinfo(lps) # locate the layer if it already exists (0..(@layout.layers-1)).each do |i| if @layout.is_valid_layer?(i) && @layout.get_info(i).is_equivalent?(lp) layer_index = i break end end # create a new layer if it does not exist return LayerProcessorLayer.new(self, layer_index || @layout.insert_layer(lp), true) end # Assign a layer as output. # Change the layer given by the layer object to the output layer specified # by a layer specification string. If the layer already exists, it is deleted. # The layer specification string is either "l", "l/d", "n(l/d)" or "n". def output(lps, l) lp = string_to_layerinfo(lps) layer_index = l.layer_index li = nil # locate the layer if it already exists (0..(@layout.layers-1)).each do |i| if @layout.is_valid_layer?(i) && @layout.get_info(i).is_equivalent?(lp) li = i break end end if l.original # delete the layer if it already exists if li @layout.delete_layer(li) end res = @layout.insert_layer(lp) @cell.copy(l.layer_index, res) @cell.called_cells.each do |c| @layout.cell(c).copy(l.layer_index, res) end else # clear the layer if it already exists if li != layer_index if li @layout.delete_layer(li) end @layout.set_info(layer_index, lp) end # remove the layer from the list of temporary layers new_temp = [] @temp_layers.each do |i| if i != layer_index new_temp.push(i) end end @temp_layers = new_temp end end # Size a layer by the given value (in micron). # "input" is the input layer, given by a layer object. # Two forms are supported: size(input, d) (isotropic) and size(input, dx, dy) (anisotropic) def size(l, *args) input = l.layer_index if args.size == 1 res = tmp() @proc.size(@layout, @cell, input, @cell.shapes(res), to_dbu(args[0]), 2, true, true, true) return LayerProcessorLayer.new(self, res, false) elsif (args.size == 2) res = tmp() @proc.size(@layout, @cell, input, @cell.shapes(res), to_dbu(args[0]), to_dbu(args[1]), 2, true, true, true) return LayerProcessorLayer.new(self, res, false) else raise "size method expects one (isotropic) or two (anisotropic) size dimension values" end end # Merge a layer # Merges the layer, optionally with overlap selection def merge(l, *args) input = l.layer_index wc = 0 if args.size == 1 wc = args[0] elsif (args.size == 0) else raise "merge method expects one or two (with overlap selection) arguments" end res = tmp() @proc.merge(@layout, @cell, input, @cell.shapes(res), true, wc, true, true) return LayerProcessorLayer.new(self, res, false) end # OR two layers # "a" and "b" are the input layers, given by layer objects. def or(la, lb) a = la.layer_index b = lb.layer_index res = tmp() @proc.boolean(@layout, @cell, a, @layout, @cell, b, @cell.shapes(res), RBA::EdgeProcessor::mode_or, true, true, true) return LayerProcessorLayer.new(self, res, false) end # XOR two layers # "a" and "b" are the input layers, given by layer objects. def xor(la, lb) a = la.layer_index b = lb.layer_index res = tmp() @proc.boolean(@layout, @cell, a, @layout, @cell, b, @cell.shapes(res), RBA::EdgeProcessor::mode_xor, true, true, true) return LayerProcessorLayer.new(self, res, false) end # AND two layers # "a" and "b" are the input layers, given by layer objects. def and(la, lb) a = la.layer_index b = lb.layer_index res = tmp() @proc.boolean(@layout, @cell, a, @layout, @cell, b, @cell.shapes(res), RBA::EdgeProcessor::mode_and, true, true, true) return LayerProcessorLayer.new(self, res, false) end # NOT two layers # "a" and "b" are the input layers, given by layer objects. def not(la, lb) a = la.layer_index b = lb.layer_index res = tmp() @proc.boolean(@layout, @cell, a, @layout, @cell, b, @cell.shapes(res), RBA::EdgeProcessor::mode_anotb, true, true, true) return LayerProcessorLayer.new(self, res, false) end # Returns the bbox of the current cell def bbox res = tmp() @cell.shapes(res).insert(@cell.bbox) return LayerProcessorLayer.new(self, res, false) end # Individual sizing of polygons (i.e. as input for merge with overlap selection) def rsize(l, *args) if args.size == 1 res = tmp() d = to_dbu(args[0]) cells = @cell.called_cells cells.push(@cell.cell_index) cells.each do |c| cc = @layout.cell(c) cc.shapes(l.layer_index).each do |shape| if (shape.is_polygon? || shape.is_box? || shape.is_path?) poly = shape.polygon poly.size(d, 2) cc.shapes(res).insert(poly) end end end return LayerProcessorLayer.new(self, res, false) elsif (args.size == 2) res = tmp() dx = to_dbu(args[0]) dy = to_dbu(args[1]) cells = @cell.called_cells cells.push(@cell.cell_index) cells.each do |c| cc = @layout.cell(c) cc.shapes(l.layer_index).each do |shape| if (shape.is_polygon? || shape.is_box? || shape.is_path?) poly = shape.polygon poly.size(dx, dy, 2) cc.shapes(res).insert(poly) end end end return LayerProcessorLayer.new(self, res, false) else raise "rsize method expects one (isotropic) or two (anisotropic) size dimension values" end end # Transform the given layer with the given transformation. t is the transformation and must be # a CplxTrans object. # This operation also flattens the layer def transform(l, t) res = tmp() output = @cell.shapes(res) si = @layout.begin_shapes(@cell.cell_index, l.layer_index) while !si.at_end? new_shape = output.insert(si.shape) output.transform(new_shape, t * si.trans) si.next end return LayerProcessorLayer.new(self, res, false) end # Round the corners of all polygons with the given inner (ri) and outer (ro) radius and n points # per circle. def round(l, ri_micron, ro_micron, n) res = tmp() ri = to_dbu(ri_micron) ro = to_dbu(ro_micron) cells = @cell.called_cells cells.push(@cell.cell_index) cells.each do |c| cc = @layout.cell(c) cc.shapes(l.layer_index).each do |shape| if (shape.is_polygon? || shape.is_box? || shape.is_path?) poly = shape.polygon if poly.points > 0 # workaround for segmentation fault on round_corners for empty polygon begin cc.shapes(res).insert(poly.round_corners(ri, ro, n)) rescue # workaround for problem with short edges $stderr.puts "Warning: corner rounding failed." cc.shapes(res).insert(poly) end end end end end return LayerProcessorLayer.new(self, res, false) end # returns the boxes of all shapes in the given input layer def bboxes(l) res = tmp() @cell.shapes(l.layer_index).each do |shape| @cell.shapes(res).insert(shape.bbox) end @cell.called_cells.each do |c| cc = @layout.cell(c) cc.shapes(l.layer_index).each do |shape| cc.shapes(res).insert(shape.bbox) end end return LayerProcessorLayer.new(self, res, false) end # Convert a value from micron to database units def to_dbu(micron) return (0.5 + micron / @layout.dbu).floor() end # Run a procedure in the context of this object def run(script) begin eval(script) ensure cleanup end end private @layout @cell @proc # Create a new temporary layer and remember for removal on "cleanup". # This method returns a layer index which can be used as input for other functions. def tmp lp = RBA::LayerInfo.new layer_index = @layout.insert_layer(lp) @temp_layers.push(layer_index) return layer_index end # Clean up all temporary layers def cleanup @temp_layers.each do |i| @layout.delete_layer(i) end @temp_layers = [] end # convert the layer specification into a LayerInfo structure # format: "l", "l/d", "n(l/d)" or "n". def string_to_layerinfo(layer_spec) ls = nil if (layer_spec =~ /^(\d+)$/) ls = RBA::LayerInfo.new($1.to_i, 0) elsif (layer_spec =~ /^(\d+)\/(\d+)$/) ls = RBA::LayerInfo.new($1.to_i, $2.to_i) elsif (layer_spec =~ /^(.*)\s*\((\d+)\/(\d+)\)$/) ls = RBA::LayerInfo.new($2.to_i, $3.to_i, $1) else ls = RBA::LayerInfo.new(layer_spec) end return ls end end # ---------------------------------------------------------------- # Helper classes class MenuAction < RBA::Action def initialize(title, shortcut, &action) self.title = title self.shortcut = shortcut @action = action end def triggered @action.call(self) end private @action end class LayerProcessorMRUAction < MenuAction def initialize(&action) super("", "", &action) @action = action end def triggered @action.call(self) end def script=(s) @script = s self.visible = (s != nil) if s self.title = File.basename(s) end end def script @script end private @action @script end # ---------------------------------------------------------------- # The layer processing script environment class LayerProcessingScriptEnvironment def initialize app = RBA::Application.instance mw = app.main_window @action = MenuAction.new("Processing Script", "") do view = RBA::Application.instance.main_window.current_view if !view raise "No view open for running the boolean script on" end fn = RBA::FileDialog::get_open_file_name("Select Script", "", "All Files (*)"); if fn.has_value? file_path = run_script(fn.value) make_mru(fn.value) end end menu = mw.menu menu.insert_separator("tools_menu.end", "processing_script_group") menu.insert_menu("tools_menu.end", "processing_script_submenu", "Processing Scripts") menu.insert_item("tools_menu.processing_script_submenu.end", "processing_script_load", @action) menu.insert_separator("tools_menu.processing_script_submenu.end.end", "processing_script_mru_group") @mru_actions = [] (1..4).each do |i| a = LayerProcessorMRUAction.new do |action| run_script(action.script) make_mru(action.script) end @mru_actions.push(a) menu.insert_item("tools_menu.processing_script_submenu.end", "processing_script_mru#{i}", a) a.script = nil end # try to save the MRU list to $HOME/.klayout-processing-mru i = 0 home = ENV["HOME"] if home fn = File.join(home, ".klayout-processing") begin File.open(fn, "r") do |file| file.readlines.each do |line| if line =~ /(.*)<\/mru>/ if i < @mru_actions.size @mru_actions[i].script = $1 end i += 1 end end end rescue end end end def run_script(fn) view = RBA::Application.instance.main_window.current_view if !view raise "No view open for running the boolean script on" end cv = view.cellview(view.active_cellview_index) file_path = fn text = nil File.open(file_path) do |file| text = file.read end if !text RBA::MessageBox::critical("Error", "Error reading file #{file_path}", RBA::MessageBox::b_ok) return end begin LayerProcessor.new(cv.layout, cv.cell).run(text) rescue RBA::MessageBox.critical("Script failed", $!, RBA::MessageBox.b_ok) ensure # Add layers that have been created so far to the layout view RBA::Application.instance.main_window.cm_lv_add_missing view.update_content end end def make_mru(script) scripts = [script] @mru_actions.each do |a| if a.script != script scripts.push(a.script) end end while scripts.size < @mru_actions.size scripts.push(nil) end (0..(@mru_actions.size-1)).each do |i| @mru_actions[i].script = scripts[i] end # try to save the MRU list to $HOME/.klayout-processing-mru home = ENV["HOME"] if home fn = File.join(home, ".klayout-processing") File.open(fn, "w") do |file| file.puts(""); @mru_actions.each do |a| if a.script file.puts("#{a.script}") end end file.puts(""); end end end end $layer_processing_environment = LayerProcessingScriptEnvironment.new