# How the rounded_corner function works?

Hi sir,
I have some question about rounded_corners function .
here is my code below....

``````customer_psv_layer=input(86,0)
outter_corner=10
inner_corner=2
shrink_value=10
customer_psv_layer.sized(-shrink_value.um).rounded_corners(inner_corner.um,outter_corner.um,100).output(50,5)
``````

and here is what the pattern we have , you can check with the attatched also.

after this script running , that is result and it is what the question do I meantion.

As you can see , most outter corner can usnig the round corner value to do trim/cut corner and base on the value do I provided.
I knew that , some of shape didn't have enought size to make the round corner base on that value .
Look like them still been make round corner , but the key is....what the value of round corner is?
Can I control that or can I measure that ?

1.7z 2.6K

• Hi jiunnweiyeh

There's a limitation for corner rounding radius, like you mensioned, if the size is smaller then the rounded value, the result will not be matched.

And `round_corner` cannot recognize chamfered corners, the two coner of this chamfered edge wil be rounded separately so the 45deg slant edge (side-1 in following image) needs to be long enough for rounding.

The maximum rounding that can be applied to a corner is as below, the slant part `side-1` is 9.11 um, `d` is 4.555um, the max radius that can be applied `Max r` is 10.996 um, which is larger than 10um

so if you look closely, you'll see the countour is not a single arc from 10um circle, instead it's form by connecting two arcs.

In cases that `Max_r` is smaller than 10um, `Max_r` will be applied instead of 10um

• A de-chamfer process can be applied to avoid the rounding limitation issue from chamfered shapes

de-chamfered result, white dash-lines removes chamfering while preserves step like features.

by filter out vertical, horiozontal and slant edges, we can use their relation to narrow down to chamfer corners.

For slant edge that interacted with both H and V edges and with length < cetran length, we preserve them for further process.

Extracting the bound box of chamfer edges automatically generates the patch shapes for the corner
combined with input layer and merge generates the final result.

sizing and rounding can be applied and shoud gives you what you need.

``````chamferLen = 35
psv_layer  = input(86,0)
edges      = layer.edges
hEdges     = edges.with_angle(0)
vEdges     = edges.with_angle(90)
sEdges     = edges.with_angle(diagonal_only)
chamfer    = sEdges.interacting(hEdges).interacting(vEdges).with_length(0..chamferLen.um)
patch      = chamfer.collect_to_region{|e| e.bbox}
patched    = (psv_layer + patch).merged

chamfer.output(50,4)
patch.output(50,5)
patched.output(50,6)
``````
• Hi @RawrRanger
Thanks a lot for your help , I will check it and make it into what my code.
Thanks.

• edited December 2023

If you're in particular interested in the inner corners and the objects you treat are larger than 40µm in dimension, you can also consider using the Minkowski sum with a circle. The Minkowski sum acts like a "pen" that is drawn around the perimeter of the polygon (like illustrated here: https://demonstrations.wolfram.com/TheMinkowskiSumOfADiskAndAPolygon/).

The Minkowski sum is not readily available in DRC, but can be emulated.

The code is that:

``````customer_psv_layer = input(86, 0)

outer_corner = 10.um
shrink_value = 10.um
num_points = 64

# NOTE: we shrink by the outer_corner radius in addition to compensate for the later
# enlargement by the Minkowski sum
sz = customer_psv_layer.sized(-(shrink_value+outer_corner))

# creates a circle with radius outer_corner centered at 0,0 in database units:
um2dbu = RBA::CplxTrans::new(dbu).inverted
circle_um = RBA::DPolygon::ellipse(RBA::DBox::new(-outer_corner, -outer_corner, outer_corner, outer_corner), num_points)
circle_dbu = um2dbu * circle_um

# apply the Minkowski sum
# NOTE: we use the data object to access the internal Region
sz.data = sz.data.minkowski_sum(circle_dbu)

sz.output(50, 5)
``````

Here is the output:

The yellow dashed polygon in the pre-Minkowski layer.

Matthias

• Hi @RawrRanger @Matthias
Do you have idea for how to check what the min/max value round_corner done in GDS?
as previsouly mention.if the edge is not long enough for rounding.
After round_corner function , the real value of rounding (as your picture , 10.996um) will not match with what the code we command.
what the code I can "GUESS" would be that...
Using "xor" function to boolean the pattern of "before_round" and "after_round"
But look like the bbox value is not what the answer I need.
Do you have any idea for that to check outter /inner rounding value?

``````after_round.xor(before_round).raw.extents.each do |xx|
if xx.bbox.height.round(2) >= xx.bbox.width.round(2) then
pm1_trim_list_inner << xx.bbox.height.round(2)
end
``````

• HI jiunnweiyeh,

Basically what following code does is to loop through all shapes and edges to check their angle/length relation

the length / angle relation check basically does what my previous post shows.

``````layer = input(86,0)

def maxRoundR (layer)
rmax = Float::INFINITY
layer.each{ |poly|
estart = nil
e1     = nil
poly.each_edge{ |e2|
if (e1.nil?) then
estart = e2 #register first edge and leave this for last edge check
else
rmax = [edgeMaxRoundR(e1, e2), rmax].min  #check e1, e2 edge angle and length relation
end
e1 = e2 #store current edge as old edge
}
rmax = [edgeMaxRoundR(e1, estart), rmax].min #check last edge, first edge angle and length relation
}
return rmax
end

def edgeMaxRoundR (e1, e2)
d     = [e1.length, e2.length].min / 2
theta = edgeAngle(e1.d, e2.d).abs  / 2
r     = d * Math::tan(theta)
return r
end

def edgeAngle(v1, v2)
vp = v1.vprod(-v2)
sp = v1.sprod(-v2)
return Math::atan2(vp, sp)
end

puts maxRoundR (layer)
``````

the runnung result of your example layout is `0.0853um` reason due to there's a small segment in this shape
to avoid this, a threshold should be applied to reject those results that is too small.

• edited January 4

Hi RawrRanger ,
sorry for that due to my depiction not accurate to make some gap.
please check with the attached file.
we have 2 layers , 151/11 layer is before round , 151/1 is after round.
I want to check what is the min corner rounding value by inner and outter side.
it is why my previsouly metnion I guess that I need using not or xor function.

``````before_round=input(151,11)
after_round=input(151,1)
after_round.xor(before_round).raw.extents.each do |xx|
if xx.bbox.height.round(2) >= xx.bbox.width.round(2) then
pm1_trim_list_inner << xx.bbox.height.round(2)
end
``````

But I knew bbox should be not the correct answer , and base on this code , I can't separate the inner/outter value .

This is base on when I am a checker not designer .
Designer will base on this code to make trim/shrink/rounding (that is not final version , I am working for combine your code into mind)

``````    customer_psv_layer=input(86,0)
outter_corner=10
inner_corner=2
shrink_value=10
customer_psv_layer.sized(-shrink_value.um).output(151,11)
customer_psv_layer.sized(-shrink_value.um).rounded_corners(inner_corner.um,outter_corner.um,100).output(151,1)
``````

But for Checker , whom have the result GDS 86,0 / 151,1 / 151,11 layers only.
our team need to measure the rouding corner by manual .
This is why/what I ask....

Thanks.

• edited January 4

Hi jiunnweiyeh,

The process can be split into four steps.
(1) Pre-Process oroginal laye to remove chamfers
(2) Feed processed layers to check max inner/outer radius
(3) Apply rounding to layer
(4) compare pre-processed layer to rounded layer and extract rounded part for measurement

The processed gds is as attaehed.
Original layer : L(151/11)
Chamfer patched layer : L(151/12)
Rounded patched layer : L(151/13)
Rounded inner corner layer : L(151/14)
Rounded outer corner layer : L(151/15)

(1) pre processing:
basically the exact copy of my previous post, works the same way.

``````def patchChamfer(layer, threshold = 35)
chamferLen = threshold
edges      = layer.edges
hEdges     = edges.with_angle(0)
vEdges     = edges.with_angle(90)
sEdges     = edges.with_angle(diagonal_only)
chamfer    = sEdges.interacting(hEdges).interacting(vEdges).with_length(0..chamferLen.um)
patch      = chamfer.collect_to_region{|e| e.bbox}
patched    = (layer + patch).merged
return patched
end

originalLayer = input(151,11)
patchedLayer  = patchChamfer(layer, 35)
patchedLayer.output(151,12)
``````

(2) For extracting max radius and (3) apply rounding:
similar code, except now we determine the corner type (inner/outer) and find max separately

``````def maxRoundR (layer)
rmax_inner = Float::INFINITY
rmax_outer = Float::INFINITY

layer.each{ |poly|
estart = nil
e1     = nil
poly.each_edge{ |e2|
if (e1.nil?) then
estart = e2
else

# if emr is negative, the corner is a inner conrner
emr = edgeMaxRoundR(e1, e2)
if emr > 0 then
rmax_outer = [emr,     rmax_outer].min
else
# add abs to remove inner/outer recognition sign
rmax_inner = [emr.abs, rmax_inner].min
end

end
e1 = e2
}

emr = edgeMaxRoundR(e1, estart)
if emr > 0 then
rmax_outer = [emr,     rmax_outer].min
else
rmax_inner = [emr.abs, rmax_inner].min
end

}
return rmax_inner, rmax_outer
end

def edgeMaxRoundR (e1, e2)
# if returned value is negative, the corner is a inner conrner
d     = [e1.length, e2.length].min / 2
theta =  edgeAngle(e1.d, e2.d) / 2
r     = d * Math::tan(theta)
return r
end

def edgeAngle(v1, v2)
# if returned value is negative, the corner is a inner conrner
vp = v1.vprod(-v2)
sp = v1.sprod(-v2)
return Math::atan2(vp, sp)
end

patchedLayer = input(151, 12)
rmax_inner, rmax_outer = maxRoundR (patchedLayer)
roundedLayer = patchedLayer.rounded_corners(rmax_inner.um, rmax_outer.um, 100)
roundedLayer.output(151, 13)

puts "rmax_inner = #{rmax_inner}\nrmax_outer = #{rmax_outer}"
``````

(4) For extracting rounded region and apply measurement:

``````def autoMeasuer(originalLayer, roundedLayer)
view            = RBA::Application.instance.main_window.current_view

# check edges from rounded layer, the non-interacting parts are rounded parts
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
# further split those edges with inside check to determine that is a inner or outer corner
outerRoundEdges = roundEdges.inside(originalLayer)
# extend edges to make those segments becomes a single polygon for the easy of grouping and processing
outerRoundGroup = outerRoundEdges.extended_in(1).merged

innerRoundEdges = roundEdges.not_inside(originalLayer)
innerRoundGroup = innerRoundEdges.extended_out(1).merged

[innerRoundGroup, outerRoundGroup].each{|g|
g.collect{ |p|
sides = []
edges = []
p.each_edge{|e|
if (e.length < 0.002) then

sides << e
else
edges << e
end
}
# check each polygon in each group, and separate them into two categories
# edges from two sides of 1dbu extension
# any given point from the arc center part
e1, e2      = sides[0], sides[1]
ec          = edges[((edges.length - 1) / 3).round()]

# form a three point arc measurement using annotation
ant         = RBA::Annotation::new()
ant.style   = RBA::Annotation::StyleLine
ant.points  = [e1.p1, ec.p1, e2.p1]
view.insert_annotation(ant)

}
}

return innerRoundEdges, outerRoundEdges
end

patchedLayer    = input(151,12)
roundedLayer    = input(151,13)
extractedEdges  = autoMeasuer(patchedLayer, roundedLayer)
innerRoundEdges = extractedEdges[0]
outerRoundEdges = extractedEdges[1]

innerRoundEdges.output(151,14)
outerRoundEdges.output(151,15)
``````
• The arc finding part is like this, first get edges, use extend in/out to form a polygon and merge it

check small length edges, which should be the target end piece pushed out by extended_in

it should have two of them, `e1` and `e2`, finallyget any given point from other edges as `ec`

feed them for annotation measurement

• edited January 4

@RawrRanger
After check your code 3 (autoMeasuer(originalLayer, roundedLayer)) that is workable.
Thanks.
Cause code1 /2 should be merged into the code of designer.
for Checker , they should using what the layers in GDS file and check/measure it.
Your code is workable for measure , but can we get the min value for inner / outter corner round ?
Now , I get the measure result in GDS as below figure.
but human (checker) need to check the value by each measure, how can I get the min. vlaue ?

by the way , what the programming command I make corner round is outter by 10um and inner 2um.
as this figure shown , some of measure result is 9.9935...um , I knew that will be gap in measure due to grid .
is that reason?

• edited January 4

Hi jiunnweiyeh,

Glad this works, unfortunatelly the annotation (Ruler) cannot provid radius data, so we'll need to implement a fit function.

example as below, this was modified from this source
I can't read math, but the code seems works fine.

``````def circle_interpolation(p1, p2, p3)
r    = Float::INFINITY
temp = p2.x * p2.x + p2.y * p2.y
bc   = (p1.x * p1.x + p1.y * p1.y - temp) / 2
cd   = (temp - p3.x * p3.x - p3.y * p3.y) / 2
det  = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y)

if (det.abs > 1.0e-6) then
cx = (bc *(p2.y - p3.y) - cd * (p1.y - p2.y)) / det
cy = ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
r  = ((cx - p1.x) ** 2 + (cy - p1.y) ** 2) ** (0.5)
end
return r
end
``````

by adding this line

``````    puts circle_interpolation (e1.p1, ec.p1, e2.p1)
``````

after this should generate all radius results.

``````    ant.points  = [e1.p1, ec.p1, e2.p1]
``````

as to the question of 10.00 um to 9.9935...um mis-match, the source of differences can be :
(1) Fit circle error
(2) Off grid of the layout
(3) The macig from edge extension

The (1) (2) is quite intuitive, the (3) probably makes most of the contribtion to this difference.

because we extend the edges to polygon for easy of operation, and loop through edges and filter our `e1`,`e2`
by their length, so they might not always appears in sequence, means either one of these edges could be `e1` depends on the shape of the object

this could leads to situations like this, where `e1.p1` is registered, but is not the point that is exactly on it's original edge

and because I kind of just choos a randon point from the polygon, by deviding the edge list in to `3` so it might also happens to be points that is on the other side, like the example shows.

• edited January 5

Hi jiunnweiyeh,

To minimize the error mensioned above, which was caused by measurement point not exactly on the edge,
I've implement a `pick3Points` function, which first uses the `cut_point` of the two side to determine the curved side.
Due to the nature of how these two small sides were generated, the cut point will always located at the curved-in side of the arc.

By comparing the distance between`e1.p1`/`e1.p2` and cut_point `cp` we can properly choose the correct point that is on the actual Arc, the one that is on the far side.

Similar approach applies to `e2` and `ec`, the mid point of Arc.

With the help of cut_point `cp` we can cast a edge from `cp` and throuth the Arc polygon at a roughly center location.

This should always generates two edgs `ecs` that intersects the cast edge, by choosing the one that is far away from the `ec`, we can determine our last point on the Arc polygon.

So as image shown, the three `far` points can be choosen and feed to circle fitting

A slighe improvement, around 1~3 dbu of accuracy is observed using previous test cases `try.gds`.

``````def pick3Points(sides, edges)
e1, e2   = sides[0], sides[1]
cp       = e1.cut_point(e2)
p1       = (cp.distance(e1.p1) > cp.distance(e1.p2)) ? e1.p1 : e1.p2
p2       = (cp.distance(e2.p1) > cp.distance(e2.p2)) ? e2.p1 : e2.p2
echeck   = RBA::DEdge::new(cp, cp + (p1-cp) + (p2-cp))
ecs      = edges.select{|e| e.intersects?(echeck)}
ec1, ec2 = ecs[0], ecs[1]
pc       = (cp.distance(ec1.p1) > cp.distance(ec2.p1)) ? ec1.p1 : ec2.p1
return p1, pc, p2
end

def addCircleAnt(view, p1, pc, p2)
ant         = RBA::Annotation::new()
ant.style   = RBA::Annotation::StyleLine
ant.points  = [p1, pc, p2]
view.insert_annotation(ant)
end

def circle_interpolation(p1, p2, p3)
r    = Float::INFINITY
temp = p2.x * p2.x + p2.y * p2.y
bc   = (p1.x * p1.x + p1.y * p1.y - temp) / 2
cd   = (temp - p3.x * p3.x - p3.y * p3.y) / 2
det  = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y)

if (det.abs > 1.0e-6) then
cx = (bc *(p2.y - p3.y) - cd * (p1.y - p2.y)) / det
cy = ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
r  = ((cx - p1.x) ** 2 + (cy - p1.y) ** 2) ** (0.5)
end
return r
end

def autoMeasuer(originalLayer, roundedLayer)
view            = RBA::Application.instance.main_window.current_view
dbu             = view.active_cellview.layout.dbu
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged

[innerRoundGroup, outerRoundGroup].each{|g|
g.collect{ |p|
sides, edges = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
addCircleAnt(view, p1, pc, p2)
puts circle_interpolation(p1, pc, p2)
}
}
return innerRoundEdges, outerRoundEdges
end

patchedLayer    = input(151,12)
roundedLayer    = input(151,13)
extractedEdges  = autoMeasuer(patchedLayer, roundedLayer)
innerRoundEdges = extractedEdges[0]
outerRoundEdges = extractedEdges[1]

innerRoundEdges.output(151,14)
outerRoundEdges.output(151,15)
``````
• Hi @RawrRanger
Thanks very much for your hard work and coding.
But look like the puts result mix both of inner / outter value.
Do you have any idea for that?

• edited January 7

Wow ... @RawrRanger I need to digest this first.

But a brief comment: There is the "Radius Ruler" type. It can measure a radius when you provide three points (clicks):

The measurements are precise only to the actual values which are subject to DBU rounding (1nm in this case).

Matthias

• edited January 8

Hi @Matthias
Let me talk you a long story~
totolay , we have 2 function , 1 for designer , 1 for checker.

The first code is for designer
I am working for bumping house , our customer provide what the passivation layer (as picture shown , it is polygon) to us.
we need to do some process like this in our design team ...

``````def patchChamfer(layer, threshold )
chamferLen = threshold
edges      = layer.edges
hEdges     = edges.with_angle(0)
vEdges     = edges.with_angle(90)
sEdges     = edges.with_angle(diagonal_only)
chamfer    = sEdges.interacting(hEdges).interacting(vEdges).with_length(0..chamferLen.um)
patch      = chamfer.collect_to_region{|e| e.bbox}
patched    = (layer + patch).merged
return patched
end

originalLayer = input(86,0)
outter_corner=10
inner_corner=2
shrink_value=10
first_step_layer=originalLayer.sized(-shrink_value.um)
patchedLayer  = patchChamfer(first_step_layer, 10)
patchedLayer.output(151,11)
patchedLayer.rounded_corners(inner_corner.um,outter_corner.um,100).output(151,12)
``````

After that , finally , when the designer finished a lot modify for the layer 151,12
our check team need to base on the GDSII file Designer provided to check if any DRC or anything wrong.
when that moment , checker only have the GDSII file include what the original layer customer provided (86,0) as my code.
and 151,11 layer (after shrink) and 151,12 (after round corner).(designer make it)

As the previosuly mention , sometimes when the polygon shape didn't have enought length to make chamfer. (10um for outter in this case)
The round_corner function will base on what the real length of shape to make it corner routing .
That is what /reason our checker need to confirm---the real rounding value is not 10um (outter) / 2um (inner) , we have to check what is the min one.
And it is what the code RawrRanger provied .

As what I said , RawrRanger's function is workable , but checker need to know what /where the min. value for inner /outter rounding.
It is hard to check the auto-measure result by human .
I have try to make a array to store circle_interpolation(p1, pc, p2) (follow RawrRanger's code)
Unfortunate , I can't seprater the value by inner / outter....
Can you help it?

• Hi jiunnweiyeh,

The round data are stored in `outerRoundGroup` and `innerRoundGroup` respectively, because my previous example group then into a list, so the output results will be mixed together.

Extract this part into a separate function `measureGroup` which takes a`view` object (for insert annotation) and group data.

``````    measureGroup(view, innerRoundGroup).each {|r| puts "inner : #{r}"}
measureGroup(view, outerRoundGroup).each {|r| puts "outer : #{r}"}
``````

this provides measurement results separately for inner/outer rounding.

the rest of the parts stays the same.

``````def measureGroup(view, roundGroup)
r = []
roundGroup.collect{ |p|
sides, edges  = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
addCircleAnt(view, p1, pc, p2)
r << circle_interpolation(p1, pc, p2)
}
return r
end

def autoMeasuer(originalLayer, roundedLayer)
view            = RBA::Application.instance.main_window.current_view
dbu             = view.active_cellview.layout.dbu
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged
measureGroup(view, innerRoundGroup).each {|r| puts "inner : #{r}"}
measureGroup(view, outerRoundGroup).each {|r| puts "outer : #{r}"}
end
``````
• Here , I combine both of your code to let our checker to easy check the round value and location...
some of case , this code is workable , but some of case I got error message as below

Do you have any idea for how the fixt it?

``````def pick3Points(sides, edges)
e1, e2   = sides[0], sides[1]
cp       = e1.cut_point(e2)
p1       = (cp.distance(e1.p1) > cp.distance(e1.p2)) ? e1.p1 : e1.p2
p2       = (cp.distance(e2.p1) > cp.distance(e2.p2)) ? e2.p1 : e2.p2
echeck   = RBA::DEdge::new(cp, cp + (p1-cp) + (p2-cp))
ecs      = edges.select{|e| e.intersects?(echeck)}
ec1, ec2 = ecs[0], ecs[1]
pc       = (cp.distance(ec1.p1) > cp.distance(ec2.p1)) ? ec1.p1 : ec2.p1
return p1, pc, p2
end

def addCircleAnt(view, p1, pc, p2)
ant         = RBA::Annotation::new()
ant.style   = RBA::Annotation::StyleLine
ant.points  = [p1, pc, p2]
view.insert_annotation(ant)
end

def circle_interpolation(p1, p2, p3)
r    = Float::INFINITY
temp = p2.x * p2.x + p2.y * p2.y
bc   = (p1.x * p1.x + p1.y * p1.y - temp) / 2
cd   = (temp - p3.x * p3.x - p3.y * p3.y) / 2
det  = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y)

if (det.abs > 1.0e-6) then
cx = (bc *(p2.y - p3.y) - cd * (p1.y - p2.y)) / det
cy = ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
r  = ((cx - p1.x) ** 2 + (cy - p1.y) ** 2) ** (0.5)
end
return r
end

def measureGroup(view, roundGroup)
r = []
roundGroup.collect{ |p|
sides, edges  = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
r << circle_interpolation(p1, pc, p2)
#  addCircleAnt(view, p1, pc, p2)
}
return r
end

def autoMeasuer(originalLayer, roundedLayer)
view            = RBA::Application.instance.main_window.current_view
dbu             = view.active_cellview.layout.dbu
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged
inner_value =Array.new()
outter_value =Array.new()
measureGroup(view, innerRoundGroup).each {|r| inner_value << r}
measureGroup(view, outerRoundGroup).each {|r| outter_value << r}
puts "the min corner value of ineer is #{inner_value.min.round(2)}"
puts "the min corner value of outter is #{outter_value.min.round(2)}"
RBA::MessageBox.info("check Value","the min corner value of ineer is #{inner_value.min.round(2)}  \
the min corner value of outter is #{outter_value.min.round(2)}", RBA::MessageBox.b_ok)
return inner_value.min.round(2) ,outter_value.min.round(2)
end

def autoMeasuer2(originalLayer, roundedLayer,min_inner,min_outter)
view            = RBA::Application.instance.main_window.current_view
dbu             = view.active_cellview.layout.dbu
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged

[innerRoundGroup, outerRoundGroup].each{|g|
g.collect{ |p|
sides, edges = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
r=(circle_interpolation(p1, pc, p2)).round(2)
if min_inner==2 && min_outter==10 then
if (r <min_outter) then
addCircleAnt(view, p1, pc, p2)
end
end

if (min_inner!=2) && (r ==min_inner || r==min_outter) then
addCircleAnt(view, p1, pc, p2)
end
}
}
return innerRoundEdges, outerRoundEdges
end

patchedLayer    = input(151,101)
roundedLayer    = input(151,1)
real_round_pattern=roundedLayer.interacting(patchedLayer)
min_inner,min_outter=autoMeasuer(patchedLayer, real_round_pattern)
if infcase=="min.value" then
extractedEdges  = autoMeasuer2(patchedLayer, real_round_pattern,min_inner,min_outter)
end

if infcase=="2(inner)/10(outter)_check" then
extractedEdges  = autoMeasuer2(patchedLayer, real_round_pattern,2,10)
end

innerRoundEdges = extractedEdges[0]
outerRoundEdges = extractedEdges[1]

innerRoundEdges.output(151,14)
outerRoundEdges.output(151,15)
``````
• edited January 10

Hi jiunnweiyeh,

The error message :
`undefined method of 'distance' for nil:NilClass`
Means you are trying to access the `distance` attribute of a empty object.

This error is caused by function `def pick3Points(sides, edges)`
the code that causes this error are lines that contains `cp.distance(XXX)`, which suggests the point `cp`
is not being generated properly by `e1.cut_point(e2)`, the only possible reason for this is
the two edge `e1` and `e2` just happens to be in parellel, and no crossing point is found
for an ideal Arc shaped object this should not occur.

This revised version will try to diliver less accurate results if the arc is not a ideal shape,
if the value is off by too much then the rest of the rounded corners, you can apply filters to hightlight the area,

``````def pick3Points(sides, edges)
e1, e2   = sides[0], sides[1]
cp       = e1.cut_point(e2)
if cp then
p1       = (cp.distance(e1.p1) > cp.distance(e1.p2)) ? e1.p1 : e1.p2
p2       = (cp.distance(e2.p1) > cp.distance(e2.p2)) ? e2.p1 : e2.p2
echeck   = RBA::DEdge::new(cp, cp + (p1-cp) + (p2-cp))
ecs      = edges.select{|e| e.intersects?(echeck)}
ec1, ec2 = ecs[0], ecs[1]
pc       = (cp.distance(ec1.p1) > cp.distance(ec2.p1)) ? ec1.p1 : ec2.p1
return p1, pc, p2
end
return e1.p1, edges[((edges.length - 1) / 4).round()].p1, e2.p1
end
``````
• Hi @RawrRanger
I have update the code as you provied , and I keep these function cause each function can help us to find the trim value , size , location.

``````circle_interpolation(p1, p2, p3)
measureGroup(view, roundGroup)
pick3Points(sides, edges)
``````

The detail (full coding) please check with the second attached file.
And runing that in the attached 1 , you will get the error message , that been shown as below.
Thanks.

• Hi jiunnweiyeh,

Thanks for the test case, and regarding to the error of `undefined method 'x'`
this is due to I for got to access the edge with `p1` attribute, the intention here is to return a point from edge instead of returning an edge.

the code above has been updated with a correct return

``````return e1.p1, edges[((edges.length - 1) / 4).round()].p1, e2.p1
#                                                    ^^^
#                                        this part was missing
``````

As to how `undefined method of 'distance' for nil:NilClass` this issue is triggered, I found the error is related to this shape, the vertex generated by rounding (red) snapped to a grid in a way that the whole edge shift slightly awayfrom original edge (white), which makes this straight line being falsely recognized as an rounded corner, due to I use `edges.select_not_interacting` to separate rounded Arcs.

• Hi @RawrRanger
Got the udpate and I will try it ASAP.
Thanks very much for your help.

• edited January 10

Hi jiunnweiyeh,

few update about the code, I see you use autoMeasure/autoMeasure2 to process shapes and analyze results separately.

So i've revised this part so it retuens a list of dictionary which contains radius info and collection of points for adding annotation, now this main function is renamed to `measuerResults` .

``````def addCircleAnt(p1, pc, p2)
view        = RBA::Application.instance.main_window.current_view
ant         = RBA::Annotation::new()
ant.style   = RBA::Annotation::StyleLine
ant.points  = [p1, pc, p2]
view.insert_annotation(ant)
end

def pick3Points(sides, edges)
e1, e2   = sides[0], sides[1]
cp       = e1.cut_point(e2)
if cp then
p1       = (cp.distance(e1.p1) > cp.distance(e1.p2)) ? e1.p1 : e1.p2
p2       = (cp.distance(e2.p1) > cp.distance(e2.p2)) ? e2.p1 : e2.p2
echeck   = RBA::DEdge::new(cp, cp + (p1-cp) + (p2-cp))
ecs      = edges.select{|e| e.intersects?(echeck)}
ec1, ec2 = ecs[0], ecs[1]
pc       = (cp.distance(ec1.p1) > cp.distance(ec2.p1)) ? ec1.p1 : ec2.p1
return p1, pc, p2
end
# condition for non-ideal arc
return e1.p1, edges[((edges.length - 1) / 4).round()].p1, e2.p1
end

def circle_interpolation(p1, p2, p3)
r    = Float::INFINITY
temp = p2.x * p2.x + p2.y * p2.y
bc   = (p1.x * p1.x + p1.y * p1.y - temp) / 2
cd   = (temp - p3.x * p3.x - p3.y * p3.y) / 2
det  = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y)

if (det.abs > 1.0e-6) then
cx = (bc *(p2.y - p3.y) - cd * (p1.y - p2.y)) / det
cy = ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
r  = ((cx - p1.x) ** 2 + (cy - p1.y) ** 2) ** (0.5)
end
return r
end

def measureGroup(roundGroup)
results = []
roundGroup.collect{ |p|
# Arc polygon with only 4 points are false signal casued by grid snapping
if p.num_points > 4 then
sides, edges  = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
results << {
"radius" => circle_interpolation(p1, pc, p2),
"points" => [p1, pc, p2]
}
end
}
# pack radius  and points info for further analyze
return results
end

def measuerResults(originalLayer, roundedLayer)
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged
inVal           = measureGroup(innerRoundGroup)
outVal          = measureGroup(outerRoundGroup)
return inVal, outVal, innerRoundEdges, outerRoundEdges
end

def showMsgbox(inVal, outVal)
# empty check, incase there's no inner rounded corners or no outer rounded corner
inValMinR     = (inVal.empty?)  ? 0 : inVal.collect{ |v| v["radius"]}.min
outValMinR    = (outVal.empty?) ? 0 : outVal.collect{|v| v["radius"]}.min

inMinRStr     = "the min corner value of inner is #{inValMinR.round(2)}"
outMinRStr    = "the min corner value of outer is #{outValMinR.round(2)}"
resultMSG     = "#{inMinRStr}\n#{outMinRStr}"
puts resultMSG
RBA::MessageBox.info("check Value", resultMSG, RBA::MessageBox.b_ok)
end

def showDialog(inVal, outVal)
# collect all "R" and get min Radius
# empty check, incase there's no inner rounded corners or no outer rounded corner
inValMinR  = (inVal.empty?)  ? 0 : inVal.collect{ |v| v["radius"]}.min
outValMinR = (outVal.empty?) ? 0 : outVal.collect{|v| v["radius"]}.min

ansList    = ["2(inner)/10(outter)_check", "min.value"]
infcase    = RBA::InputDialog::ask_item("request", "choose one way to find answer:", ansList, 1)

if infcase == "min.value" then
min_inner  = inValMinR.round(2)
min_outter = outValMinR.round(2)
# Use "R" to filter out sites that needs a Annotation,
# hight light by insert Ant using existed data unpacked from "Points"
inVal.each{ |v| (v["radius"].round(2) == min_inner)  ? addCircleAnt( *v["points"]) : nil }
outVal.each{|v| (v["radius"].round(2) == min_outter) ? addCircleAnt( *v["points"]) : nil }
end

if infcase == "2(inner)/10(outter)_check" then
min_inner  =  2
min_outter = 10
inVal.each{ |v| (v["radius"].round(2) < min_outter) ? addCircleAnt( *v["points"]) : nil }
outVal.each{|v| (v["radius"].round(2) < min_outter) ? addCircleAnt( *v["points"]) : nil }
end
end

patchedLayer  = input(151,101)
roundedLayer  = input(151,1)
inVal, outVal, inEdges, outEdges = measuerResults(patchedLayer, roundedLayer)

# this provides a data in following format,
# with radius and points for Annotation insetion
# inVal = [
#     {"radius" => R1, "points" => [p1_1, pc_1, p2_1]},
#     {"radius" => R2, "points" => [p1_2, pc_2, p2_2]},
#       ...
#     {"radius" => Rn, "points" => [p1_n, pc_n, p2_n]},
# ]

showMsgbox(inVal, outVal)
showDialog(inVal, outVal)
inEdges.output(151, 14)
outEdges.output(151, 15)
``````
• edited January 10

Hi @RawrRanger
Thanks , look like that will make a exception caught named "cut_point" .

• Hi jiunnweiyeh,

This should be related to edge lengh check part, of `measureGroup`
`p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }`

which did not get sufficient amount of edges, results in input value `e1` in `pick3Points` becomse nil

In normal case, this should gurantee at least 2 edge is generated, which was from extend function `Edges.extended_in(1).merged`

I've checked the result using sample `try.gds` and new sample `9999.gds`, but cannot successfully triggger this exception, a sample of layout that triggers this message will be very helpful.

• Hi @RawrRanger
I got the answer ,cause some of pattern didn't have "patchedLayer" , so your code is workable .
I just need to add a simple code to filter it .

``````real_round_pattern=(roundedLayer.interacting(patchedLayer)).not_interacting(outruleLayer)
inVal, outVal, inEdges, outEdges = measuerResults(patchedLayer, real_round_pattern)
``````

Thanks very much for your kindly help.

• edited January 11

Hi @RawrRanger and @jiunnweiyeh,

your collaboration is amazing ... I really appreciate this, specifically the nice visualizations of @RawrRanger!

I just wanted to come back to @jiunnweiyeh 's comment at Jan 8:

The round_corner function will base on what the real length of shape to make it corner routing .
That is what /reason our checker need to confirm---the real rounding value is not 10um (outter) / 2um (inner) , we have to check what is the min one.

The rules by which the `round_corner` functions works are not very elaborate and can be evaluated.

Basically, two connecting edges make a circular corner. Three edges form two corners. Here is one with an inner (r1) and outer corner (r2):

This is a well-formed case where the given radii can be implemented. The parts denoted by "s1" and "s2" and their counterparts on the attached edges are converted into circular sections.

However, when `s1 + s2 == length(E2)`, E2 is entirely consumed by the circular sections. If `s1 + s2 > length(E2)`, the circular sections cannot be formed any longer without introducing polygon path inversion - this is something we like to avoid.

To avoid this, the radii r1 and r2 are reduced in this case:

``````f = length(E2) / (s1 + s2)
if f < 1.0:
r1 *= f
r2 *= f
``````

In general, each radius will potentially need to be reduced twice: r1 needs to both fit into E2 (together with r2) and E1 (together with the predecessor leftmost r0 which is not shown in the picture).

So the full scheme is that:

``````# for r1, fitting r0 (not shown) and r2 into E1 and E2 respectively
f1 = length(E1) / (s0 + s1)
f2 = length(E2) / (s1 + s2)
f = min(f1, f2)
if f < 1.0:
r1 *= f
``````

So with this knowledge we can basically compute the minimum radius.

Here is some code that performs this evaluation for one selected polygon and two sample radii (inner: 0.5 and outer: 1.0 µm):

``````rinner = 0.5
router = 1.0

# Needs to have one polygon selected
# (disclaimer: no checking)

s = RBA::LayoutView::current.each_object_selected.first.shape
poly = s.dsimple_polygon

# Radius evaluation for one Polygon:

# Code translated from dbPolygonTools.cc

points = poly.each_point.to_a
inner_or_outer = []
seg = []

# collect the circular segment lengths s[i] in "seg"
# and the proposed radii in "rad".
# "inner_or_outer" collects a flag:
# true for inner and false for outer corner

points.each_with_index do |p,i|

# prev and next point
pp = points[i - 1]
pn = points[i + 1 - points.size]

# unit vectors along the edge before p and after p
eb = p - pp
eb *= 1.0 / eb.length
ea = pn - p
ea *= 1.0 / ea.length

# angle between edges
sin_a = eb.vprod(ea)
cos_a = eb.sprod(ea)
a = Math.atan2(sin_a, cos_a).abs

io = sin_a > 0.0
r = io ? rinner : router
s = r * (Math.sin(a * 0.5) / Math.cos(a * 0.5)).abs

inner_or_outer[i] = io
seg[i] = s

end

min_rinner = nil
max_rinner = nil
min_router = nil
max_router = nil

points.each_with_index do |p,i|

# prev and next point
pp = points[i - 1]
pn = points[i + 1 - points.size]

# prev, this and next circular segment lengths
sp = seg [i - 1]
s  = seg [i]
sn = seg [i + 1 - seg.size]

# reduction factors from edge before and after
# (normally 1 in the good case)
fb = [ 1.0, (p - pp).length / (sp + s) ].min
fa = [ 1.0, (pn - p).length / (s + sn) ].min

r = [ fb, fa ].min * rad [i]

if inner_or_outer[i]
max_rinner = [ max_rinner || r, r ].max
min_rinner = [ min_rinner || r, r ].min
else
max_router = [ max_router || r, r ].max
min_router = [ min_router || r, r ].min
end

end

# print results

puts "R(inner): %.7g .. %.7g" % [ min_rinner, max_rinner ]
puts "R(outer): %.7g .. %.7g" % [ min_router, max_router ]
``````

The results for this polygon:

Are these:

``````R(inner): 0.06666667 .. 0.3313708
R(outer): 0.1333333 .. 1
``````

The minimum values correspond well to the critical situation here, within the limits of measurement accuracy of the radii:

Best regards,

Matthias

P.S: I am aware that the quality of the drawings do NOT match those of @RawrRanger

• Hi @Matthias,
Thanks for your code, let me digestion it.
in my side , our design team have another idea for corner rounding by programming.
--Maybe I can check if the shape single-edge size small than rule value (maybe 10.um)
Let the programming process all of other shape for corner_rounding (if the single -edge length > 10um)
The others , let designer to check and process by manufature.
I will check if I can do that or I need your help.
Thanks very much for all your help.

• edited January 12

Hi sir ,
Do you have function to check the single-edge length < rule ?
such as ..

``````psv=input(86,0)
psv.edges.with_length(0..10).output(86,1)
chklayer=input(86,1)
psv.interacting(chklayer).output(100,50)
``````

look like I have to change polygon layer to a edge layer , but I have no idea for how to make it....

• update , I got answer , it should be ...
psv=input(86,0)
chklayer=psv.edges.with_length(0.um..5.um)
psv.interacting(chklayer.extended(:out => 1.0)).output(100,50)
Thanks.

• Hi @RawrRanger
Could you please help to check why I can't measure the cut corner value in this pattern?

here the GDS file and what the meausre script as below.

``````def pick3Points(sides, edges)
e1, e2   = sides[0], sides[1]
cp       = e1.cut_point(e2)
if cp then
p1       = (cp.distance(e1.p1) > cp.distance(e1.p2)) ? e1.p1 : e1.p2
p2       = (cp.distance(e2.p1) > cp.distance(e2.p2)) ? e2.p1 : e2.p2
echeck   = RBA::DEdge::new(cp, cp + (p1-cp) + (p2-cp))
ecs      = edges.select{|e| e.intersects?(echeck)}
ec1, ec2 = ecs[0], ecs[1]
pc       = (cp.distance(ec1.p1) > cp.distance(ec2.p1)) ? ec1.p1 : ec2.p1
return p1, pc, p2
end
return e1.p1, edges[((edges.length - 1) / 4).round(1)].p1, e2.p1
end

def addCircleAnt(view, p1, pc, p2)
ant         = RBA::Annotation::new()
ant.style   = RBA::Annotation::StyleLine
ant.points  = [p1, pc, p2]
view.insert_annotation(ant)
end

def circle_interpolation(p1, p2, p3)
r    = Float::INFINITY
temp = p2.x * p2.x + p2.y * p2.y
bc   = (p1.x * p1.x + p1.y * p1.y - temp) / 2
cd   = (temp - p3.x * p3.x - p3.y * p3.y) / 2
det  = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y)

if (det.abs > 1.0e-6) then
cx = (bc *(p2.y - p3.y) - cd * (p1.y - p2.y)) / det
cy = ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
r  = ((cx - p1.x) ** 2 + (cy - p1.y) ** 2) ** (0.5)
end
return r
end

def measureGroup(view, roundGroup,minvalue,gap)
layout = RBA::CellView::active.layout
marklayer = layout.layer(500,0)
dbu  = view.active_cellview.layout.dbu
databaseunit= 1 / dbu
current_work_cell= RBA::Application::instance.main_window.current_view.active_cellview.cell_name
drawtop = layout.cell(current_work_cell)
r = []
roundGroup.collect{ |p|
sides, edges  = [], []
p.each_edge{|e| ((e.length < dbu * 2) ? sides : edges) << e }
p1, pc, p2  = pick3Points(sides, edges)
rc=circle_interpolation(p1, pc, p2)
r << rc
if rc.round(3) < (minvalue.to_f-gap.to_f ).round(3) then
addCircleAnt(view, p1, pc, p2)
p1x=(p1.x*databaseunit).round(0)
p1y=(p1.y*databaseunit).round(0)
p2x=(p2.x*databaseunit).round(0)
p2y=(p2.y*databaseunit).round(0)
drawtop.shapes(marklayer).insert(RBA::Box::new(p1x,p1y,p2x,p2y))
end
}
return r
end

def autoMeasuer(originalLayer, roundedLayer , measure_type , min_value , tolerence)

view            = RBA::Application.instance.main_window.current_view
dbu             = view.active_cellview.layout.dbu
roundEdges      = roundedLayer.edges.select_not_interacting(originalLayer.edges)
outerRoundEdges = roundEdges.inside(originalLayer)
innerRoundEdges = roundEdges.not_inside(originalLayer)
outerRoundGroup = outerRoundEdges.extended_in(1).merged
innerRoundGroup = innerRoundEdges.extended_out(1).merged
inner_value =Array.new()
outter_value =Array.new()

if measure_type=="1" then
measureGroup(view, innerRoundGroup,min_value,tolerence).each {|r| inner_value << r}
if (inner_value.min).is_a? Numeric then
return inner_value.min.round(2)
else
return 0
end
end

if measure_type=="2" then
measureGroup(view, outerRoundGroup,min_value,tolerence).each {|r| outter_value << r}
if (outter_value.min).is_a? Numeric then
return outter_value.min.round(2)
else
return 0
end
end
end
######################################################################
def userform ()
layoutView = RBA::Application::instance.main_window.current_view.active_cellview.layout
dialog = RBA::QDialog.new( RBA::Application.instance.main_window)
dialog.windowTitle = "measure PM1 corner trim value"
formlayout = RBA::QVBoxLayout::new(dialog)
dialog.setLayout(formlayout)
dialog.setFixedWidth(350)
mw = RBA::Application::instance.main_window

mw.layout
view = mw.current_view
label =  RBA::QLabel.new(dialog)
measure_type =  RBA::QComboBox.new(dialog)
label =  RBA::QLabel.new(dialog)

label =  RBA::QLabel.new(dialog)
label.text = "(min. check size)"
min_Check_value=  RBA::QLineEdit.new(dialog)

label =  RBA::QLabel.new(dialog)
label.text = "(tolerence)"
tolerence=  RBA::QLineEdit.new(dialog)
tolerence.text="0.05"

buttonOK = RBA::QPushButton.new(dialog)
buttonOK.text = " OK "
buttonOK.clicked do
#############################################################
outrule_layer=20
measure_base_layer=40
tolerence=tolerence.text.to_f
min_Check_value=min_Check_value.text.to_f
infcase=measure_type.currentText.split(".")[0]
patchedLayer    = input(151,measure_base_layer)
roundedLayer    = input(151,1)
outruleLayer    = input(151,outrule_layer)

if roundedLayer.is_empty? then
patchedLayer    = input(51,measure_base_layer)
roundedLayer    = input(51,1)
outruleLayer    = input(51,outrule_layer)
end

real_round_pattern=(roundedLayer.interacting(patchedLayer)).not_interacting(outruleLayer)

if infcase=="1" then
min_inner=autoMeasuer(patchedLayer, real_round_pattern,infcase,min_Check_value,tolerence)
RBA::MessageBox.info("check_result","The min corner value of inner(小數下第二位四捨五入(round(2))) is #{min_inner}", RBA::MessageBox.b_ok)
end

if infcase=="2" then
min_outer=autoMeasuer(patchedLayer, real_round_pattern,infcase,min_Check_value,tolerence)
RBA::MessageBox.info("check_result","The min corner value of outer(小數下第二位四捨五入(round(2)) is #{min_outer}", RBA::MessageBox.b_ok)
end
#############################################################
mark_layer=input(500,0)
mark_layer.output(500,0)
dialog.accept()
end  ##### for button OK.
buttonOK = RBA::QPushButton.new(dialog)