From 9c0123df20a35479c0279ea8251cfd1f52a40396 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 3 Feb 2019 21:34:23 +0100 Subject: [PATCH] Implemented implicit joining of nets with the same label. --- src/db/db/dbHierNetworkProcessor.cc | 65 ++++- src/db/db/dbHierNetworkProcessor.h | 16 +- src/db/db/dbLayoutToNetlist.cc | 4 +- src/db/db/dbLayoutToNetlist.h | 2 +- src/db/db/dbNetlistExtractor.cc | 32 ++- src/db/db/dbNetlistExtractor.h | 2 +- src/db/db/gsiDeclDbLayoutToNetlist.cc | 4 +- src/db/unit_tests/dbNetlistExtractorTests.cc | 244 +++++++++++++++- src/tl/tl/tl.pro | 6 +- src/tl/tl/tlEquivalenceClusters.cc | 30 ++ src/tl/tl/tlEquivalenceClusters.h | 263 ++++++++++++++++++ .../unit_tests/tlEquivalenceClustersTests.cc | 211 ++++++++++++++ src/tl/unit_tests/unit_tests.pro | 3 +- .../algo/device_extract_au1_implicit_nets.gds | Bin 0 -> 9160 bytes .../algo/device_extract_l1_implicit_nets.gds | Bin 0 -> 3276 bytes 15 files changed, 860 insertions(+), 22 deletions(-) create mode 100644 src/tl/tl/tlEquivalenceClusters.cc create mode 100644 src/tl/tl/tlEquivalenceClusters.h create mode 100644 src/tl/unit_tests/tlEquivalenceClustersTests.cc create mode 100644 testdata/algo/device_extract_au1_implicit_nets.gds create mode 100644 testdata/algo/device_extract_l1_implicit_nets.gds diff --git a/src/db/db/dbHierNetworkProcessor.cc b/src/db/db/dbHierNetworkProcessor.cc index 57272b113..452d160fc 100644 --- a/src/db/db/dbHierNetworkProcessor.cc +++ b/src/db/db/dbHierNetworkProcessor.cc @@ -817,7 +817,7 @@ private: template void -local_clusters::build_clusters (const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn) +local_clusters::build_clusters (const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence) { bool report_progress = tl::verbosity () >= 50; static std::string desc = tl::to_string (tr ("Building local clusters")); @@ -836,6 +836,57 @@ local_clusters::build_clusters (const db::Cell &cell, db::ShapeIterator::flag cluster_building_receiver rec (conn); bs.process (rec, 1 /*==touching*/, bc); rec.generate_clusters (*this); + + if (attr_equivalence && attr_equivalence->size () > 0) { + apply_attr_equivalences (*attr_equivalence); + } +} + +template +void +local_clusters::apply_attr_equivalences (const tl::equivalence_clusters &attr_equivalence) +{ + tl::equivalence_clusters eq; + + // collect all local attributes (the ones which are present in attr_equivalence) into "eq" + // and form equivalences for multi-attribute clusters. + for (const_iterator c = begin (); c != end (); ++c) { + typename local_cluster::attr_iterator a0; + for (typename local_cluster::attr_iterator a = c->begin_attr (); a != c->end_attr (); ++a) { + if (attr_equivalence.has_attribute (*a)) { + if (a0 == typename local_cluster::attr_iterator ()) { + a0 = a; + } + eq.same (*a0, *a); + } + } + } + + // apply the equivalences implied by attr_equivalence + eq.apply_equivalences (attr_equivalence); + + // identify the layout clusters joined into one attribute cluster and join them + + std::map::cluster_id_type, std::set > c2c; + + for (const_iterator c = begin (); c != end (); ++c) { + for (typename local_cluster::attr_iterator a = c->begin_attr (); a != c->end_attr (); ++a) { + tl::equivalence_clusters::cluster_id_type cl = attr_equivalence.cluster_id (*a); + if (cl > 0) { + c2c [cl].insert (c->id ()); + } + } + } + + for (std::map::cluster_id_type, std::set >::const_iterator c = c2c.begin (); c != c2c.end (); ++c) { + if (c->second.size () > 1) { + std::set::const_iterator cl0 = c->second.begin (); + std::set::const_iterator cl = cl0; + while (++cl != c->second.end ()) { + join_cluster_with (*cl0, *cl); + } + } + } } // explicit instantiations @@ -992,11 +1043,11 @@ void hier_clusters::clear () template void -hier_clusters::build (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn) +hier_clusters::build (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence) { clear (); cell_clusters_box_converter cbc (layout, *this); - do_build (cbc, layout, cell, shape_flags, conn); + do_build (cbc, layout, cell, shape_flags, conn, attr_equivalence); } namespace @@ -1649,7 +1700,7 @@ hier_clusters::make_path (const db::Layout &layout, const db::Cell &cell, siz template void -hier_clusters::do_build (cell_clusters_box_converter &cbc, const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn) +hier_clusters::do_build (cell_clusters_box_converter &cbc, const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence) { tl::SelfTimer timer (tl::verbosity () >= 21, tl::to_string (tr ("Computing shape clusters"))); @@ -1664,7 +1715,7 @@ hier_clusters::do_build (cell_clusters_box_converter &cbc, const db::Layou tl::RelativeProgress progress (tl::to_string (tr ("Computing local clusters")), called.size (), 1); for (std::set::const_iterator c = called.begin (); c != called.end (); ++c) { - build_local_cluster (layout, layout.cell (*c), shape_flags, conn); + build_local_cluster (layout, layout.cell (*c), shape_flags, conn, attr_equivalence); ++progress; } } @@ -1707,7 +1758,7 @@ hier_clusters::do_build (cell_clusters_box_converter &cbc, const db::Layou template void -hier_clusters::build_local_cluster (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn) +hier_clusters::build_local_cluster (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence) { std::string msg = tl::to_string (tr ("Computing local clusters for cell: ")) + std::string (layout.cell_name (cell.cell_index ())); if (tl::verbosity () >= 40) { @@ -1716,7 +1767,7 @@ hier_clusters::build_local_cluster (const db::Layout &layout, const db::Cell tl::SelfTimer timer (tl::verbosity () >= 41, msg); connected_clusters &local = m_per_cell_clusters [cell.cell_index ()]; - local.build_clusters (cell, shape_flags, conn); + local.build_clusters (cell, shape_flags, conn, attr_equivalence); } template diff --git a/src/db/db/dbHierNetworkProcessor.h b/src/db/db/dbHierNetworkProcessor.h index 46dc00f54..aed77cd28 100644 --- a/src/db/db/dbHierNetworkProcessor.h +++ b/src/db/db/dbHierNetworkProcessor.h @@ -30,6 +30,7 @@ #include "dbBoxTree.h" #include "dbCell.h" #include "dbInstElement.h" +#include "tlEquivalenceClusters.h" #include #include @@ -463,8 +464,13 @@ public: * This method will only build the local clusters. Child cells * are not taken into account. Only the shape types listed in * shape_flags are taken. + * + * If attr_equivalence is non-null, all clusters with attributes + * listed as equivalent in this object are joined. Additional + * cluster joining may happen in this case, because multi-attribute + * assignment might create connections too. */ - void build_clusters (const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn); + void build_clusters (const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence = 0); /** * @brief Creates and inserts a new clusters @@ -490,6 +496,8 @@ private: box_type m_bbox; tree_type m_clusters; size_t m_next_dummy_id; + + void apply_attr_equivalences (const tl::equivalence_clusters &attr_equivalence); }; /** @@ -740,7 +748,7 @@ public: /** * @brief Builds a hierarchy of clusters from a cell hierarchy and given connectivity */ - void build (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn); + void build (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence = 0); /** * @brief Gets the connected clusters for a given cell @@ -778,10 +786,10 @@ public: ClusterInstance make_path (const db::Layout &layout, const db::Cell &cell, size_t id, const std::vector &path); private: - void build_local_cluster (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn); + void build_local_cluster (const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence); void build_hier_connections (cell_clusters_box_converter &cbc, const db::Layout &layout, const db::Cell &cell, const db::Connectivity &conn); void build_hier_connections_for_cells (cell_clusters_box_converter &cbc, const db::Layout &layout, const std::vector &cells, const db::Connectivity &conn, tl::RelativeProgress &progress); - void do_build (cell_clusters_box_converter &cbc, const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn); + void do_build (cell_clusters_box_converter &cbc, const db::Layout &layout, const db::Cell &cell, db::ShapeIterator::flags_type shape_flags, const db::Connectivity &conn, const tl::equivalence_clusters *attr_equivalence = 0); std::map > m_per_cell_clusters; }; diff --git a/src/db/db/dbLayoutToNetlist.cc b/src/db/db/dbLayoutToNetlist.cc index d0955b5ad..556cb93dc 100644 --- a/src/db/db/dbLayoutToNetlist.cc +++ b/src/db/db/dbLayoutToNetlist.cc @@ -226,7 +226,7 @@ size_t LayoutToNetlist::global_net_id (const std::string &name) return m_conn.global_net_id (name); } -void LayoutToNetlist::extract_netlist () +void LayoutToNetlist::extract_netlist (bool join_nets_by_label) { if (m_netlist_extracted) { throw tl::Exception (tl::to_string (tr ("The netlist has already been extracted"))); @@ -236,7 +236,7 @@ void LayoutToNetlist::extract_netlist () } db::NetlistExtractor netex; - netex.extract_nets(m_dss, m_conn, *mp_netlist, m_net_clusters); + netex.extract_nets(m_dss, m_conn, *mp_netlist, m_net_clusters, join_nets_by_label); m_netlist_extracted = true; } diff --git a/src/db/db/dbLayoutToNetlist.h b/src/db/db/dbLayoutToNetlist.h index 4379fb202..30a46063d 100644 --- a/src/db/db/dbLayoutToNetlist.h +++ b/src/db/db/dbLayoutToNetlist.h @@ -269,7 +269,7 @@ public: * @brief Runs the netlist extraction * See the class description for more details. */ - void extract_netlist (); + void extract_netlist (bool join_nets_by_label = true); /** * @brief Marks the netlist as extracted diff --git a/src/db/db/dbNetlistExtractor.cc b/src/db/db/dbNetlistExtractor.cc index b1227f916..fc3d4f836 100644 --- a/src/db/db/dbNetlistExtractor.cc +++ b/src/db/db/dbNetlistExtractor.cc @@ -33,8 +33,32 @@ NetlistExtractor::NetlistExtractor () // .. nothing yet .. } +static void +build_net_name_equivalence (const db::Layout *layout, db::property_names_id_type net_name_id, tl::equivalence_clusters &eq) +{ + std::map > prop_by_name; + + for (db::PropertiesRepository::iterator i = layout->properties_repository ().begin (); i != layout->properties_repository ().end (); ++i) { + for (db::PropertiesRepository::properties_set::const_iterator p = i->second.begin (); p != i->second.end (); ++p) { + if (p->first == net_name_id) { + std::string nn = p->second.to_string (); + prop_by_name [nn].insert (i->first); + } + } + } + + for (std::map >::const_iterator pn = prop_by_name.begin (); pn != prop_by_name.end (); ++pn) { + std::set::const_iterator p = pn->second.begin (); + std::set::const_iterator p0 = p; + while (p != pn->second.end ()) { + eq.same (*p0, *p); + ++p; + } + } +} + void -NetlistExtractor::extract_nets (const db::DeepShapeStore &dss, const db::Connectivity &conn, db::Netlist &nl, hier_clusters_type &clusters) +NetlistExtractor::extract_nets (const db::DeepShapeStore &dss, const db::Connectivity &conn, db::Netlist &nl, hier_clusters_type &clusters, bool join_nets_by_label) { mp_clusters = &clusters; mp_layout = &dss.const_layout (); @@ -52,7 +76,11 @@ NetlistExtractor::extract_nets (const db::DeepShapeStore &dss, const db::Connect // the big part: actually extract the nets - mp_clusters->build (*mp_layout, *mp_cell, db::ShapeIterator::Polygons, conn); + tl::equivalence_clusters net_name_equivalence; + if (m_text_annot_name_id.first && join_nets_by_label) { + build_net_name_equivalence (mp_layout, m_text_annot_name_id.second, net_name_equivalence); + } + mp_clusters->build (*mp_layout, *mp_cell, db::ShapeIterator::Polygons, conn, &net_name_equivalence); // reverse lookup for Circuit vs. cell index std::map circuits; diff --git a/src/db/db/dbNetlistExtractor.h b/src/db/db/dbNetlistExtractor.h index 3189eefec..c76c6c75f 100644 --- a/src/db/db/dbNetlistExtractor.h +++ b/src/db/db/dbNetlistExtractor.h @@ -82,7 +82,7 @@ public: * @brief Extract the nets * See the class description for more details. */ - void extract_nets (const db::DeepShapeStore &dss, const db::Connectivity &conn, db::Netlist &nl, hier_clusters_type &clusters); + void extract_nets (const db::DeepShapeStore &dss, const db::Connectivity &conn, db::Netlist &nl, hier_clusters_type &clusters, bool join_nets_by_label = true); private: hier_clusters_type *mp_clusters; diff --git a/src/db/db/gsiDeclDbLayoutToNetlist.cc b/src/db/db/gsiDeclDbLayoutToNetlist.cc index 7fc2e642b..153b6601b 100644 --- a/src/db/db/gsiDeclDbLayoutToNetlist.cc +++ b/src/db/db/gsiDeclDbLayoutToNetlist.cc @@ -213,8 +213,10 @@ Class decl_dbLayoutToNetlist ("db", "LayoutToNetlist", gsi::method ("global_net_name", &db::LayoutToNetlist::global_net_name, gsi::arg ("global_net_id"), "@brief Gets the global net name for the given global net ID." ) + - gsi::method ("extract_netlist", &db::LayoutToNetlist::extract_netlist, + gsi::method ("extract_netlist", &db::LayoutToNetlist::extract_netlist, gsi::arg ("join_nets_by_label", true), "@brief Runs the netlist extraction\n" + "If join_nets_by_label is true, nets on the same hierarchy level carrying the same label will be connected " + "implicitly even if there is no physical connection.\n" "See the class description for more details.\n" ) + gsi::method_ext ("internal_layout", &l2n_internal_layout, diff --git a/src/db/unit_tests/dbNetlistExtractorTests.cc b/src/db/unit_tests/dbNetlistExtractorTests.cc index aaaa44a27..3b03ea926 100644 --- a/src/db/unit_tests/dbNetlistExtractorTests.cc +++ b/src/db/unit_tests/dbNetlistExtractorTests.cc @@ -465,7 +465,8 @@ TEST(2_DeviceAndNetExtractionFlat) // extract the nets - net_ex.extract_nets (dss, conn, nl, cl); + // don't use "join_nets_by_label" because the flattened texts will spoil everything + net_ex.extract_nets (dss, conn, nl, cl, false); // debug layers produced for nets // 202/0 -> Active @@ -547,3 +548,244 @@ TEST(2_DeviceAndNetExtractionFlat) db::compare_layouts (_this, ly, au); } + +static bool +all_net_names_unique (const db::Circuit &c) +{ + std::set names; + for (db::Circuit::const_net_iterator n = c.begin_nets (); n != c.end_nets (); ++n) { + if (! n->name ().empty ()) { + if (names.find (n->name ()) != names.end ()) { + return false; + } else { + names.insert (n->name ()); + } + } + } + return true; +} + +static bool +all_net_names_unique (const db::Netlist &nl) +{ + for (db::Netlist::const_circuit_iterator c = nl.begin_circuits (); c != nl.end_circuits (); ++c) { + if (! all_net_names_unique (*c)) { + return false; + } + } + return true; +} + +TEST(3_DeviceAndNetExtractionWithImplicitConnections) +{ + 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); + + { + 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, "device_extract_l1_implicit_nets.gds"); + + 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 (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); + + // derived regions + + 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; + + // 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 + + 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); + + // perform the extraction + + db::Netlist nl; + db::hier_clusters cl; + + db::NetlistDeviceExtractorMOS3Transistor pmos_ex ("PMOS"); + db::NetlistDeviceExtractorMOS3Transistor nmos_ex ("NMOS"); + + db::NetlistDeviceExtractor::input_layers dl; + + dl["SD"] = &rpsd; + dl["G"] = &rpgate; + dl["P"] = &rpoly; // not needed for extraction but to return terminal shapes + pmos_ex.extract (dss, dl, nl, cl); + + dl["SD"] = &rnsd; + dl["G"] = &rngate; + dl["P"] = &rpoly; // not needed for extraction but to return terminal shapes + nmos_ex.extract (dss, 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, conn, nl, cl); + + 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); + + // compare netlist as string + EXPECT_EQ (nl.to_string (), + "Circuit RINGO ():\n" + " XINV2 $1 (IN=$I8,$2=FB,OUT=OSC,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $2 (IN=FB,$2=$I38,OUT=$I19,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $3 (IN=NEXT,$2=$I43,OUT=$I5,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $4 (IN=$I3,$2=$I42,OUT=NEXT,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $5 (IN=$I5,$2=$I44,OUT=$I6,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $6 (IN=$I6,$2=$I45,OUT=$I7,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $7 (IN=$I7,$2=$I46,OUT=$I8,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $8 (IN=$I19,$2=$I39,OUT=$I1,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $9 (IN=$I1,$2=$I40,OUT=$I2,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $10 (IN=$I2,$2=$I41,OUT=$I3,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + "Circuit INV2 (IN=IN,$2=$2,OUT=OUT,$4=$4,$5=$5):\n" + " DPMOS $1 (S=$2,G=IN,D=$5) [L=0.25,W=0.95,AS=0.49875,AD=0.26125,PS=2.95,PD=1.5]\n" + " DPMOS $2 (S=$5,G=$2,D=OUT) [L=0.25,W=0.95,AS=0.26125,AD=0.49875,PS=1.5,PD=2.95]\n" + " DNMOS $3 (S=$2,G=IN,D=$4) [L=0.25,W=0.95,AS=0.49875,AD=0.26125,PS=2.95,PD=1.5]\n" + " DNMOS $4 (S=$4,G=$2,D=OUT) [L=0.25,W=0.95,AS=0.26125,AD=0.49875,PS=1.5,PD=2.95]\n" + " XTRANS $1 ($1=$2,$2=$4,$3=IN)\n" + " XTRANS $2 ($1=$2,$2=$5,$3=IN)\n" + " XTRANS $3 ($1=$5,$2=OUT,$3=$2)\n" + " XTRANS $4 ($1=$4,$2=OUT,$3=$2)\n" + "Circuit TRANS ($1=$1,$2=$2,$3=$3):\n" + ); + + // doesn't do anything here, but we test that this does not destroy anything: + nl.combine_devices (); + + // make pins for named nets of top-level circuits - this way they are not purged + nl.make_top_level_pins (); + nl.purge (); + + // compare netlist as string + EXPECT_EQ (nl.to_string (), + "Circuit RINGO (FB=FB,OSC=OSC,NEXT=NEXT,'VSSZ,VSS'='VSSZ,VSS','VDDZ,VDD'='VDDZ,VDD'):\n" + " XINV2 $1 (IN=$I8,$2=FB,OUT=OSC,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $2 (IN=FB,$2=(null),OUT=$I19,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $3 (IN=NEXT,$2=(null),OUT=$I5,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $4 (IN=$I3,$2=(null),OUT=NEXT,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $5 (IN=$I5,$2=(null),OUT=$I6,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $6 (IN=$I6,$2=(null),OUT=$I7,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $7 (IN=$I7,$2=(null),OUT=$I8,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $8 (IN=$I19,$2=(null),OUT=$I1,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $9 (IN=$I1,$2=(null),OUT=$I2,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + " XINV2 $10 (IN=$I2,$2=(null),OUT=$I3,$4='VSSZ,VSS',$5='VDDZ,VDD')\n" + "Circuit INV2 (IN=IN,$2=$2,OUT=OUT,$4=$4,$5=$5):\n" + " DPMOS $1 (S=$2,G=IN,D=$5) [L=0.25,W=0.95,AS=0.49875,AD=0.26125,PS=2.95,PD=1.5]\n" + " DPMOS $2 (S=$5,G=$2,D=OUT) [L=0.25,W=0.95,AS=0.26125,AD=0.49875,PS=1.5,PD=2.95]\n" + " DNMOS $3 (S=$2,G=IN,D=$4) [L=0.25,W=0.95,AS=0.49875,AD=0.26125,PS=2.95,PD=1.5]\n" + " DNMOS $4 (S=$4,G=$2,D=OUT) [L=0.25,W=0.95,AS=0.26125,AD=0.49875,PS=1.5,PD=2.95]\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_au1_implicit_nets.gds"); + + db::compare_layouts (_this, ly, au); +} + diff --git a/src/tl/tl/tl.pro b/src/tl/tl/tl.pro index 850bced12..95deceb8c 100644 --- a/src/tl/tl/tl.pro +++ b/src/tl/tl/tl.pro @@ -43,7 +43,8 @@ SOURCES = \ tlUri.cc \ tlLongInt.cc \ tlUniqueId.cc \ - tlList.cc + tlList.cc \ + tlEquivalenceClusters.cc HEADERS = \ tlAlgorithm.h \ @@ -96,7 +97,8 @@ HEADERS = \ tlUri.h \ tlLongInt.h \ tlUniqueId.h \ - tlList.h + tlList.h \ + tlEquivalenceClusters.h equals(HAVE_CURL, "1") { diff --git a/src/tl/tl/tlEquivalenceClusters.cc b/src/tl/tl/tlEquivalenceClusters.cc new file mode 100644 index 000000000..519d6aa40 --- /dev/null +++ b/src/tl/tl/tlEquivalenceClusters.cc @@ -0,0 +1,30 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2019 Matthias Koefferlein + + 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. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + +#include "tlEquivalenceClusters.h" + +namespace tl +{ + + // .. nothing yet .. + +} diff --git a/src/tl/tl/tlEquivalenceClusters.h b/src/tl/tl/tlEquivalenceClusters.h new file mode 100644 index 000000000..5d4b23d4c --- /dev/null +++ b/src/tl/tl/tlEquivalenceClusters.h @@ -0,0 +1,263 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2019 Matthias Koefferlein + + 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. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#ifndef HDR_tlEquivalenceClusters +#define HDR_tlEquivalenceClusters + +#include "tlCommon.h" +#include "tlAssert.h" + +#include +#include +#include +#include + +namespace tl +{ + +/** + * @brief A utility class forming clusters based on equivalence of a certain attribute + * + * To use this class, feed it with equivalences using "is_same", e.g. + * + * @code + * equivalence_clusters eq; + * // forms two clusters: 1,2,5 and 3,4 + * eq.same (1, 2); + * eq.same (3, 4); + * eq.same (1, 5); + * @endcode + * + * Self-equivalence is a way of introduction an attribute without + * an equivalence: + * + * @code + * equivalence_clusters eq; + * // after this, 1 is a known attribute + * eq.same (1, 1); + * eq.has_attribute (1); // ->true + * @endcode + * + * Equivalence clusters can be merged, forming new and bigger clusters. + * + * Eventually, each cluster is represented by a non-zero integer ID. + * The cluster ID can obtained per attribute value using "cluster_id". + * In the above example this will be: + * + * @code + * eq.cluster_id (1); // ->1 + * eq.cluster_id (2); // ->1 + * eq.cluster_id (3); // ->2 + * eq.cluster_id (4); // ->2 + * eq.cluster_id (5); // ->1 + * eq.cluster_id (6); // ->0 (unknown) + * @endcode + * + * "size" will give the maximum cluster ID. + */ +template +class equivalence_clusters +{ +public: + typedef size_t cluster_id_type; + typedef T attribute_type; + typedef typename std::vector::iterator>::const_iterator cluster_iterator; + + /** + * @brief Creates an empty equivalence cluster + */ + equivalence_clusters () + { + // .. nothing yet .. + } + + /** + * @brief Makes attr1 and attr2 equivalent + */ + void same (const T &attr1, const T &attr2) + { + cluster_id_type cl1 = cluster_id (attr1); + + if (attr1 == attr2) { + // special case of "self-identity" + if (! cl1) { + cluster_id_type cl = new_cluster (); + insert (attr1, cl); + } + return; + } + + cluster_id_type cl2 = cluster_id (attr2); + if (! cl1 || ! cl2) { + + if (cl1) { + insert (attr2, cl1); + } else if (cl2) { + insert (attr1, cl2); + } else { + cluster_id_type cl = new_cluster (); + insert (attr1, cl); + insert (attr2, cl); + } + + } else if (cl1 != cl2) { + join (cl1, cl2); + } + } + + /** + * @brief Returns true, if attr is part of the equivalence clusters + */ + bool has_attribute (const T &attr) const + { + return m_cluster_id_by_attr.find (attr) != m_cluster_id_by_attr.end (); + } + + /** + * @brief Returns the cluster ID for the given attribute of 0 if the attribute is not assigned to a cluster + */ + cluster_id_type cluster_id (const T &attr) const + { + typename std::map::const_iterator c = m_cluster_id_by_attr.find (attr); + if (c != m_cluster_id_by_attr.end ()) { + return c->second; + } else { + return 0; + } + } + + /** + * @brief Applies the equivalences of the other clusters + * + * This method will join clusters already within this equivalence cluster collection + * based on the equivalences listed in the other clusters collection. + * + * In contrast to merge, his method will not introduce new attributes. + */ + void apply_equivalences (const equivalence_clusters &other) + { + std::vector attrs; + for (typename std::map::const_iterator a = m_cluster_id_by_attr.begin (); a != m_cluster_id_by_attr.end (); ++a) { + if (other.has_attribute (a->first)) { + attrs.push_back (a->first); + } + } + + for (typename std::vector::const_iterator a = attrs.begin (); a != attrs.end (); ++a) { + cluster_id_type cl = other.cluster_id (*a); + cluster_iterator b = other.begin_cluster (cl); + cluster_iterator e = other.end_cluster (cl); + for (cluster_iterator i = b; i != e; ++i) { + if ((*i)->first != *a && has_attribute ((*i)->first)) { + same ((*i)->first, *a); + } + } + } + } + + /** + * @brief Merges the equivalences of the other clusters into this + * + * This method will add all attributes from the other cluster collection and join + * clusters based on the equivalences there. + */ + void merge (const equivalence_clusters &other) + { + for (cluster_id_type cl = 1; cl <= other.size (); ++cl) { + cluster_iterator b = other.begin_cluster (cl); + cluster_iterator e = other.end_cluster (cl); + for (cluster_iterator i = b; i != e; ++i) { + same ((*b)->first, (*i)->first); + } + } + } + + /** + * @brief Returns the number of clusters kept inside this collection + */ + size_t size () const + { + return m_clusters.size (); + } + + /** + * @brief Begin iterator for the cluster elements for a given cluster ID. + * To access, the attribute, use "(*i)->first". + */ + cluster_iterator begin_cluster (size_t cluster_id) const + { + tl_assert (cluster_id > 0); + return m_clusters [cluster_id - 1].begin (); + } + + /** + * @brief End iterator for the cluster elements for a given cluster ID. + */ + cluster_iterator end_cluster (size_t cluster_id) const + { + tl_assert (cluster_id > 0); + return m_clusters [cluster_id - 1].end (); + } + +private: + void insert (const T &attr, cluster_id_type into) + { + typename std::map::iterator c = m_cluster_id_by_attr.insert (std::make_pair (attr, into)).first; + m_clusters [into - 1].push_back (c); + } + + void join (cluster_id_type id, cluster_id_type with_id) + { + tl_assert (id > 0); + tl_assert (with_id > 0); + std::vector::iterator> &cnew = m_clusters [id - 1]; + std::vector::iterator> &c = m_clusters [with_id - 1]; + for (typename std::vector::iterator>::iterator i = c.begin (); i != c.end (); ++i) { + (*i)->second = id; + cnew.push_back (*i); + } + + c.clear (); + m_free_slots.push_back (with_id); + } + + cluster_id_type new_cluster () + { + if (! m_free_slots.empty ()) { + cluster_id_type cl = m_free_slots.back (); + m_free_slots.pop_back (); + return cl; + } else { + m_clusters.push_back (std::vector::iterator> ()); + return m_clusters.size (); + } + } + + std::map m_cluster_id_by_attr; + std::vector::iterator> > m_clusters; + std::vector m_free_slots; +}; + +} + +#endif diff --git a/src/tl/unit_tests/tlEquivalenceClustersTests.cc b/src/tl/unit_tests/tlEquivalenceClustersTests.cc new file mode 100644 index 000000000..5d48edd5c --- /dev/null +++ b/src/tl/unit_tests/tlEquivalenceClustersTests.cc @@ -0,0 +1,211 @@ + +/* + + KLayout Layout Viewer + Copyright (C) 2006-2019 Matthias Koefferlein + + 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. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +*/ + + +#include "tlEquivalenceClusters.h" +#include "tlUnitTest.h" + +// basics +TEST(1_basics) +{ + tl::equivalence_clusters eq; + + eq.same (1, 5); + eq.same (2, 3); + eq.same (5, 4); + + EXPECT_EQ (eq.cluster_id (1), size_t (1)); + EXPECT_EQ (eq.cluster_id (2), size_t (2)); + EXPECT_EQ (eq.cluster_id (3), size_t (2)); + EXPECT_EQ (eq.cluster_id (4), size_t (1)); + EXPECT_EQ (eq.cluster_id (5), size_t (1)); + EXPECT_EQ (eq.cluster_id (6), size_t (0)); + + EXPECT_EQ (eq.size (), size_t (2)); + + eq.same (2, 6); + EXPECT_EQ (eq.cluster_id (6), size_t (2)); + EXPECT_EQ (eq.cluster_id (7), size_t (0)); + + EXPECT_EQ (eq.size (), size_t (2)); + + eq.same (7, 8); + EXPECT_EQ (eq.size (), size_t (3)); + EXPECT_EQ (eq.cluster_id (6), size_t (2)); + EXPECT_EQ (eq.cluster_id (7), size_t (3)); + EXPECT_EQ (eq.cluster_id (8), size_t (3)); +} + +// joining of clusters +TEST(2_join) +{ + tl::equivalence_clusters eq; + + eq.same (1, 2); + eq.same (3, 4); + eq.same (5, 6); + + EXPECT_EQ (eq.cluster_id (1), size_t (1)); + EXPECT_EQ (eq.cluster_id (2), size_t (1)); + EXPECT_EQ (eq.cluster_id (3), size_t (2)); + EXPECT_EQ (eq.cluster_id (4), size_t (2)); + EXPECT_EQ (eq.cluster_id (5), size_t (3)); + EXPECT_EQ (eq.cluster_id (6), size_t (3)); + + eq.same (3, 2); + + EXPECT_EQ (eq.cluster_id (1), size_t (2)); + EXPECT_EQ (eq.cluster_id (2), size_t (2)); + EXPECT_EQ (eq.cluster_id (3), size_t (2)); + EXPECT_EQ (eq.cluster_id (4), size_t (2)); + EXPECT_EQ (eq.cluster_id (5), size_t (3)); + EXPECT_EQ (eq.cluster_id (6), size_t (3)); + + eq.same (4, 5); + + EXPECT_EQ (eq.cluster_id (1), size_t (2)); + EXPECT_EQ (eq.cluster_id (2), size_t (2)); + EXPECT_EQ (eq.cluster_id (3), size_t (2)); + EXPECT_EQ (eq.cluster_id (4), size_t (2)); + EXPECT_EQ (eq.cluster_id (5), size_t (2)); + EXPECT_EQ (eq.cluster_id (6), size_t (2)); + + eq.same (10, 11); + eq.same (12, 13); + + EXPECT_EQ (eq.cluster_id (10), size_t (3)); + EXPECT_EQ (eq.cluster_id (11), size_t (3)); + + EXPECT_EQ (eq.cluster_id (12), size_t (1)); + EXPECT_EQ (eq.cluster_id (13), size_t (1)); +} + +// size +TEST(3_size) +{ + tl::equivalence_clusters eq; + + eq.same (1, 2); + EXPECT_EQ (eq.size (), size_t (1)); + eq.same (2, 4); + EXPECT_EQ (eq.size (), size_t (1)); + eq.same (5, 6); + EXPECT_EQ (eq.size (), size_t (2)); +} + +// has_attribute +TEST(4_has_attribute) +{ + tl::equivalence_clusters eq; + + eq.same (1, 1); + EXPECT_EQ (eq.has_attribute (1), true); + EXPECT_EQ (eq.has_attribute (2), false); + eq.same (1, 2); + EXPECT_EQ (eq.has_attribute (1), true); + EXPECT_EQ (eq.has_attribute (2), true); + EXPECT_EQ (eq.has_attribute (3), false); +} + +std::string eq2string (const tl::equivalence_clusters &eq) +{ + std::string res; + for (size_t c = 1; c <= eq.size (); ++c) { + for (tl::equivalence_clusters::cluster_iterator i = eq.begin_cluster (c); i != eq.end_cluster (c); ++i) { + if (i != eq.begin_cluster (c)) { + res += ","; + } else if (! res.empty ()) { + res += ";"; + } + res += tl::to_string ((*i)->first); + } + } + return res; +} + +// iterator +TEST(5_iterator) +{ + tl::equivalence_clusters eq; + + eq.same (1, 1); + EXPECT_EQ (eq2string (eq), "1"); + + eq.same (1, 2); + EXPECT_EQ (eq2string (eq), "1,2"); + + eq.same (3, 4); + EXPECT_EQ (eq2string (eq), "1,2;3,4"); + + eq.same (10, 11); + EXPECT_EQ (eq2string (eq), "1,2;3,4;10,11"); + + eq.same (1, 10); + EXPECT_EQ (eq2string (eq), "1,2,10,11;3,4"); +} + +// apply_other_equivalences +TEST(6_apply_equivalences) +{ + tl::equivalence_clusters eq; + + eq.same (1, 1); + eq.same (2, 2); + eq.same (3, 4); + eq.same (5, 6); + EXPECT_EQ (eq2string (eq), "1;2;3,4;5,6"); + + tl::equivalence_clusters eq2; + + eq2.same (2, 2); + eq2.same (4, 5); + eq2.same (4, 10); + eq2.same (11, 11); + EXPECT_EQ (eq2string (eq2), "2;4,5,10;11"); + + eq.apply_equivalences (eq2); + EXPECT_EQ (eq2string (eq), "1;2;5,6,3,4"); +} + +// merge +TEST(7_merge) +{ + tl::equivalence_clusters eq; + + eq.same (1, 1); + eq.same (2, 2); + eq.same (3, 4); + eq.same (5, 6); + EXPECT_EQ (eq2string (eq), "1;2;3,4;5,6"); + + tl::equivalence_clusters eq2; + + eq2.same (2, 2); + eq2.same (4, 5); + eq2.same (4, 10); + eq2.same (11, 11); + EXPECT_EQ (eq2string (eq2), "2;4,5,10;11"); + + eq.merge (eq2); + EXPECT_EQ (eq2string (eq), "1;2;3,4,5,6,10;11"); +} + diff --git a/src/tl/unit_tests/unit_tests.pro b/src/tl/unit_tests/unit_tests.pro index bd1680b9f..aa8106e22 100644 --- a/src/tl/unit_tests/unit_tests.pro +++ b/src/tl/unit_tests/unit_tests.pro @@ -36,7 +36,8 @@ SOURCES = \ tlInt128Support.cc \ tlLongInt.cc \ tlUniqueIdTests.cc \ - tlListTests.cc + tlListTests.cc \ + tlEquivalenceClustersTests.cc !equals(HAVE_QT, "0") { diff --git a/testdata/algo/device_extract_au1_implicit_nets.gds b/testdata/algo/device_extract_au1_implicit_nets.gds new file mode 100644 index 0000000000000000000000000000000000000000..29b65f48eb4abce9e43f3248fc984a6b60fa85af GIT binary patch literal 9160 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