Repository: ucb-bar/hammer (language: python)

Issue:
Use special cells for the power straps reference cell
We should use "Special cells" to replace the `technology.core.tap_cell_rail_reference` setting.
Add input ilms to par output_ilms for top-level in hierarchical flow
Top level par should "output" ilms so lvs can see all the schematics
Route obstruction options
https://github.com/ucb-bar/hammer-cadence-plugins/blob/902657b8bdba7aec12726d6d3c85a2639a4030ad/hammer/par/innovus/__init__.py#L1014

This line as is obstructs both routing and pg_nets. Is that the desired behavior? 

If we want to obstruct only routing but not pg_nets, we need the flag `except_pg_nets`

Use case: connecting a macro to power grid, but block routing over it to avoid unpredictable coupling effects.


Discussion / hints:
Is there a possibility that a technology may supply multiple tap cells?
I think the lvs.inputs.ilms approach is better. I'll fix this soon
I agree. Let's PR this @harrisonliew ?
As a note, this would require either (a) changing the default behavior or (b) adding another option to obstructions (e.g. route_only?)

Fix patch:
diff --git a/doc/Hammer-Basics/Migration-Guide.md b/doc/Hammer-Basics/Migration-Guide.md
index 76fc78b56..ca9e8416c 100644
--- a/doc/Hammer-Basics/Migration-Guide.md
+++ b/doc/Hammer-Basics/Migration-Guide.md
@@ -28,7 +28,7 @@ vlsi.core.technology: hammer.technology.<tech_name>
 vlsi.core.synthesis_tool: hammer.synthesis.<syn_tool_name>
 ```
 
-See the [](plugin_package) section below for details about plugin package structure.
+The package name is defined by the file structure of the plugin package. See the [](plugin_package) section below for details.
 
 ## `import` Statements
 
@@ -115,7 +115,7 @@ mytech/
     layer.map         # some tech-specific file
 action/
     action_tool/
-        __init__.py   # contains action_tool-specific hooks
+        __init__.py   # contains action_tool-specific hooks for mytech technology
         tool.options  # some tech-specific file needed by this tool
 ```
 
@@ -130,10 +130,12 @@ hammer/
         layer.map         # some tech-specific file
         action/
             action_tool/
-                __init__.py   # contains action_tool-specific hooks
+                __init__.py   # contains action_tool-specific hooks for mytech technology
                 tool.options  # some tech-specific file needed for this tool
 ```
 
+In this case, the technology package name is `hammer.mytech` (note that standalone plugin repositories may not be `hammer.technology.mytech`, unlike the ones included in the main Hammer repository).
+
 ## Resource Files
 
 Technology plugins will often provide special static files needed by tools, such as the `layer.map` file in the above example. If this file is already specified with the `prependlocal` meta action, the behavior will remain the same:
diff --git a/doc/Hammer-Use/Buildfile.rst b/doc/Hammer-Use/Buildfile.rst
index 05248bd22..79c7a5552 100644
--- a/doc/Hammer-Use/Buildfile.rst
+++ b/doc/Hammer-Use/Buildfile.rst
@@ -18,7 +18,7 @@ For hierarchical designs, Hammer will use the hierarchy to build a dependency gr
 ``hammer.d`` should be included in a higher-level Makefile.
 While ``hammer.d`` defines all of the variables that it needs, there are often reasons to set these elsewhere.
 Because ``hammer.d`` uses ``?=`` assignment, the settings created in the top-level Makefile will persist.
-An example of this setup is found in `Chipyard <https://github.com/ucb-bar/chipyard/blob/master/vlsi/Makefile>`__.
+An example of this setup is found in `Chipyard <https://github.com/ucb-bar/chipyard/blob/main/vlsi/Makefile>`__.
 
 
 To enable interactive usage, ``hammer.d`` also contains a set of companion "redo" targets (e.g. ``redo-par`` and ``redo-syn``).
diff --git a/doc/Hammer-Use/Hammer-Config.rst b/doc/Hammer-Use/Hammer-Config.rst
index 6a12e8e5f..d509b6777 100644
--- a/doc/Hammer-Use/Hammer-Config.rst
+++ b/doc/Hammer-Use/Hammer-Config.rst
@@ -96,6 +96,7 @@ Common meta actions
 -------------------
 
 * ``append``: append the elements provided to the base list. (See the above ``vlsi.tech.foobar65.bad_cells`` example.)
+* ``prepend``: prepend the elements provided to the base list. Useful where list order matters, but make sure the file containing this directive is at the end of the list of files passed to ``hammer-vlsi`` with ``-p``.
 * ``subst``: substitute variables into a string.
 
   Base:
@@ -161,6 +162,8 @@ Common meta actions
 
   Result: ``get_setting("foo.flash")`` returns ``noman``
 
+  In general, putting ``lazy`` in front of all other directives will defer the meta processing until the very end.
+
 * ``crossref`` - directly reference another setting. Example:
 
   Level 1:
@@ -178,6 +181,8 @@ Common meta actions
 
   Result: ``get_setting("foo.mob")`` returns ``yes``
 
+  You can also append/prepend instead of substitute using ``crossappendref`` and ``crossprependref``.
+
 * ``transclude`` - transclude the given path. Example:
 
   Level 1:
diff --git a/doc/Hammer-Use/Hooks.rst b/doc/Hammer-Use/Hooks.rst
index db22d8602..bb53ea4dc 100644
--- a/doc/Hammer-Use/Hooks.rst
+++ b/doc/Hammer-Use/Hooks.rst
@@ -16,8 +16,8 @@ Hook methods need to take in an instance of a particular ``HammerTool``, which p
 Hooks (depending on how they are included, see below) get turned into step objects that can be targeted with ``--from/after/to/until_step`` and other hooks.
 
 Hooks can live in a Python file inside the design root so that it is available to the class that needs to extend the default ``CLIDriver``.
-An example of some skeletons of hooks are found in `Chipyard <https://github.com/ucb-bar/chipyard/blob/master/vlsi/example-vlsi>`__.
-For more comprehensive examples, refer to the hooks unit tests in the ``HammerToolHooksTest`` class of `test.py <https://github.com/ucb-bar/hammer/blob/master/src/hammer-vlsi/test.py>`__.
+An example of some skeletons of hooks are found in `Chipyard <https://github.com/ucb-bar/chipyard/blob/main/vlsi/example-vlsi>`__.
+For more comprehensive examples, refer to the hooks unit tests in the ``TestHammerToolHooks`` class of `test_hooks.py <https://github.com/ucb-bar/hammer/blob/master/tests/test_hooks.py>`__.
 
 Including Hooks
 ----------------
diff --git a/hammer/config/config_src.py b/hammer/config/config_src.py
index 99a3fadb9..00af47c55 100644
--- a/hammer/config/config_src.py
+++ b/hammer/config/config_src.py
@@ -228,6 +228,94 @@ def replace_if_target_setting(setting: str) -> str:
                                                  target_settings=crossappendref_targets,
                                                  rename_target=crossappendref_rename)
 
+    def prepend_action(config_dict: dict, key: str, value: Any) -> None:
+        if key not in config_dict:
+            config_dict[key] = []
+
+        if not isinstance(config_dict[key], list):
+            raise ValueError(f"Trying to prepend to non-list setting {key}")
+        if not isinstance(value, list):
+            raise ValueError(f"Trying to prepend to list {key} with non-list {value}")
+        config_dict[key] = value + config_dict[key]
+
+    def prepend_rename(key: str, value: Any, target_setting: str, replacement_setting: str) -> Optional[Tuple[Any, str]]:
+        return [replacement_setting, value], "crossprepend"
+
+    # prepend depends only on itself
+    directives['prepend'] = MetaDirective(action=prepend_action,
+                                         target_settings=lambda key, value: [key],
+                                         rename_target=prepend_rename)
+
+    def crossprepend_decode(value: Any) -> Tuple[str, list]:
+        assert isinstance(value, list), "crossprepend takes a list of two elements"
+        assert len(value) == 2, "crossprepend takes a list of two elements"
+        target_setting = value[0]  # type: str
+        prepend_value = value[1]  # type: list
+        assert isinstance(target_setting, str), "crossprepend target setting must be a string"
+        assert isinstance(prepend_value, list), "crossprepend must prepend a list"
+        return target_setting, prepend_value
+
+    # crossprepend takes a list that has two elements.
+    # The first is the target list (the list to prepend to), and the second is
+    # a list to prepend to the target list.
+    # e.g. if base has ["1"] and crossprepend has ["base", ["2", "3"]], then
+    # the result will be ["2", "3", "1"].
+    def crossprepend_action(config_dict: dict, key: str, value: Any) -> None:
+        target_setting, prepend_value = crossprepend_decode(value)
+        config_dict[key] = prepend_value + config_dict[target_setting]
+
+    def crossprepend_targets(key: str, value: Any) -> List[str]:
+        target_setting, prepend_value = crossprepend_decode(value)
+        return [target_setting]
+
+    def crossprepend_rename(key: str, value: Any, target_setting: str, replacement_setting: str) -> Optional[
+        Tuple[Any, str]]:
+        crossprepend_target, prepend_value = crossprepend_decode(value)
+        return [replacement_setting if crossprepend_target == target_setting else crossprepend_target,
+                prepend_value], "crossprepend"
+
+    directives['crossprepend'] = MetaDirective(action=crossprepend_action,
+                                              target_settings=crossprepend_targets,
+                                              rename_target=crossprepend_rename)
+
+    def crossprependref_decode(value: Any) -> Tuple[str, str]:
+        assert isinstance(value, list), "crossprependref takes a list of two elements"
+        assert len(value) == 2, "crossprependref takes a list of two elements"
+        target_key = value[0]  # type: str
+        prepend_key = value[1]  # type: str
+        assert isinstance(target_key, str), "crossprependref target setting must be a string"
+        assert isinstance(prepend_key, str), "crossprepend prepend list setting must be a string"
+        return target_key, prepend_key
+
+    # crossprependref takes a list that has two elements.
+    # The first is the target list (the list to prepend to), and the second is
+    # a setting that contains a list to prepend.
+    # e.g. if base has ["1"], app has ["2", "3"], and crossprepend has ["base", "app"], the result
+    # is ["2", "3", "1"].
+    def crossprependref_action(config_dict: dict, key: str, value: Any) -> None:
+        target_setting, prepend_setting = crossprependref_decode(value)
+        config_dict[key] = config_dict[prepend_setting] + config_dict[target_setting]
+
+    def crossprependref_targets(key: str, value: Any) -> List[str]:
+        target_setting, prepend_setting = crossprependref_decode(value)
+        return [target_setting, prepend_setting]
+
+    def crossprependref_rename(key: str, value: Any, target_setting: str, replacement_setting: str) -> Optional[
+        Tuple[Any, str]]:
+        target, prepend = crossprependref_decode(value)
+
+        def replace_if_target_setting(setting: str) -> str:
+            """Helper function to replace the given setting with the
+            replacement if it is equal to target_setting."""
+            return replacement_setting if setting == target_setting else setting
+
+        return [replace_if_target_setting(target),
+                replace_if_target_setting(prepend)], "crossprependref"
+
+    directives['crossprependref'] = MetaDirective(action=crossprependref_action,
+                                                 target_settings=crossprependref_targets,
+                                                 rename_target=crossprependref_rename)
+
     def subst_str(input_str: str, replacement_func: Callable[[str], str]) -> str:
         """Substitute ${...}"""
         return re.sub(__VARIABLE_EXPANSION_REGEX, lambda x: replacement_func(x.group(1)), input_str)
@@ -777,7 +865,7 @@ def get_setting_suffix(self, key: str, suffix: str, nullvalue: Any = None, check
         default  = key
         override = default + "_" + suffix
         value = None
-        try: 
+        try:
             value = self.get_config()[override]
         except:
             try:
@@ -901,7 +989,7 @@ def get_settings_from_dict(self, key_default_dict: Dict[str, Any], key_prefix: s
 
         opt_dict={}
         for key, default_value in key_default_dict.items():
-            try: 
+            try:
                 if not key_prefix:
                     extracted_value = self.get_setting(f"{key}", default_value)
                 else:
@@ -910,7 +998,7 @@ def get_settings_from_dict(self, key_default_dict: Dict[str, Any], key_prefix: s
             except KeyError:
                 if key not in optional_keys:
                     raise ValueError(f"Missing a mandatory requested key: {key_prefix}.{key}")
-                else: 
+                else:
                     opt_dict[key] = None
 
         return opt_dict
diff --git a/hammer/config/defaults.yml b/hammer/config/defaults.yml
index f1c02159c..7db0b33b8 100644
--- a/hammer/config/defaults.yml
+++ b/hammer/config/defaults.yml
@@ -66,6 +66,9 @@ vlsi.technology:
 
   tap_cell_offset: 0.0 # Offset of the first column of tape cells from the edge of the floorplan (float)
   # Should be overridden by technology plugin defaults.
+  
+  routing_layers: null # If specified, set/override the [bottom, top] layers used for routing. (Optional[Tuple[int, int]])
+  # Both must match the index number (not name) of layers used in the stackup.
 
   extra_libraries: [] # Extra semiconductor IP libraries/stdcell libs/hard macros. (List[ExtraLibrary])
   # Used for non-technology-vendor-provided custom IP blocks.
@@ -643,8 +646,6 @@ lvs.inputs:
 
   schematic_files: [] # Input list of schematic files
 
-  ilms: [] # Input list of ILMStructs
-
   hcells_list: [] # Input list of LVS Hcells
 
   additional_lvs_text_mode: "auto" # Valid modes are auto, manual, prepend, and append
@@ -685,9 +686,6 @@ technology.core:
     std_cell_rail_layer: null # This should specify the TOPMOST metal layer the standard cells use for power rails
     # Note that this is not usually stackup specific; it is based on the std cell library
     # type: str
-    tap_cell_rail_reference: null # This is used to provide a reference master for generating standard cell rails
-    # Can be a wildcard/glob
-    # type: str
     std_cell_supplies:
     # Names of the power and ground rails of the standard cells
         power: ["VDD"]
diff --git a/hammer/config/defaults_types.yml b/hammer/config/defaults_types.yml
index 2828d0ef1..0f2e76dd0 100644
--- a/hammer/config/defaults_types.yml
+++ b/hammer/config/defaults_types.yml
@@ -60,6 +60,9 @@ vlsi.technology:
   # type: float
   tap_cell_offset: float 
 
+  # Set the [bottom, top] layer used for routing. (Optional[Tuple[int, int]])
+  routing_layers: Optional[list[int]]
+
   # Extra semiconductor IP libraries/stdcell libs/hard macros. (List[ExtraLibrary])
   # ExtraLibrary is serialized as a list[dict[str, str]]
   # TODO: Extralibrary does not have a clean JSON serialization as of late
@@ -356,9 +359,6 @@ lvs.inputs:
   # Input list of schematic files
   schematic_files: list[str]
 
-  # Input list of ILMStructs
-  ilms: list[dict[str, Any]]
-
   # Input list of Hcells
   hcells_list: list[str]
 
@@ -397,9 +397,6 @@ technology.core:
     # This should specify the TOPMOST metal layer the standard cells use for power rails
     # type: str
     std_cell_rail_layer: Optional[str]
-    # This is used to provide a reference master for generating standard cell rails
-    # type: str
-    tap_cell_rail_reference: Optional[str]
 
     # Name of standard cell power/ground rails
     std_cell_supplies:
diff --git a/hammer/generate_properties.py b/hammer/generate_properties.py
index ccd769ccc..df8f29907 100755
--- a/hammer/generate_properties.py
+++ b/hammer/generate_properties.py
@@ -184,9 +184,7 @@ def main(args) -> int:
                                   InterfaceVar("schematic_files", "List[str]",
                                                "path to the input SPICE or Verilog schematic files (e.g. *.v or *.spi)"),
                                   InterfaceVar("hcells_list", "List[str]",
-                                               "list of cells to explicitly map hierarchically in LVS"),
-                                  InterfaceVar("ilms", "List[ILMStruct]",
-                                               "list of (optional) input ILM information for hierarchical mode")
+                                               "list of cells to explicitly map hierarchically in LVS")
                               ],
                               outputs=[]
                               )
diff --git a/hammer/lvs/icv/__init__.py b/hammer/lvs/icv/__init__.py
index 18f8f4b80..bab2dc594 100644
--- a/hammer/lvs/icv/__init__.py
+++ b/hammer/lvs/icv/__init__.py
@@ -123,7 +123,7 @@ def generate_hcells_file(self) -> None:
 
     def generate_top_icv_file(self) -> None:
         library_spice_files = self.technology.read_libs([hammer_tech.filters.spice_filter], hammer_tech.HammerTechnologyUtils.to_plain_item)
-        ilms = list(map(lambda x: x.netlist, self.ilms))  # type: List[str]
+        ilms = list(map(lambda x: x.netlist, self.get_input_ilms()))  # type: List[str]
 
         all_files = library_spice_files + self.schematic_files + ilms
         spice_files = list(filter(lambda x: get_filetype(x) is HammerFiletype.SPICE, all_files))
diff --git a/hammer/lvs/mocklvs/__init__.py b/hammer/lvs/mocklvs/__init__.py
index 308cc7ca0..a5ec02763 100644
--- a/hammer/lvs/mocklvs/__init__.py
+++ b/hammer/lvs/mocklvs/__init__.py
@@ -3,11 +3,19 @@
 #
 #  See LICENSE for licence details.
 
-from hammer.vlsi import HammerLVSTool, DummyHammerTool
+from hammer.vlsi import HammerLVSTool, DummyHammerTool, HammerToolStep, HierarchicalMode
 from typing import List, Dict
+import os
+import json
 
 
 class MockLVS(HammerLVSTool, DummyHammerTool):
+    @property
+    def steps(self) -> List[HammerToolStep]:
+        return self.make_steps_from_methods([
+            self.get_ilms
+        ])
+
     def fill_outputs(self) -> bool:
         return True
 
@@ -20,5 +28,10 @@ def erc_results_pre_waived(self) -> Dict[str, int]:
     def lvs_results(self) -> List[str]:
         return ["VDD is connected to VSS"]
 
+    def get_ilms(self) -> bool:
+        if self.hierarchical_mode in [HierarchicalMode.Hierarchical, HierarchicalMode.Top]:
+            with open(os.path.join(self.run_dir, "input_ilms.json"), "w") as f:
+                f.write(json.dumps(list(map(lambda s: s.to_setting(), self.get_input_ilms()))))
+        return True
 
 tool = MockLVS
diff --git a/hammer/lvs/netgen/__init__.py b/hammer/lvs/netgen/__init__.py
index 485aa163e..10744317b 100644
--- a/hammer/lvs/netgen/__init__.py
+++ b/hammer/lvs/netgen/__init__.py
@@ -193,7 +193,7 @@ def run_lvs(self) -> bool:
         # libs for circuit2. auto format detection (somewhat dangerous).
         self.append('puts "Reading schematics..."')
         library_spice_files = self.technology.read_libs([hammer_tech.filters.spice_filter], hammer_tech.HammerTechnologyUtils.to_plain_item)
-        ilms = list(map(lambda x: x.netlist, self.ilms))  # type: List[str]
+        ilms = list(map(lambda x: x.netlist, self.get_input_ilms()))  # type: List[str]
         for sch in self.schematic_files + ilms + library_spice_files:
             self.append('puts "Reading {}..."'.format(sch))
             self.append("readnet {} 2".format(sch))
diff --git a/hammer/par/innovus/__init__.py b/hammer/par/innovus/__init__.py
index ceec05675..8d9d9ec74 100644
--- a/hammer/par/innovus/__init__.py
+++ b/hammer/par/innovus/__init__.py
@@ -249,6 +249,18 @@ def init_design(self) -> bool:
         # Match SDC time units to timing libraries
         verbose_append("set_library_unit -time 1{}".format(self.get_time_unit().value_prefix + self.get_time_unit().unit))
 
+        # Set the top and bottom global/detail routing layers.
+        layers = self.get_setting("vlsi.technology.routing_layers")
+        if layers is not None:
+            if self.version() >= self.version_number("201"):
+                verbose_append(f"set_db design_bottom_routing_layer {layers[0]}")
+                verbose_append(f"set_db design_top_routing_layer {layers[1]}")
+            else:
+                verbose_append(f"set_db route_early_global_bottom_layer {layers[0]}")
+                verbose_append(f"set_db route_early_global_top_layer {layers[1]}")
+                verbose_append(f"set_db route_design_bottom_layer {layers[0]}")
+                verbose_append(f"set_db route_design_top_layer {layers[1]}")
+
         # Read LEF layouts.
         lef_files = self.technology.read_libs([
             hammer_tech.filters.lef_filter
@@ -397,11 +409,21 @@ def place_pins(self) -> bool:
         self.verbose_append("set_db assign_pins_edit_in_batch true")
 
         promoted_pins = []  # type: List[str]
+        # Set the top/bottom layers for promoting macro pins
+        self.append(f"set_db assign_pins_promoted_macro_bottom_layer {self.get_stackup().get_metal_by_index(1).name}")
+        self.append(f"set_db assign_pins_promoted_macro_top_layer {self.get_stackup().get_metal_by_index(-1).name}")
+        self.append('set all_ppins ""')
         for pin in pin_assignments:
             if pin.preplaced:
-                # First set promoted pins
-                self.verbose_append("set_promoted_macro_pin -pins {{ {p} }}".format(p=pin.pins))
-                promoted_pins.extend(pin.pins.split())
+                # Find the pin object that should be promoted. Only one of the two (driver_pins or load_pins) should be non-empty.
+                self.append(f'set ppins [get_db [get_nets {pin.pins}] .driver_pins]')
+                self.append(f'if {{$ppins eq ""}} {{set ppins [get_db [get_nets {pin.pins}] .load_pins]}}')
+                self.append("lappend all_ppins $ppins")
+                # First promote the pin
+                self.append("set_promoted_macro_pin -insts [get_db $ppins .inst.name] -pins [get_db $ppins .base_name]")
+                # Then set them to don't touch and skip routing
+                self.append("set_dont_touch [get_db $ppins .net]")
+                self.append("set_db [get_db $ppins .net] .skip_routing true")
             else:
                 # TODO: Do we need pin blockages for our layers?
                 # Seems like we will only need special pin blockages if the vias are larger than the straps
@@ -461,10 +483,8 @@ def place_pins(self) -> bool:
 
                 self.verbose_append(" ".join(cmd))
 
-        # In case the * wildcard is used after preplaced pins, this will place promoted pins correctly.
-        # Innovus errors instead of warns if the name matching does not work (e.g. bad wildcards).
-        for ppin in promoted_pins:
-            self.verbose_append("assign_io_pins -move_fixed_pin -pins [get_db [get_db pins -if {{.name == {p} }}] .net.name]".format(p=ppin))
+        # In case the * wildcard is used after preplaced pins, this will put back the promoted pins correctly.
+        self.verbose_append("if {[llength $all_ppins] ne 0} {assign_io_pins -move_fixed_pin -pins [get_db $all_ppins .net.name]}")
 
         self.verbose_append("set_db assign_pins_edit_in_batch false")
         return True
@@ -1009,7 +1029,7 @@ def generate_floorplan_tcl(self) -> List[str]:
                             ury=constraint.y+constraint.height
                         ))
                     if ObstructionType.Route in obs_types:
-                        output.append("create_route_blockage -layers {{{layers}}} -spacing 0 -{area_flag} {{{x} {y} {urx} {ury}}}".format(
+                        output.append("create_route_blockage -except_pg_nets -layers {{{layers}}} -spacing 0 -{area_flag} {{{x} {y} {urx} {ury}}}".format(
                             x=constraint.x,
                             y=constraint.y,
                             urx=constraint.x+constraint.width,
@@ -1033,7 +1053,7 @@ def generate_floorplan_tcl(self) -> List[str]:
     def specify_std_cell_power_straps(self, blockage_spacing: Decimal, bbox: Optional[List[Decimal]], nets: List[str]) -> List[str]:
         """
         Generate a list of TCL commands that build the low-level standard cell power strap rails.
-        This will use the -master option to create power straps based on technology.core.tap_cell_rail_reference.
+        This will use the -master option to create power straps based on the tapcells in the special cells list.
         The layer is set by technology.core.std_cell_rail_layer, which should be the highest metal layer in the std cell rails.
 
         :param bbox: The optional (2N)-point bounding box of the area to generate straps. By default the entire core area is used.
@@ -1049,12 +1069,12 @@ def specify_std_cell_power_straps(self, blockage_spacing: Decimal, bbox: Optiona
             "set_db add_stripes_stacked_via_top_layer {}".format(layer_name),
             "set_db add_stripes_spacing_from_block {}".format(blockage_spacing)
         ]
-        tapcell = self.get_setting("technology.core.tap_cell_rail_reference")
+        tapcells = self.technology.get_special_cell_by_type(CellType.TapCell)[0].name
         options = [
             "-pin_layer", layer_name,
             "-layer", layer_name,
             "-over_pins", "1",
-            "-master", "\"{}\"".format(tapcell),
+            "-master", "\"{}\"".format(" ".join(tapcells)),
             "-block_ring_bottom_layer_limit", layer_name,
             "-block_ring_top_layer_limit", layer_name,
             "-pad_core_ring_bottom_layer_limit", layer_name,
diff --git a/hammer/par/mockpar/__init__.py b/hammer/par/mockpar/__init__.py
index 8b4159447..952bdfa4b 100644
--- a/hammer/par/mockpar/__init__.py
+++ b/hammer/par/mockpar/__init__.py
@@ -4,8 +4,9 @@
 #
 #  See LICENSE for licence details.
 
-from hammer.vlsi import HammerPlaceAndRouteTool, DummyHammerTool, HammerToolStep, deepdict
+from hammer.vlsi import HammerPlaceAndRouteTool, DummyHammerTool, HammerToolStep, deepdict, HierarchicalMode, ILMStruct
 from hammer.config import HammerJSONEncoder
+from hammer.tech.specialcells import CellType, SpecialCell
 
 from typing import Dict, List, Any, Optional
 from decimal import Decimal
@@ -31,7 +32,8 @@ def temp_file(self, filename: str) -> str:
     @property
     def steps(self) -> List[HammerToolStep]:
         return self.make_steps_from_methods([
-            self.power_straps
+            self.power_straps,
+            self.get_ilms
         ])
 
     def power_straps(self) -> bool:
@@ -69,18 +71,31 @@ def specify_std_cell_power_straps(self, blockage_spacing: Decimal, bbox: Optiona
         self._power_straps_check_index(layer_name)
         output_dict = {
             "layer_name": layer_name,
-            "tap_cell_name": self.get_setting("technology.core.tap_cell_rail_reference"),
+            "tap_cell_name": self.technology.get_special_cell_by_type(CellType.TapCell)[0].name[0],
             "bbox": [] if bbox is None else list(map(str, bbox)),
             "nets": list(map(str, nets))
         }
         return [json.dumps(output_dict, cls=HammerJSONEncoder)]
 
+    def get_ilms(self) -> bool:
+        if self.hierarchical_mode in [HierarchicalMode.Hierarchical, HierarchicalMode.Top]:
+            with open(os.path.join(self.run_dir, "input_ilms.json"), "w") as f:
+                f.write(json.dumps(list(map(lambda s: s.to_setting(), self.get_input_ilms()))))
+        return True
+
     def fill_outputs(self) -> bool:
         self.output_gds = "/dev/null"
-        self.output_ilms = []
         self.output_netlist = "/dev/null"
         self.output_sim_netlist = "/dev/null"
         self.hcells_list = []
+        if self.hierarchical_mode in [HierarchicalMode.Leaf, HierarchicalMode.Hierarchical]:
+            self.output_ilms = [
+                ILMStruct(dir="/dev/null", data_dir="/dev/null", module=self.top_module,
+                          lef="/dev/null", gds=self.output_gds, netlist=self.output_netlist,
+                          sim_netlist=self.output_sim_netlist)
+            ]
+        else:
+            self.output_ilms = []
         return True
 
 
diff --git a/hammer/par/openroad/__init__.py b/hammer/par/openroad/__init__.py
index cc1c773a4..9d63e2cdc 100644
--- a/hammer/par/openroad/__init__.py
+++ b/hammer/par/openroad/__init__.py
@@ -226,7 +226,7 @@ def metrics_file(self) -> str:
     def fill_cells(self) -> str:
         stdfillers = self.technology.get_special_cell_by_type(CellType.StdFiller)
         return ' '.join(list(map(lambda c: str(c), stdfillers[0].name)))
-    
+
     @property
     def timing_driven(self) -> bool:
         return self.get_setting('par.openroad.timing_driven')
@@ -389,12 +389,12 @@ def create_archive(self, output: str, code: int) -> bool:
         for openrcx_file in openrcx_files:
             if os.path.exists(openrcx_file):
                 shutil.copy2(openrcx_file, os.path.join(issue_dir, os.path.basename(openrcx_file)))
-        
+
         # KLayout tech file
         klayout_techfile_path = self.setup_klayout_techfile()
         if klayout_techfile_path and os.path.exists(klayout_techfile_path):
             shutil.copy2(klayout_techfile_path, os.path.join(issue_dir, os.path.basename(klayout_techfile_path)))
-        
+
         # DEF2Stream file
         def2stream_file = self.get_setting('par.openroad.def2stream_file')
         if os.path.exists(def2stream_file):
@@ -1081,10 +1081,16 @@ def io_placement(self) -> bool:
 
 
     def global_placement(self) -> bool:
-        metals=self.get_stackup().metals[1:]
+        layers = self.get_setting("vlsi.technology.routing_layers")
+        if layers is not None:
+            bottom_layer = self.get_stackup().get_metal_by_index(layers[0]).name
+            top_layer = self.get_stackup().get_metal_by_index(layers[1]).name
+        else:
+            metals=self.get_stackup().metals[1:]
+            bottom_layer = metals[0].name
+            top_layer = metals[-1].name
         routing_adj = self.get_setting('par.openroad.global_placement.routing_adjustment')
         spacing = self.get_setting('par.blockage_spacing')
-        idx_clock_bottom_metal=min(2,len(metals)-1)
         density = self.get_setting('par.openroad.global_placement.density')
         padding = self.get_setting('par.openroad.global_placement.placement_padding')
         routability_driven = "-routability_driven" if self.get_setting('par.openroad.global_placement.routability_driven') else ""
@@ -1094,8 +1100,8 @@ def global_placement(self) -> bool:
         # Global placement (with placed IOs, timing-driven, and routability-driven)
         # set_dont_use {{{' '.join(self.get_dont_use_list())}}}
         # reduce the routing resources of all routing layers by X%
-        set_global_routing_layer_adjustment {metals[0].name}-{metals[-1].name} {routing_adj}
-        set_routing_layers -signal {metals[0].name}-{metals[-1].name} -clock {metals[idx_clock_bottom_metal].name}-{metals[-1].name}
+        set_global_routing_layer_adjustment {bottom_layer}-{top_layer} {routing_adj}
+        set_routing_layers -signal {bottom_layer}-{top_layer} -clock {bottom_layer}-{top_layer}
         # creates blockages around macros
         # set_macro_extension {spacing}
 
@@ -1489,7 +1495,7 @@ def write_sdf(self) -> str:
     def write_regs(self) -> bool:
         # TODO: currently no analagous OpenROAD default script
         return True
-    
+
     def write_reports(self, prefix: str) -> str:
         if self.get_setting('par.openroad.write_reports') and self.timing_driven:
             return f"write_reports {prefix}"
@@ -1513,7 +1519,7 @@ def create_write_reports_tcl(self) -> List[str]:
             and is called with the command "write_reports <prefix>"
         Output report info with header and footer, to be read by the log_to_reports() function
             and written to the appropriate report file after OpenROAD completes.
-        The <cmd>_metric(s) commands tells OpenROAD to dump this metric into the JSON 
+        The <cmd>_metric(s) commands tells OpenROAD to dump this metric into the JSON
             file specified by the -metrics flag in the tool invocation (see self.metrics_file)
         '''
         # NOTE: both report_check_types and report_power commands have [> filename] option but it just generates an empty file...
@@ -1713,7 +1719,7 @@ def rotate_coordinates_old(self, origin: Tuple[Decimal, Decimal], size: Tuple[De
         else:
             self.logger.error(f"Invalid orientation {orientation}")
             return (x,y)
-    
+
     def rotate_coordinates(self, origin: Tuple[Decimal, Decimal], orientation: str) -> Tuple[str, str]:
         # TODO: need to figure out origin translations for rotations besides R90/R270
         x = str(origin[0])
@@ -1859,7 +1865,7 @@ def generate_floorplan_tcl(self) -> List[str]:
     def specify_std_cell_power_straps(self, blockage_spacing: Decimal, bbox: Optional[List[Decimal]], nets: List[str]) -> List[str]:
         """
         Generate a list of TCL commands that build the low-level standard cell power strap rails.
-        This will use the -master option to create power straps based on technology.core.tap_cell_rail_reference.
+        This will use the -master option to create power straps based on the tapcells in the special cells list.
         The layer is set by technology.core.std_cell_rail_layer, which should be the highest metal layer in the std cell rails.
         :param bbox: The optional (2N)-point bounding box of the area to generate straps. By default the entire core area is used.
         :param nets: A list of power net names (e.g. ["VDD", "VSS"]). Currently only two are supported.
diff --git a/hammer/synthesis/genus/__init__.py b/hammer/synthesis/genus/__init__.py
index 6d5a10ed1..6b14d594e 100644
--- a/hammer/synthesis/genus/__init__.py
+++ b/hammer/synthesis/genus/__init__.py
@@ -106,7 +106,11 @@ def do_pre_steps(self, first_step: HammerToolStep) -> bool:
     def do_between_steps(self, prev: HammerToolStep, next: HammerToolStep) -> bool:
         assert super().do_between_steps(prev, next)
         # Write a checkpoint to disk.
-        self.verbose_append("write_db -to_file pre_{step}".format(step=next.name))
+        if self.version() >= self.version_number("221"):
+            # -common now enables database reading in Innovus
+            self.verbose_append("write_db -common -to_file pre_{step}".format(step=next.name))
+        else:
+            self.verbose_append("write_db -to_file pre_{step}".format(step=next.name))
         return True
 
     def do_post_steps(self) -> bool:
@@ -197,7 +201,6 @@ def init_environment(self) -> bool:
             # Innovus will create instances named CLKGATE_foo, CLKGATE_bar, etc.
             verbose_append("set_db lp_clock_gating_prefix  {CLKGATE}")
             verbose_append("set_db lp_insert_clock_gating  true")
-            verbose_append("set_db lp_clock_gating_hierarchical true")
             verbose_append("set_db lp_insert_clock_gating_incremental true")
             verbose_append("set_db lp_clock_gating_register_aware true")
 
@@ -357,11 +360,15 @@ def write_outputs(self) -> bool:
 
         verbose_append("write_sdf > {run_dir}/{top}.mapped.sdf".format(run_dir=self.run_dir, top=top))
 
-        # We just get "Cannot trace ILM directory. Data corrupted."
-        # -hierarchical needs to be used for non-leaf modules
-        is_hier = self.hierarchical_mode != HierarchicalMode.Leaf # self.hierarchical_mode != HierarchicalMode.Flat
-        verbose_append("write_design -innovus {hier_flag} -gzip_files {top}".format(
-            hier_flag="-hierarchical" if is_hier else "", top=top))
+        if self.version() >= self.version_number("221"):
+            # New write_design is now meant for non-Cadence tools. write_db -common is now for Innovus-compatible databases.
+            verbose_append("write_design -gzip_files {top}".format(top=top))
+        else:
+            # We just get "Cannot trace ILM directory. Data corrupted."
+            # -hierarchical needs to be used for non-leaf modules
+            is_hier = self.hierarchical_mode != HierarchicalMode.Leaf # self.hierarchical_mode != HierarchicalMode.Flat
+            verbose_append("write_design -innovus {hier_flag} -gzip_files {top}".format(
+                hier_flag="-hierarchical" if is_hier else "", top=top))
 
         self.ran_write_outputs = True
 
diff --git a/hammer/tech/stackup.py b/hammer/tech/stackup.py
index 76e270a7d..66a3f351d 100644
--- a/hammer/tech/stackup.py
+++ b/hammer/tech/stackup.py
@@ -84,10 +84,10 @@ class Metal(BaseModel):
     min_width: The minimum wire width for this layer.
     max_width: The maximum wire width for this layer.
     pitch: The minimum cross-mask pitch for this layer (NOT same-mask pitch
-           for multiple-patterned layers). Width of routing grid for a given layer. 
-           To route denser wires on chip, multiple masks are required. 
-           During fabrication, the masks are applied separately with some spatial offsets 
-           to achieve denser line patterning. For more information on multiple-patterning, 
+           for multiple-patterned layers). Width of routing grid for a given layer.
+           To route denser wires on chip, multiple masks are required.
+           During fabrication, the masks are applied separately with some spatial offsets
+           to achieve denser line patterning. For more information on multiple-patterning,
            check https://en.wikipedia.org/wiki/Multiple_patterning
     offset: The routing track offset from the origin for the first track in this layer.
             (0 = first track is on an axis).
@@ -393,11 +393,15 @@ def get_metal(self, name: str) -> Metal:
     def get_metal_by_index(self, index: int) -> Metal:
         """
         Get a given metal layer by index.
+        If index is given as -1, return the highest metal layer (by index).
 
         :param index: Index of the metal layer
         :return: A metal layer object
         """
-        for m in self.metals:
-            if m.index == index:
-                return m
+        if index == -1:
+            return max(self.metals, key=lambda m: m.index)
+        else:
+            for m in self.metals:
+                if m.index == index:
+                    return m
         raise ValueError("Metal with index %d is not defined in stackup %s" % (index, self.name))
diff --git a/hammer/technology/asap7/__init__.py b/hammer/technology/asap7/__init__.py
index 2a640a821..6b8f71fae 100644
--- a/hammer/technology/asap7/__init__.py
+++ b/hammer/technology/asap7/__init__.py
@@ -200,9 +200,6 @@ def asap7_innovus_settings(ht: HammerTool) -> bool:
     Note that the particular routing layer settings here will persist in Innovus;
     this hook only serves as an example of what commands may need to persist."""
     ht.append('''
-set_db route_design_bottom_routing_layer 2
-set_db route_design_top_routing_layer 7
-
 # Ignore 1e+31 removal arcs for ASYNC DFF cells
 set_db timing_analysis_async_checks no_async
 
diff --git a/hammer/technology/asap7/defaults.yml b/hammer/technology/asap7/defaults.yml
index c9c5347db..ba5f45d19 100644
--- a/hammer/technology/asap7/defaults.yml
+++ b/hammer/technology/asap7/defaults.yml
@@ -39,12 +39,13 @@ vlsi:
     tap_cell_interval: 50.0 # Set the interval and offset for tap cells
     tap_cell_offset: 10.564 # Set the interval and offset for tap cells
 
+    routing_layers: [2, 7]
+
 
 technology.core:
   stackup: "asap7_3Ma_2Mb_2Mc_2Md" # This key should exist in the stackups list in the tech json
   std_cell_rail_layer: "M1" # This should specify the TOPMOST metal layer the standard cells use for power rails
   # Note that this is not usually stackup specific; it is based on the std cell libraries themselves
-  tap_cell_rail_reference: "{TAPCELL*}" # This is used to provide a reference master for generating standard cells
 
 synthesis.yosys:
   latch_map_file: "latch_map.v"
diff --git a/hammer/technology/sky130/__init__.py b/hammer/technology/sky130/__init__.py
index b7fd9d4ed..83cf28b97 100644
--- a/hammer/technology/sky130/__init__.py
+++ b/hammer/technology/sky130/__init__.py
@@ -35,7 +35,7 @@ def post_install_script(self) -> None:
 
 
     def setup_cdl(self) -> None:
-        ''' Copy and hack the cdl, replacing pfet_01v8_hvt/nfet_01v8 with 
+        ''' Copy and hack the cdl, replacing pfet_01v8_hvt/nfet_01v8 with
             respective names in LVS deck
         '''
         setting_dir = self.get_setting("technology.sky130.sky130A")
@@ -196,7 +196,7 @@ def openram_sram_names() -> List[str]:
             "sky130_sram_1kbyte_1rw1r_8x1024_8",
             "sky130_sram_2kbyte_1rw1r_32x512_8"
         ]
-    
+
     @staticmethod
     def sky130_sram_names() -> List[str]:
         sky130_sram_names = []
@@ -275,7 +275,6 @@ def sky130_innovus_settings(ht: HammerTool) -> bool:
 #-------------------------------------------------------------------------------
 set_db route_design_antenna_diode_insertion 1
 set_db route_design_antenna_cell_name "sky130_fd_sc_hd__diode_2"
-set_db route_design_bottom_routing_layer 2
 
 set_db route_design_high_freq_search_repair true
 set_db route_design_detail_post_route_spread_wire true
diff --git a/hammer/technology/sky130/defaults.yml b/hammer/technology/sky130/defaults.yml
index 6f084dc1d..5311dc06b 100644
--- a/hammer/technology/sky130/defaults.yml
+++ b/hammer/technology/sky130/defaults.yml
@@ -86,11 +86,12 @@ vlsi:
     tap_cell_interval: 15.0 # Set the interval and offset for tap cells
     tap_cell_offset: 5.0
 
+    routing_layers: [2, 6]
+
 technology.core:
   stackup: "sky130_fd_sc_hd" # This key should exist in the stackups list in the tech json
   std_cell_rail_layer: "met1" # This should specify the TOPMOST metal layer the standard cells use for power rails
   # Note that this is not usually stackup specific; it is based on the std cell libraries themselves
-  tap_cell_rail_reference: "sky130_fd_sc_hd__tap*" # This is used to provide a reference master for generating standard cells
   std_cell_supplies: 
   # default power pins are VDD/VSS
     power:  ["VPWR"]
diff --git a/hammer/vlsi/driver.py b/hammer/vlsi/driver.py
index dda264ed9..0273aa122 100644
--- a/hammer/vlsi/driver.py
+++ b/hammer/vlsi/driver.py
@@ -422,7 +422,6 @@ def set_up_lvs_tool(self, lvs_tool: HammerLVSTool,
         lvs_tool.layout_file = self.database.get_setting("lvs.inputs.layout_file", nullvalue="")
         lvs_tool.top_module = self.database.get_setting("lvs.inputs.top_module", nullvalue="")
         lvs_tool.hcells_list = self.database.get_setting("lvs.inputs.hcells_list", nullvalue=[])
-        lvs_tool.ilms = list(map(lambda x: ILMStruct.from_setting(x), self.database.get_setting("lvs.inputs.ilms", nullvalue=[])))
         missing_inputs = False
         if lvs_tool.top_module == "":
             self.log.error("Top module not specified for LVS")
@@ -1173,7 +1172,6 @@ def par_output_to_timing_input(output_dict: dict) -> Optional[dict]:
                 "timing.inputs.top_module": output_dict["par.inputs.top_module"],
                 "timing.inputs.spefs": output_dict["par.outputs.spefs"],
                 "timing.inputs.sdf_file": output_dict["par.outputs.sdf_file"],
-                "timing.inputs.ilms": output_dict["par.outputs.output_ilms"],
                 "vlsi.builtins.is_complete": False
             }  # type: Dict[str, Any]
             return result
@@ -1238,7 +1236,7 @@ def par_output_to_syn_input(output_dict: dict) -> Optional[dict]:
         """
         try:
             result = {
-                "vlsi.inputs.ilms": output_dict["par.outputs.output_ilms"],
+                "vlsi.inputs.ilms": output_dict["vlsi.inputs.ilms"] + output_dict["par.outputs.output_ilms"],
                 "vlsi.builtins.is_complete": False
             }  # type: Dict[str, Any]
             return result
@@ -1282,7 +1280,6 @@ def par_output_to_lvs_input(output_dict: dict) -> Optional[dict]:
                 "lvs.inputs.top_module": output_dict["par.inputs.top_module"],
                 "lvs.inputs.layout_file": output_dict["par.outputs.output_gds"],
                 "lvs.inputs.schematic_files": [output_dict["par.outputs.output_netlist"]],
-                "lvs.inputs.ilms": output_dict["par.outputs.output_ilms"],
                 "lvs.inputs.hcells_list": output_dict["par.outputs.hcells_list"],
                 "vlsi.builtins.is_complete": False
             }  # type: Dict[str, Any]
@@ -1810,7 +1807,7 @@ def visit_module(mod: str) -> None:
     @property
     def dump_history(self) -> bool:
         return self._dump_history
-    
+
     @dump_history.setter
     def dump_history(self, new_dump_history: bool) -> None:
         self._dump_history = new_dump_history
diff --git a/hammer/vlsi/hammer_tool.py b/hammer/vlsi/hammer_tool.py
index 8deb08334..532e08702 100644
--- a/hammer/vlsi/hammer_tool.py
+++ b/hammer/vlsi/hammer_tool.py
@@ -1107,7 +1107,7 @@ def get_independent_ground_nets(self) -> List[Supply]:
 
     def _get_by_bump_dim_pitch(self) -> Dict[str, float]:
         """
-        Return pitches in the x and y directions. 
+        Return pitches in the x and y directions.
         """
         pitch_x = self.get_setting_suffix('vlsi.inputs.bumps.pitch', 'x')
         pitch_y = self.get_setting_suffix('vlsi.inputs.bumps.pitch', 'y')
@@ -1136,7 +1136,7 @@ def get_bumps(self) -> Optional[BumpsDefinition]:
             else:
                 assignments.append(BumpAssignment(name=name, no_connect=no_con,
                     x=x, y=y, group=group, custom_cell=cell))
-        
+
         pitch_settings = self._get_by_bump_dim_pitch()
 
         return BumpsDefinition(
@@ -1538,13 +1538,16 @@ def get_stackup(self) -> Stackup:
         # TODO how does python cache this? Do we need to avoid re-processing this every time?
         return self.technology.get_stackup_by_name(self.get_setting("technology.core.stackup"))
 
-    def get_input_ilms(self) -> List[ILMStruct]:
+    def get_input_ilms(self, full_tree=False) -> List[ILMStruct]:
         """
         Get a list of input ILM modules for hierarchical mode.
+        :param full_tree: if true, obtains the full tree (up to the current level of hierarchy) from vlsi.inputs.ilms.
+        Otherwise, obtains only children ilms from par.outputs.output_ilms
         """
-        ilms = self.get_setting("vlsi.inputs.ilms")  # type: List[dict]
-        assert isinstance(ilms, list)
-        return list(map(ILMStruct.from_setting, ilms))
+        if full_tree:
+            return list(map(ILMStruct.from_setting, self.get_setting("vlsi.inputs.ilms")))
+        else:
+            return list(map(ILMStruct.from_setting, self.get_setting("par.outputs.output_ilms")))
 
     def get_output_load_constraints(self) -> List[OutputLoadConstraint]:
         """
diff --git a/hammer/vlsi/hammer_vlsi_impl.py b/hammer/vlsi/hammer_vlsi_impl.py
index 642117277..490a75f93 100644
--- a/hammer/vlsi/hammer_vlsi_impl.py
+++ b/hammer/vlsi/hammer_vlsi_impl.py
@@ -389,7 +389,9 @@ def fill_outputs(self) -> bool:
     def export_config_outputs(self) -> Dict[str, Any]:
         outputs = deepdict(super().export_config_outputs())
         outputs["par.outputs.output_ilms"] = list(map(lambda s: s.to_setting(), self.output_ilms))
-        outputs["par.outputs.output_ilms_meta"] = "append"
+        outputs["par.outputs.output_ilms_meta"] = "append"  # to coalesce ILMs for current level of hierarchy
+        outputs["vlsi.inputs.ilms"] = list(map(lambda s: s.to_setting(), self.get_input_ilms(full_tree=True)))
+        outputs["vlsi.inputs.ilms_meta"] = "append"  # to coalesce ILMs for entire hierarchical tree
         outputs["par.outputs.output_gds"] = str(self.output_gds)
         outputs["par.outputs.output_netlist"] = str(self.output_netlist)
         outputs["par.outputs.output_sim_netlist"] = str(self.output_sim_netlist)
@@ -1053,7 +1055,7 @@ def specify_std_cell_power_straps(self, blockage_spacing: Decimal, bbox: Optiona
         """
         Generate a list of TCL commands that build the low-level standard cell power strap rails.
         This is a low-level, cad-tool-specific API. It is designed to be called by higher-level methods, so calling this directly is not recommended.
-        This will create power straps based on technology.core.tap_cell_rail_reference.
+        This will create power straps based on the tapcells in the special cells list.
         The layer is set by technology.core.std_cell_rail_layer, which should be the highest metal layer in the std cell rails.
         This method should be called before any calls to specify_power_straps.
 
@@ -1201,6 +1203,9 @@ def export_config_outputs(self) -> Dict[str, Any]:
         outputs["lvs.inputs.top_module"] = self.top_module
         return outputs
 
+    def get_input_ilms(self, full_tree=True) -> List[ILMStruct]:
+        return super().get_input_ilms(full_tree)
+
     @abstractmethod
     def fill_outputs(self) -> bool:
         pass
@@ -1332,26 +1337,6 @@ def hcells_list(self, value: List[str]) -> None:
         self.attr_setter("_hcells_list", value)
 
 
-    @property
-    def ilms(self) -> List[ILMStruct]:
-        """
-        Get the list of (optional) input ILM information for hierarchical mode.
-
-        :return: The list of (optional) input ILM information for hierarchical mode.
-        """
-        try:
-            return self.attr_getter("_ilms", None)
-        except AttributeError:
-            raise ValueError("Nothing set for the list of (optional) input ILM information for hierarchical mode yet")
-
-    @ilms.setter
-    def ilms(self, value: List[ILMStruct]) -> None:
-        """Set the list of (optional) input ILM information for hierarchical mode."""
-        if not (isinstance(value, List)):
-            raise TypeError("ilms must be a List[ILMStruct]")
-        self.attr_setter("_ilms", value)
-
-
     ### Outputs ###
     ### END Generated interface HammerLVSTool ###
 
@@ -2072,9 +2057,9 @@ def upf_power_specification(self) -> str:
         output.append(f'create_power_domain {domain} \\')
         output.append(f'\t-elements {{.}}')
         #Get Supply Nets
-        power_nets = self.get_all_power_nets() 
+        power_nets = self.get_all_power_nets()
         ground_nets = self.get_all_ground_nets()
-        #Create Supply Ports 
+        #Create Supply Ports
         for pg_net in (power_nets+ground_nets):
             if(pg_net.pin != None):
                 #Create Supply Nets
@@ -2087,7 +2072,7 @@ def upf_power_specification(self) -> str:
         output.append(f'set_domain_supply_net {domain} \\')
         output.append(f'\t-primary_power_net {power_nets[0].name} \\')
         output.append(f'\t-primary_ground_net {ground_nets[0].name}')
-        #Add Port States 
+        #Add Port States
         for p_net in power_nets:
             if(p_net.pin != None):
                 output.append(f'add_port_state {p_net.name} \\')


Test patch:
diff --git a/e2e/configs/mock_hier.yml b/e2e/configs/mock_hier.yml
new file mode 100644
index 000000000..40ef02692
--- /dev/null
+++ b/e2e/configs/mock_hier.yml
@@ -0,0 +1,37 @@
+# Generate Make include to aid in flow
+vlsi.core.build_system: make
+
+vlsi.inputs.power_spec_type: "cpf"
+vlsi.inputs.power_spec_mode: "auto"
+
+synthesis.inputs:
+  top_module: "pass"
+  input_files: ["src/pass.v"]
+
+vlsi.inputs.hierarchical:
+  mode: hierarchical
+  top_module: ChipTop
+  config_source: manual
+  manual_modules:
+    - ChipTop:
+        - SubModA
+        - SubModB
+    - SubModA:
+        - SubModC
+        - SubModD
+    - SubModB:
+        - SubModE
+  manual_placement_constraints:
+    - ChipTop: []
+    - SubModA: []
+    - SubModB: []
+    - SubModC: []
+    - SubModD: []
+    - SubModE: []
+
+vlsi.core.synthesis_tool: "hammer.synthesis.mocksynth"
+synthesis.mocksynth.temp_folder: "obj_dir"
+vlsi.core.par_tool: "hammer.par.mockpar"
+vlsi.core.sim_tool: "hammer.sim.mocksim"
+vlsi.core.drc_tool: "hammer.drc.mockdrc"
+vlsi.core.lvs_tool: "hammer.lvs.mocklvs"
diff --git a/e2e/hammer-mentor-plugins b/e2e/hammer-mentor-plugins
index eca060af4..c3c8eccf9 160000
--- a/e2e/hammer-mentor-plugins
+++ b/e2e/hammer-mentor-plugins
@@ -1,1 +1,1 @@
-Subproject commit eca060af456043ded569bbcc8bea49c03bff6911
+Subproject commit c3c8eccf90761dc9d2ccd91a5ec667010cdab8d3
diff --git a/tests/test_config.py b/tests/test_config.py
index 321b1e3bb..0586346b2 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -272,6 +272,66 @@ def test_meta_crossappendref(self) -> None:
         assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala"]
         assert db.get_setting("foo.bar.languages", check_type=False) == ["scala", "python"]
 
+    def test_meta_prepend(self) -> None:
+        """
+        Test that the meta attribute "prepend" works.
+        """
+        db = hammer_config.HammerDatabase()
+        base = hammer_config.load_config_from_string("""
+foo:
+    bar:
+        adc: "yes"
+        dac: "no"
+        dsl: ["scala"]
+""", is_yaml=True)
+        meta = hammer_config.load_config_from_string("""
+{
+  "foo.bar.dsl": ["python"],
+  "foo.bar.dsl_meta": "prepend",
+  "foo.bar.dac": "current_weighted"
+}
+""", is_yaml=False)
+        db.update_core([base, meta], self.NO_TYPES)
+        assert db.get_setting("foo.bar.dac", check_type=False) == "current_weighted"
+        assert db.get_setting("foo.bar.dsl", check_type=False) == ["python", "scala"]
+
+    def test_meta_crossprepend(self) -> None:
+        """
+        Test that the meta attribute "crossprepend" works.
+        """
+        db = hammer_config.HammerDatabase()
+        base = hammer_config.load_config_from_string("""
+foo.bar.dsl: ["scala"]
+""", is_yaml=True)
+        meta = hammer_config.load_config_from_string("""
+{
+  "foo.bar.languages": ["foo.bar.dsl", ["python"]],
+  "foo.bar.languages_meta": "crossprepend"
+}
+""", is_yaml=False)
+        db.update_core([base, meta], self.NO_TYPES)
+        assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala"]
+        assert db.get_setting("foo.bar.languages", check_type=False) == ["python", "scala"]
+
+    def test_meta_crossprependref(self) -> None:
+        """
+        Test that the meta attribute "crossprependref" works.
+        """
+        db = hammer_config.HammerDatabase()
+        base = hammer_config.load_config_from_string("""
+foo.bar.dsl: ["scala"]
+snakes: ["python"]
+""", is_yaml=True)
+        meta = hammer_config.load_config_from_string("""
+{
+  "foo.bar.languages": ["foo.bar.dsl", "snakes"],
+  "foo.bar.languages_meta": "crossprependref"
+}
+""", is_yaml=False)
+        db.update_core([base, meta], self.NO_TYPES)
+        assert db.get_setting("foo.bar.dsl", check_type=False) == ["scala"]
+        assert db.get_setting("foo.bar.languages", check_type=False) == ["python", "scala"]
+
     def test_meta_crossref(self) -> None:
         """
         Test that the meta attribute "crossref" works.
@@ -1003,13 +1063,13 @@ def test_get_settings_from_dict(self) -> None:
         """
         Test functionality, error handling, of utility funct: `get_settings_from_dict`.
         """
-        
+
         db = hammer_config.HammerDatabase()
         base = hammer_config.load_config_from_string("""
 key_1: True
 key_2: ["i", "c", "v"]
 key_3: 123
-key_4: 
+key_4:
 abc.key: "largo"
 def.a_key: "west"
 def.b_key: "bank"
@@ -1018,7 +1078,7 @@ def test_get_settings_from_dict(self) -> None:
         db.update_core([base], self.NO_TYPES)
         # 1st input tests basic functionality for mandatory keys.
         # 2nd input tests requests using a shared key_prefix.
-        # 3rd input tests output when an optional key is queried for but not found. 
+        # 3rd input tests output when an optional key is queried for but not found.
         # Note it must NOT use a default value if it was not specified to begin with.
         inputs   = [{"key_1": False, "key_2": [], "key_3": [], "key_4": "lvs", "abc.key": ""},
                     {"a_key": "", "b_key": ""},
@@ -1032,8 +1092,8 @@ def test_get_settings_from_dict(self) -> None:
         # Test prefix functionality.
         for (input_dict, prefix, opt, ref) in zip(inputs, prefixes, opts, refs):
             assert db.get_settings_from_dict(input_dict, prefix, opt) == ref  # type: ignore
-        
+
         # In the final case, test error handling when mandatory keys are not specified.
         with pytest.raises(ValueError):
-            db.get_settings_from_dict({"false_key": ""}) 
-        
\ No newline at end of file
+            db.get_settings_from_dict({"false_key": ""})
+
diff --git a/tests/test_power_straps.py b/tests/test_power_straps.py
index 6cc3f3a48..eb1790960 100644
--- a/tests/test_power_straps.py
+++ b/tests/test_power_straps.py
@@ -11,6 +11,7 @@
 from hammer.config import HammerJSONEncoder
 from hammer.logging import HammerVLSILogging
 from hammer.utils import deepdict, add_dicts
+from hammer.tech.specialcells import CellType, SpecialCell
 from utils.stackup import StackupTestHelper
 from utils.tech import HasGetTech
 from utils.tool import HammerToolTestHelpers
@@ -40,6 +41,7 @@ def add_stackup_and_site(in_dict: Dict[str, Any]) -> Dict[str, Any]:
         out_dict["stackups"] = [StackupTestHelper.create_test_stackup(8, StackupTestHelper.mfr_grid()).dict()]
         out_dict["sites"] = [StackupTestHelper.create_test_site(StackupTestHelper.mfr_grid()).dict()]
         out_dict["grid_unit"] = str(StackupTestHelper.mfr_grid())
+        out_dict["special_cells"] = [SpecialCell(cell_type=CellType.TapCell, name=["FakeTapCell"]).dict()]
         return out_dict
 
     HammerToolTestHelpers.write_tech_json(tech_json_filename, tech_name, add_stackup_and_site)
@@ -56,7 +58,6 @@ def add_stackup_and_site(in_dict: Dict[str, Any]) -> Dict[str, Any]:
             "par.inputs.input_files": ("/dev/null",),
             "technology.core.stackup": "StackupWith8Metals",
             "technology.core.std_cell_rail_layer": "M1",
-            "technology.core.tap_cell_rail_reference": "FakeTapCell",
             "par.mockpar.temp_folder": tech_dir_base
         }, straps_options), cls=HammerJSONEncoder, indent=4))