From dea2b76dc85890e68c309246b309fd13b19ec0dc Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Wed, 29 May 2019 21:35:02 +0200 Subject: [PATCH] Added unit tests for res and cap device extractors. --- src/db/db/dbNetlist.cc | 2 +- src/db/unit_tests/dbNetlistExtractorTests.cc | 301 +++++++++++++++++-- testdata/algo/device_extract_capres_nets.gds | Bin 0 -> 14282 bytes testdata/algo/devices_test.oas | Bin 0 -> 1282 bytes 4 files changed, 281 insertions(+), 22 deletions(-) create mode 100644 testdata/algo/device_extract_capres_nets.gds create mode 100644 testdata/algo/devices_test.oas diff --git a/src/db/db/dbNetlist.cc b/src/db/db/dbNetlist.cc index 24a9329ff..ea1359b44 100644 --- a/src/db/db/dbNetlist.cc +++ b/src/db/db/dbNetlist.cc @@ -546,7 +546,7 @@ std::string Netlist::to_string () const if (p != pd.begin ()) { ps += ","; } - ps += p->name () + "=" + tl::to_string (d->parameter_value (p->id ())); + ps += p->name () + "=" + tl::sprintf ("%.12g", d->parameter_value (p->id ())); } res += std::string (" device ") + tl::to_word_or_quoted_string (d->device_class ()->name ()) + " " + device2string (*d) + " (" + ts + ") (" + ps + ");\n"; } diff --git a/src/db/unit_tests/dbNetlistExtractorTests.cc b/src/db/unit_tests/dbNetlistExtractorTests.cc index b9e67104d..413e9b949 100644 --- a/src/db/unit_tests/dbNetlistExtractorTests.cc +++ b/src/db/unit_tests/dbNetlistExtractorTests.cc @@ -58,7 +58,7 @@ static unsigned int define_layer (db::Layout &ly, db::LayerMap &lmap, int gds_la return lid; } -static void dump_nets_to_layout (const db::Netlist &nl, const db::hier_clusters &clusters, db::Layout &ly, const std::map &lmap, const db::CellMapping &cmap) +static void dump_nets_to_layout (const db::Netlist &nl, const db::hier_clusters &clusters, db::Layout &ly, const std::map &lmap, const db::CellMapping &cmap, bool with_device_cells = false) { std::set device_cells_seen; @@ -75,7 +75,7 @@ static void dump_nets_to_layout (const db::Netlist &nl, const db::hier_clusters< any_shapes = ! lc.begin (m->first).at_end (); } - if (any_shapes) { + if (any_shapes || (with_device_cells && n->terminal_count() > 0)) { std::string nn = "NET_" + c->name () + "_" + n->expanded_name (); db::Cell &net_cell = ly.cell (ly.add_cell (nn.c_str ())); @@ -88,40 +88,63 @@ static void dump_nets_to_layout (const db::Netlist &nl, const db::hier_clusters< } } + if (with_device_cells) { + for (db::Net::const_terminal_iterator t = n->begin_terminals (); t != n->end_terminals (); ++t) { + + const db::NetTerminalRef &tref = *t; + db::cell_index_type dci = tref.device ()->device_abstract ()->cell_index (); + db::DCplxTrans dtr = tref.device ()->trans (); + + net_cell.insert (db::CellInstArray (db::CellInst (dci), db::CplxTrans (ly.dbu ()).inverted () * dtr * db::CplxTrans (ly.dbu ()))); + + for (std::vector::const_iterator a = tref.device ()->other_abstracts ().begin (); a != tref.device ()->other_abstracts ().end (); ++a) { + const db::DeviceAbstractRef &aref = *a; + dci = aref.device_abstract->cell_index (); + net_cell.insert (db::CellInstArray (db::CellInst (dci), db::CplxTrans (ly.dbu ()).inverted () * dtr * aref.trans * db::CplxTrans (ly.dbu ()))); + } + + } + } + } } for (db::Circuit::const_device_iterator d = c->begin_devices (); d != c->end_devices (); ++d) { + std::vector original_device_cells; + db::cell_index_type dci = d->device_abstract ()->cell_index (); - - if (device_cells_seen.find (dci) != device_cells_seen.end ()) { - continue; + if (device_cells_seen.find (dci) == device_cells_seen.end ()) { + original_device_cells.push_back (dci); + device_cells_seen.insert (dci); } - db::Cell &device_cell = ly.cell (cmap.cell_mapping (dci)); - device_cells_seen.insert (dci); - - std::string ps; - const std::vector &pd = d->device_class ()->parameter_definitions (); - for (std::vector::const_iterator i = pd.begin (); i != pd.end (); ++i) { - if (! ps.empty ()) { - ps += ","; + for (std::vector::const_iterator a = d->other_abstracts ().begin (); a != d->other_abstracts ().end (); ++a) { + db::cell_index_type dci = a->device_abstract->cell_index (); + if (device_cells_seen.find (dci) == device_cells_seen.end ()) { + original_device_cells.push_back (dci); + device_cells_seen.insert (dci); } - ps += i->name () + "=" + tl::to_string (d->parameter_value (i->id ())); } - const std::vector &td = d->device_class ()->terminal_definitions (); - for (std::vector::const_iterator t = td.begin (); t != td.end (); ++t) { + for (std::vector::const_iterator i = original_device_cells.begin (); i != original_device_cells.end (); ++i) { - const db::local_cluster &dc = clusters.clusters_per_cell (dci).cluster_by_id (d->device_abstract ()->cluster_id_for_terminal (t->id ())); + db::cell_index_type dci = *i; + db::Cell &device_cell = ly.cell (cmap.cell_mapping (dci)); - for (std::map::const_iterator m = lmap.begin (); m != lmap.end (); ++m) { - db::Shapes &target = device_cell.shapes (m->second); - for (db::local_cluster::shape_iterator s = dc.begin (m->first); !s.at_end (); ++s) { - target.insert (*s); + const std::vector &td = d->device_class ()->terminal_definitions (); + for (std::vector::const_iterator t = td.begin (); t != td.end (); ++t) { + + const db::local_cluster &dc = clusters.clusters_per_cell (dci).cluster_by_id (d->device_abstract ()->cluster_id_for_terminal (t->id ())); + + for (std::map::const_iterator m = lmap.begin (); m != lmap.end (); ++m) { + db::Shapes &target = device_cell.shapes (m->second); + for (db::local_cluster::shape_iterator s = dc.begin (m->first); !s.at_end (); ++s) { + target.insert (*s); + } } + } } @@ -823,3 +846,239 @@ TEST(3_DeviceAndNetExtractionWithImplicitConnections) db::compare_layouts (_this, ly, au); } +TEST(4_ResAndCapExtraction) +{ + db::Layout ly; + db::LayerMap lmap; + + unsigned int nwell = define_layer (ly, lmap, 1); + unsigned int active = define_layer (ly, lmap, 2); + unsigned int poly = define_layer (ly, lmap, 3); + unsigned int poly_lbl = define_layer (ly, lmap, 3, 1); + unsigned int diff_cont = define_layer (ly, lmap, 4); + unsigned int poly_cont = define_layer (ly, lmap, 5); + unsigned int metal1 = define_layer (ly, lmap, 6); + unsigned int metal1_lbl = define_layer (ly, lmap, 6, 1); + unsigned int via1 = define_layer (ly, lmap, 7); + unsigned int metal2 = define_layer (ly, lmap, 8); + unsigned int metal2_lbl = define_layer (ly, lmap, 8, 1); + unsigned int cap = define_layer (ly, lmap, 10); + unsigned int res = define_layer (ly, lmap, 11); + + { + db::LoadLayoutOptions options; + options.get_options ().layer_map = lmap; + options.get_options ().create_other_layers = false; + + std::string fn (tl::testsrc ()); + fn = tl::combine_path (fn, "testdata"); + fn = tl::combine_path (fn, "algo"); + fn = tl::combine_path (fn, "devices_test.oas"); + + tl::InputStream stream (fn); + db::Reader reader (stream); + reader.read (ly, options); + } + + db::Cell &tc = ly.cell (*ly.begin_top_down ()); + + db::DeepShapeStore dss; + dss.set_text_enlargement (1); + dss.set_text_property_name (tl::Variant ("LABEL")); + + // original layers + db::Region rnwell (db::RecursiveShapeIterator (ly, tc, nwell), dss); + db::Region ractive (db::RecursiveShapeIterator (ly, tc, active), dss); + db::Region rpoly_all (db::RecursiveShapeIterator (ly, tc, poly), dss); + db::Region rpoly_lbl (db::RecursiveShapeIterator (ly, tc, poly_lbl), dss); + db::Region rdiff_cont (db::RecursiveShapeIterator (ly, tc, diff_cont), dss); + db::Region rpoly_cont (db::RecursiveShapeIterator (ly, tc, poly_cont), dss); + db::Region rmetal1 (db::RecursiveShapeIterator (ly, tc, metal1), dss); + db::Region rmetal1_lbl (db::RecursiveShapeIterator (ly, tc, metal1_lbl), dss); + db::Region rvia1 (db::RecursiveShapeIterator (ly, tc, via1), dss); + db::Region rmetal2 (db::RecursiveShapeIterator (ly, tc, metal2), dss); + db::Region rmetal2_lbl (db::RecursiveShapeIterator (ly, tc, metal2_lbl), dss); + db::Region rcap (db::RecursiveShapeIterator (ly, tc, cap), dss); + db::Region rres (db::RecursiveShapeIterator (ly, tc, res), dss); + + // derived regions + + db::Region rpoly = rpoly_all - rres; + db::Region rpoly_res = rpoly_all & rres; + + db::Region rpactive = ractive & rnwell; + db::Region rpgate = rpactive & rpoly; + db::Region rpsd = rpactive - rpgate; + + db::Region rnactive = ractive - rnwell; + db::Region rngate = rnactive & rpoly; + db::Region rnsd = rnactive - rngate; + + db::Region rcap1 = rmetal1 & rcap; + db::Region rcap2 = rmetal2 & rcap; + + // return the computed layers into the original layout and write it for debugging purposes + + unsigned int lgate = ly.insert_layer (db::LayerProperties (10, 0)); // 10/0 -> Gate + unsigned int lsd = ly.insert_layer (db::LayerProperties (11, 0)); // 11/0 -> Source/Drain + unsigned int lpdiff = ly.insert_layer (db::LayerProperties (12, 0)); // 12/0 -> P Diffusion + unsigned int lndiff = ly.insert_layer (db::LayerProperties (13, 0)); // 13/0 -> N Diffusion + unsigned int lpoly_res = ly.insert_layer (db::LayerProperties (14, 0)); // 14/0 -> Resistor Poly + unsigned int lcap1 = ly.insert_layer (db::LayerProperties (15, 0)); // 15/0 -> Cap 1 + unsigned int lcap2 = ly.insert_layer (db::LayerProperties (16, 0)); // 16/0 -> Cap 2 + + rpgate.insert_into (&ly, tc.cell_index (), lgate); + rngate.insert_into (&ly, tc.cell_index (), lgate); + rpsd.insert_into (&ly, tc.cell_index (), lsd); + rnsd.insert_into (&ly, tc.cell_index (), lsd); + rpsd.insert_into (&ly, tc.cell_index (), lpdiff); + rnsd.insert_into (&ly, tc.cell_index (), lndiff); + rpoly_res.insert_into (&ly, tc.cell_index (), lpoly_res); + rcap1.insert_into (&ly, tc.cell_index (), lcap1); + rcap2.insert_into (&ly, tc.cell_index (), lcap2); + + // perform the extraction + + db::Netlist nl; + db::hier_clusters cl; + + db::NetlistDeviceExtractorMOS3Transistor pmos_ex ("PMOS"); + db::NetlistDeviceExtractorMOS3Transistor nmos_ex ("NMOS"); + db::NetlistDeviceExtractorResistor res_ex ("POLY_RES", 50.0); + db::NetlistDeviceExtractorCapacitor cap_ex ("MIM_CAP", 1.0e-15); + + db::NetlistDeviceExtractor::input_layers dl; + + dl["SD"] = &rpsd; + dl["G"] = &rpgate; + // terminal patches + dl["tG"] = &rpoly; + dl["tS"] = &rpsd; + dl["tD"] = &rpsd; + pmos_ex.extract (dss, 0, dl, nl, cl); + + dl.clear (); + dl["SD"] = &rnsd; + dl["G"] = &rngate; + // terminal patches + dl["tG"] = &rpoly; + dl["tS"] = &rnsd; + dl["tD"] = &rnsd; + nmos_ex.extract (dss, 0, dl, nl, cl); + + dl.clear (); + dl["R"] = &rpoly_res; + dl["C"] = &rpoly; + // terminal patches + dl["tA"] = &rpoly; + dl["tB"] = &rpoly; + res_ex.extract (dss, 0, dl, nl, cl); + + dl.clear (); + dl["P1"] = &rcap1; + dl["P2"] = &rcap2; + // terminal patches + dl["tA"] = &rmetal1; + dl["tB"] = &rmetal2; + cap_ex.extract (dss, 0, dl, nl, cl); + + + // perform the net extraction + + db::NetlistExtractor net_ex; + + db::Connectivity conn; + // Intra-layer + conn.connect (rpsd); + conn.connect (rnsd); + conn.connect (rpoly); + conn.connect (rdiff_cont); + conn.connect (rpoly_cont); + conn.connect (rmetal1); + conn.connect (rvia1); + conn.connect (rmetal2); + // Inter-layer + conn.connect (rpsd, rdiff_cont); + conn.connect (rnsd, rdiff_cont); + conn.connect (rpoly, rpoly_cont); + conn.connect (rpoly_cont, rmetal1); + conn.connect (rdiff_cont, rmetal1); + conn.connect (rmetal1, rvia1); + conn.connect (rvia1, rmetal2); + conn.connect (rpoly, rpoly_lbl); // attaches labels + conn.connect (rmetal1, rmetal1_lbl); // attaches labels + conn.connect (rmetal2, rmetal2_lbl); // attaches labels + + // extract the nets + + net_ex.extract_nets (dss, 0, conn, nl, cl, "*"); + + // Flatten device circuits + + std::vector circuits_to_flatten; + circuits_to_flatten.push_back ("RES"); + circuits_to_flatten.push_back ("RES_MEANDER"); + circuits_to_flatten.push_back ("TRANS"); + circuits_to_flatten.push_back ("TRANS2"); + + for (std::vector::const_iterator i = circuits_to_flatten.begin (); i != circuits_to_flatten.end (); ++i) { + db::Circuit *c = nl.circuit_by_name (*i); + tl_assert (c != 0); + nl.flatten_circuit (c); + } + + // cleanup + completion + nl.combine_devices (); + nl.make_top_level_pins (); + nl.purge (); + + EXPECT_EQ (all_net_names_unique (nl), true); + + // debug layers produced for nets + // 202/0 -> Active + // 203/0 -> Poly + // 204/0 -> Diffusion contacts + // 205/0 -> Poly contacts + // 206/0 -> Metal1 + // 207/0 -> Via1 + // 208/0 -> Metal2 + // 210/0 -> N source/drain + // 211/0 -> P source/drain + std::map dump_map; + dump_map [layer_of (rpsd) ] = ly.insert_layer (db::LayerProperties (210, 0)); + dump_map [layer_of (rnsd) ] = ly.insert_layer (db::LayerProperties (211, 0)); + dump_map [layer_of (rpoly) ] = ly.insert_layer (db::LayerProperties (203, 0)); + dump_map [layer_of (rdiff_cont)] = ly.insert_layer (db::LayerProperties (204, 0)); + dump_map [layer_of (rpoly_cont)] = ly.insert_layer (db::LayerProperties (205, 0)); + dump_map [layer_of (rmetal1) ] = ly.insert_layer (db::LayerProperties (206, 0)); + dump_map [layer_of (rvia1) ] = ly.insert_layer (db::LayerProperties (207, 0)); + dump_map [layer_of (rmetal2) ] = ly.insert_layer (db::LayerProperties (208, 0)); + + // write nets to layout + db::CellMapping cm = dss.cell_mapping_to_original (0, &ly, tc.cell_index ()); + dump_nets_to_layout (nl, cl, ly, dump_map, cm, true /*with device cells*/); + + // compare netlist as string + CHECKPOINT (); + db::compare_netlist (_this, nl, + "circuit TOP (VSS=VSS,IN=IN,OUT=OUT,VDD=VDD);\n" + " device PMOS $1 (S=VDD,G=$4,D=OUT) (L=0.4,W=2.3,AS=1.38,AD=1.38,PS=5.8,PD=5.8);\n" + " device PMOS $2 (S=VDD,G=IN,D=$3) (L=0.4,W=2.3,AS=1.38,AD=1.38,PS=5.8,PD=5.8);\n" + " device NMOS $3 (S=VSS,G=$4,D=OUT) (L=0.4,W=4.6,AS=2.185,AD=2.185,PS=8.8,PD=8.8);\n" + " device MIM_CAP $5 (A=$4,B=VSS) (C=2.622e-14,A=26.22,P=29.8);\n" + " device POLY_RES $7 (A=$3,B=$4) (R=750,A=2.4,P=13.6);\n" + " device POLY_RES $9 (A=$4,B=VSS) (R=1825,A=5.84,P=30);\n" + " device NMOS $10 (S=VSS,G=IN,D=$3) (L=0.4,W=3.1,AS=1.86,AD=1.86,PS=7.4,PD=7.4);\n" + "end;\n" + ); + + // compare the collected test data + + std::string au = tl::testsrc (); + au = tl::combine_path (au, "testdata"); + au = tl::combine_path (au, "algo"); + au = tl::combine_path (au, "device_extract_capres_nets.gds"); + + db::compare_layouts (_this, ly, au); +} + diff --git a/testdata/algo/device_extract_capres_nets.gds b/testdata/algo/device_extract_capres_nets.gds new file mode 100644 index 0000000000000000000000000000000000000000..63b4ef0e1179510a13750abe2710061feb4980f0 GIT binary patch literal 14282 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 literal 0 HcmV?d00001 diff --git a/testdata/algo/devices_test.oas b/testdata/algo/devices_test.oas new file mode 100644 index 0000000000000000000000000000000000000000..aa7221ed2736bd1c44acb4eaaeb4f710435305af GIT binary patch literal 1282 zcmd^*&1(}u7{+(@-Q6TR8%>%?+ti9ssS3r~589K0YqG72$tGmCtu2K`=^x-lNoWfx zcxXX10S~DZRPZA7kU|d?dq}`TdTYQ#4lSnGLwXR}l0&KVM$C%slhFzj@|m zx^*R>UJuH;s@p{b{k?rm(ZV#3PIrfH2ITvuu8wMo&r~%-8CAmOgz7WyTDeYpXRSP@ z{R`-Hi@Iq9L#9{p`z4?X1S2eK`T+3N9U!defazie6j}2rVW?0%jQf0a(RH3xH6DUv z2IfLR|0Dn~QbS=u_8RKA0xWf#G`9KFkrC4y)C}Ng>Fg2L-lYDiz`Qm=8w5nX-xAv@ zFV<4{O3Ild76OVP`};wlr6!GGT$RC6Nw14KIRMrMS=AW4atOoP&x~5gJUBV~5tSz+@56*a|%-;#${VovGog7aI6V`&nFQ z!YkM8oO7GW$UaH|Bs^4vj~b`vf^(o6H^_C4jxi<3*YG4tk971{S>B>MW!CZ9XMJWYS>fe>@V;3J