Repository: ucb-bar/hammer

Issue:
place_pins failures should terminate the flow
### Background Work

- [X] Yes, I searched the [Development](https://github.com/ucb-bar/hammer/discussions/categories/development) section of the Discussions and have read the [posting guidelines](https://github.com/ucb-bar/hammer/discussions/717)
- [X] Yes, I searched the [documentation](https://hammer-vlsi.readthedocs.io/)
- [X] This feature is specific to Hammer, i.e., does not belong in a plugin repository instead

### Feature Description

Failures in place_pins, specificallly `edit_pins` will cause sub-blocks to generate no pins. Surprisingly, this can cause no other obvious signs of problems all the way until LVS.

We should terminate the flow early as soon as `edit_pins` fails

### Motivating Example

_No response_

Hints from issue comments:

Solution patch:
diff --git a/hammer/common/cadence/__init__.py b/hammer/common/cadence/__init__.py
index 4d0702f7..1f16069f 100644
--- a/hammer/common/cadence/__init__.py
+++ b/hammer/common/cadence/__init__.py
@@ -207,10 +207,15 @@ class CadenceTool(HasSDCSupport, HasCPFSupport, HasUPFSupport, TCLTool, HammerTo
 
             # Finally, apply the analysis view.
             # TODO: should not need to analyze extra views as well. Defaulting to hold for now (min. runtime impact).
-            append_mmmc("set_analysis_view -setup {{ {setup_views} }} -hold {{ {hold_views} {extra_views} }}".format(
+            # First extra view is assumed to be for dynamic and leakage power calculation.
+            power_opts = ""
+            if len(extra_view_names) > 0:
+                power_opts = f"-dynamic {extra_view_names[0]} -leakage {extra_view_names[0]}"
+            append_mmmc("set_analysis_view -setup {{ {setup_views} }} -hold {{ {hold_views} {extra_views} }} {power}".format(
                 setup_views=" ".join(setup_view_names),
                 hold_views=" ".join(hold_view_names),
-                extra_views=" ".join(extra_view_names)
+                extra_views=" ".join(extra_view_names),
+                power=power_opts
             ))
         else:
             # First, create an Innovus library set.
diff --git a/hammer/config/defaults.yml b/hammer/config/defaults.yml
index 2578ee6d..1adabf4c 100644
--- a/hammer/config/defaults.yml
+++ b/hammer/config/defaults.yml
@@ -221,7 +221,8 @@ vlsi.inputs:
   # - Required for all types
   # - One of the following:
   #   - "dummy" (does nothing with this constraint)
-  #   - "placement" (creates a placement constraint for an instance)
+  #   - "placement" or "soft_placement" (creates a soft placement constraint for a hierarchical instance)
+  #   - "hard_placement" (creates a hard placement constraint for a hierarchical instance)
   #   - "toplevel" (top-level chip dimensions; may only occur once, for the top-level module)
   #   - "hardmacro" (places this hard macro at a particular spot)
   #   - "hierarchical" (marks this instance as part of hierarchical place and route)
diff --git a/hammer/par/innovus/__init__.py b/hammer/par/innovus/__init__.py
index ad442a7d..3f95f866 100644
--- a/hammer/par/innovus/__init__.py
+++ b/hammer/par/innovus/__init__.py
@@ -4,6 +4,7 @@
 
 import shutil
 from typing import List, Dict, Optional, Callable, Tuple, Any, cast
+from itertools import chain
 
 import os
 import errno
@@ -155,6 +156,8 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
         v["INNOVUS_BIN"] = self.get_setting("par.innovus.innovus_bin")
         if self.version() >= self.version_number("221"):  # support critical region resynthesis with DDI
             v["PATH"] = f'{os.environ.copy()["PATH"]}:{os.path.dirname(self.get_setting("par.innovus.innovus_bin").replace("INNOVUS", "GENUS"))}'
+        if self.get_setting("par.innovus.signoff"):  # add path to Tempus
+            v["PATH"] = v["PATH"] + f':{self.get_setting("cadence.cadence_home")}/SSV/SSV{self.get_setting("par.innovus.version")}/bin'
         return v
 
     @property
@@ -223,10 +226,18 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
             self.place_pins,
             self.place_opt_design,
             self.clock_tree,
-            self.add_fillers,
-            self.route_design,
-            self.opt_design
+            self.add_fillers
         ]
+        if self.version() >= self.version_number("231"):
+            # Route & post-route opt are now combined
+            steps += [self.route_opt_design]
+        else:
+            steps += [
+                self.route_design,
+                self.opt_design
+            ]
+        if self.get_setting("par.innovus.signoff"):
+            steps += [self.opt_signoff]
         write_design_step = [
             self.write_regs,
             self.write_design
@@ -392,6 +403,10 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
                     lly = "[get_db bump:Bump_{x}.{y} .bbox.ll.y]".format(x=bump.x, y=bump.y),
                     urx = "[get_db bump:Bump_{x}.{y} .bbox.ur.x]".format(x=bump.x, y=bump.y),
                     ury = "[get_db bump:Bump_{x}.{y} .bbox.ur.y]".format(x=bump.x, y=bump.y)))
+
+            # Use early global bump router
+            if self.version() >= self.version_number("231") and self.get_setting("par.innovus.early_route_bumps"):
+                self.append("set_db route_early_global_route_bump_nets true")
         return True
 
     def place_tap_cells(self) -> bool:
@@ -423,7 +438,7 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
             return False
 
         power_pin_layers = self.get_setting("par.generate_power_straps_options.by_tracks.pin_layers")
-        
+
         const = cast(PlacementConstraint, topconst)
         assert isinstance(const.margins, Margins), "Margins must be defined for the top level"
         fp_llx = const.margins.left
@@ -486,10 +501,10 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
                     assign_arg = "-assign {{ {x} {y} }}".format(x=pin.location[0], y=pin.location[1])
 
                 layers_arg = ""
-                
+
                 if set(pin.layers or []).intersection(set(power_pin_layers)):
                     self.logger.error("Signal pins will be generated on the same layer(s) as power pins. Double-check to see if intended.")
-                
+
                 if pin.layers is not None and len(pin.layers) > 0:
                     layers_arg = "-layer {{ {} }}".format(" ".join(pin.layers))
 
@@ -528,17 +543,32 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
 
     def place_opt_design(self) -> bool:
         """Place the design and do pre-routing optimization."""
+        # First, check that no ports are left unplaced. Exit with error code for user to fix.
+        self.append('''
+            set unplaced_pins [get_db ports -if {.place_status == unplaced}]
+            if {$unplaced_pins ne ""} {
+                print_message -error "Some pins remain unplaced, which will cause invalid placement and routing. These are the unplaced pins: $unplaced_pins"
+                exit 2
+            }
+            ''', clean=True)
         if self.hierarchical_mode.is_nonleaf_hierarchical():
             self.verbose_append('''
             flatten_ilm
             update_constraint_mode -name my_constraint_mode -ilm_sdc_files {sdc}
             '''.format(sdc=self.post_synth_sdc), clean=True)
+
+        # Use place_opt_design V2 (POD-Turbo). Option must be explicitly set only in 22.1.
+        if self.version() >= self.version_number("221") and self.version() < self.version_number("231"):
+            self.verbose_append("set_db opt_enable_podv2_clock_opt_flow true")
+
         self.verbose_append("place_opt_design")
         return True
 
     def clock_tree(self) -> bool:
         """Setup and route a clock tree for clock nets."""
         if len(self.get_clock_ports()) > 0:
+            # Fix fanout load violations
+            self.verbose_append("set_db opt_fix_fanout_load true")
             # Ignore clock tree when there are no clocks
             # If special cells are specified, explicitly set them instead of letting tool infer from libs
             buffer_cells = self.technology.get_special_cell_by_type(CellType.CTSBuffer)
@@ -554,11 +584,18 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
             if len(logic_cells) > 0:
                 self.append(f"set_db cts_logic_cells {{{' '.join(logic_cells[0].name)}}}")
             self.verbose_append("create_clock_tree_spec")
-            if bool(self.get_setting("par.innovus.use_cco")):
-                # -hold is a secret flag for ccopt_design (undocumented anywhere)
-                self.verbose_append("ccopt_design -hold -report_dir hammer_cts_debug -report_prefix hammer_cts")
+            if self.version() >= self.version_number("221"):
+                # Required for place_opt_design v2 (POD-Turbo)
+                if bool(self.get_setting("par.innovus.use_cco")):
+                    self.verbose_append("clock_opt_design -hold -timing_debug_report")
+                else:
+                    self.verbose_append("clock_opt_design -cts -timing_debug_report")
             else:
-                self.verbose_append("clock_design")
+                if bool(self.get_setting("par.innovus.use_cco")):
+                    # -hold is a secret flag for ccopt_design (undocumented anywhere)
+                    self.verbose_append("ccopt_design -hold -timing_debug_report")
+                else:
+                    self.verbose_append("clock_design")
         if self.hierarchical_mode.is_nonleaf_hierarchical():
             self.verbose_append("unflatten_ilm")
         return True
@@ -641,12 +678,82 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
         self.verbose_append("route_design")
         return True
 
+    def opt_settings(self) -> str:
+        cmds = []
+        # Enable auto hold recovery if slack degrades
+        cmds.append("set_db opt_post_route_hold_recovery auto")
+        # Fix SI-induced slew violations (glitch fixing enabled by default)
+        cmds.append("set_db opt_post_route_fix_si_transitions true")
+        # Report reasons for not fixing hold
+        cmds.append("set_db opt_verbose true")
+        # Report reasons for not fixing DRVs
+        cmds.append("set_db opt_detail_drv_failure_reason true")
+        if self.version() >= self.version_number("221"):  # critical region resynthesis
+            # Report failure reasons
+            cmds.append("set_db opt_sequential_genus_restructure_report_failure_reason true")
+        return "\n".join(cmds)
+
     def opt_design(self) -> bool:
         """
         Post-route optimization and fix setup & hold time violations.
         -expanded_views creates timing reports for each MMMC view.
         """
-        self.verbose_append("opt_design -post_route -setup -hold -expanded_views")
+        self.append(self.opt_settings())
+        self.verbose_append("opt_design -post_route -setup -hold -expanded_views -timing_debug_report")
+        if self.hierarchical_mode.is_nonleaf_hierarchical():
+            self.verbose_append("unflatten_ilm")
+        return True
+
+    def route_opt_design(self) -> bool:
+        """
+        Routing and post-route optimization. New flow as default in 23.1.
+        Performs more optimization during the routing stage.
+        Fixes setup, hold, and DRV violations.
+        """
+        if self.hierarchical_mode.is_nonleaf_hierarchical():
+            self.verbose_append("flatten_ilm")
+
+        # Allow express design effort to complete running.
+        # By default, route_design will abort in express mode with
+        # "WARNING (NRIG-142) Express flow by default will not run routing".
+        self.verbose_append("set_db design_express_route true")
+
+        self.append(self.opt_settings())
+
+        self.verbose_append("route_opt_design -timing_debug_report")
+
+        if self.hierarchical_mode.is_nonleaf_hierarchical():
+            self.verbose_append("unflatten_ilm")
+        return True
+
+    def opt_signoff(self) -> bool:
+        """
+        Signoff timing and optimization.
+        Should only be run after all implementation flows to obtain better timing,
+        setup/hold fixing, DRV fixing, and power optimization.
+        Note: runs Tempus in batch mode.
+        """
+        if self.hierarchical_mode.is_nonleaf_hierarchical():
+            self.verbose_append("flatten_ilm")
+
+        # Options
+        # Set clock fixing cell list
+        cell_types = [CellType.CTSBuffer, CellType.CTSInverter, CellType.CTSGate, CellType.CTSLogic]
+        cells_by_type = chain(*map(lambda c: self.technology.get_special_cell_by_type(c), cell_types))
+        flat_cells = chain(*map(lambda cells: cells.name, cells_by_type))
+        regexp = "|".join(flat_cells)
+        self.verbose_append(f"set_db opt_signoff_clock_cell_list [lsearch -regexp -all -inline [get_db lib_cells .base_name] {regexp}]")
+        # Fix DRVs
+        self.verbose_append("set_db opt_signoff_fix_data_drv true")
+        self.verbose_append("set_db opt_signoff_fix_clock_drv true")
+
+        # Signoff timing requires remote host setting
+        self.verbose_append(f"set_multi_cpu_usage -remote_host 1 -cpu_per_remote_host {self.get_setting('vlsi.core.max_threads')}")
+        self.verbose_append("time_design_signoff")
+
+        # Run
+        self.verbose_append("opt_signoff -all")
+
         if self.hierarchical_mode.is_nonleaf_hierarchical():
             self.verbose_append("unflatten_ilm")
         return True
@@ -746,7 +853,24 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
             self.verbose_append("flatten_ilm")
 
         # Output the Standard Delay Format File for use in timing annotated gate level simulations
-        self.verbose_append("write_sdf {run_dir}/{top}.par.sdf".format(run_dir=self.run_dir, top=self.top_module))
+        corners = self.get_mmmc_corners()
+        if corners:
+            # Derive flags for min::type::max
+            max_view = next((c for c in corners if c.type is MMMCCornerType.Setup), None)
+            max_view_flag = ""
+            if max_view is not None:
+                max_view_flag = f"-max_view {max_view.name}.setup_view"
+            min_view = next((c for c in corners if c.type is MMMCCornerType.Hold), None)
+            min_view_flag = ""
+            if min_view is not None:
+                min_view_flag = f"-min_view {min_view.name}.hold_view"
+            typ_view = next((c for c in corners if c.type is MMMCCornerType.Extra), None)
+            typ_view_flag = ""
+            if typ_view is not None:
+                typ_view_flag = f"-typical_view {typ_view.name}.extra_view"
+            self.verbose_append(f"write_sdf {max_view_flag} {min_view_flag} {typ_view_flag} {self.run_dir}/{self.top_module}.par.sdf")
+        else:
+            self.verbose_append(f"write_sdf {self.run_dir}/{self.top_module}.par.sdf".format(run_dir=self.run_dir, top=self.top_module))
 
         return True
 
@@ -804,6 +928,10 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
         return True
 
     def write_design(self) -> bool:
+        # Because implementation is done, enable report_timing -early/late and SDF writing
+        # without recalculating timing graph for each analysis view
+        self.append("set_db timing_enable_simultaneous_setup_hold_mode true")
+
         # Save the Innovus design.
         self.verbose_append("write_db {lib_name} -def -verilog".format(
             lib_name=self.output_innovus_lib_name
@@ -990,9 +1118,6 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
         """
         output = []  # type: List[str]
 
-        # TODO(edwardw): proper source locators/SourceInfo
-        output.append("# Floorplan automatically generated from HAMMER")
-
         # Top-level chip size constraint.
         # Default/fallback constraints if no other constraints are provided.
         # TODO snap this to a core site
@@ -1039,8 +1164,16 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
                     ))
                 if constraint.type == PlacementConstraintType.Dummy:
                     pass
-                elif constraint.type == PlacementConstraintType.Placement:
-                    output.append("create_guide -name {name} -area {x1} {y1} {x2} {y2}".format(
+                elif constraint.type == PlacementConstraintType.SoftPlacement:
+                    output.append("create_boundary_constraint -type guide -hinst {name} -rects {{ {x1} {y1} {x2} {y2} }}".format(
+                        name=new_path,
+                        x1=constraint.x,
+                        x2=constraint.x + constraint.width,
+                        y1=constraint.y,
+                        y2=constraint.y + constraint.height
+                    ))
+                elif constraint.type == PlacementConstraintType.HardPlacement:
+                    output.append("create_boundary_constraint -type region -hinst {name} -rects {{ {x1} {y1} {x2} {y2} }}".format(
                         name=new_path,
                         x1=constraint.x,
                         x2=constraint.x + constraint.width,
@@ -1073,20 +1206,20 @@ class Innovus(HammerPlaceAndRouteTool, CadenceTool):
                     if current_top_layer is not None:
                         bot_layer = self.get_stackup().get_metal_by_index(1).name
                         cover_layers = list(map(lambda m: m.name, self.get_stackup().get_metals_below_layer(current_top_layer)))
-                        output.append("create_route_halo -bottom_layer {b} -space {s} -top_layer {t} -inst {inst}".format(     
+                        output.append("create_route_halo -bottom_layer {b} -space {s} -top_layer {t} -inst {inst}".format(
                             inst=new_path, b=bot_layer, t=current_top_layer, s=spacing))
-                        
+
                         if(self.get_setting("par.power_to_route_blockage_ratio") < 1):
                             self.logger.warning("The power strap blockage region is smaller than the routing halo region for hard macros. Double-check if this is intended.")
-                            
-                        place_push_out = round(spacing*self.get_setting("par.power_to_route_blockage_ratio") , 1) # Push the place halo, and therefore PG blockage, further out from route halo so router is aware of straps before entering final routing.  
 
-                        output.append("create_place_halo -insts {inst} -halo_deltas {{{s} {s} {s} {s}}} -snap_to_site".format(  
+                        place_push_out = round(spacing*self.get_setting("par.power_to_route_blockage_ratio") , 1) # Push the place halo, and therefore PG blockage, further out from route halo so router is aware of straps before entering final routing.
+
+                        output.append("create_place_halo -insts {inst} -halo_deltas {{{s} {s} {s} {s}}} -snap_to_site".format(
                             inst=new_path, s=place_push_out))
                         output.append("set pg_blockage_shape [get_db [get_db hinsts {inst}][get_db insts {inst}] .place_halo_polygon]".format(
                             inst=new_path))
                         output.append("create_route_blockage -pg_nets -layers {{{layers}}} -polygon $pg_blockage_shape".format(layers=" ".join(cover_layers)))
-                        
+
                 elif constraint.type == PlacementConstraintType.Obstruction:
                     obs_types = get_or_else(constraint.obs_types, [])  # type: List[ObstructionType]
                     assert '/' not in new_path, "'obstruction' placement constraints must be provided a path directly under the top level"
diff --git a/hammer/par/innovus/defaults.yml b/hammer/par/innovus/defaults.yml
index 22c59461..3e753d32 100644
--- a/hammer/par/innovus/defaults.yml
+++ b/hammer/par/innovus/defaults.yml
@@ -8,7 +8,7 @@ par.innovus:
 
   # Innovus version to use.
   # Used to locate the binary - e.g. the '221' in ${cadence.cadence_home}/DDI/DDI221/INNOVUS221/bin/innovus
-  version: "221"
+  version: "231"
 
   # Design flow effort.
   # Valid options: express (fastest), standard, and extreme (slowest).
@@ -35,3 +35,13 @@ par.innovus:
   # Note that this requires an optional licence (enccco).
   # type: bool
   use_cco: true
+
+  # Route bumps with early global router (version >= 23.1 only).
+  # By default, bumps are ignored for routing. If true, turn on early bump routing.
+  # This is useful for RC delay estimation if you don't have IO cells routed to bumps via the flip-chip router.
+  early_route_bumps: false
+
+  # Perform signoff timing, then timing/DRV/power optimization with opt_signoff.
+  # Should only be run at the very end, as runtime impact is significant.
+  # Requires the same version of Tempus to be installed at ${cadence.cadence_home}/SSV/SSV${par.innovus.bin}/bin
+  signoff: false
diff --git a/hammer/par/openroad/__init__.py b/hammer/par/openroad/__init__.py
index afd90b89..dfe3085c 100644
--- a/hammer/par/openroad/__init__.py
+++ b/hammer/par/openroad/__init__.py
@@ -1814,7 +1814,7 @@ class OpenROADPlaceAndRoute(OpenROADPlaceAndRouteTool):
                     pass
                 if constraint.type == PlacementConstraintType.Dummy:
                     pass
-                elif constraint.type == PlacementConstraintType.Placement:
+                elif constraint.type in [PlacementConstraintType.SoftPlacement, PlacementConstraintType.HardPlacement]:
                     pass
                 # for OpenROAD
                 elif constraint.type in [PlacementConstraintType.HardMacro, PlacementConstraintType.Hierarchical]:
diff --git a/hammer/power/joules/defaults.yml b/hammer/power/joules/defaults.yml
index 1feb6fef..d9eda232 100644
--- a/hammer/power/joules/defaults.yml
+++ b/hammer/power/joules/defaults.yml
@@ -8,4 +8,4 @@ power.joules:
 
   # Joules version to use.
   # Used to locate the binary - e.g. the '221' in ${cadence.cadence_home}/DDI/DDI221/JLS221/bin/joules
-  version: "221"
+  version: "231"
diff --git a/hammer/synthesis/genus/__init__.py b/hammer/synthesis/genus/__init__.py
index 5a658e69..a167f08b 100644
--- a/hammer/synthesis/genus/__init__.py
+++ b/hammer/synthesis/genus/__init__.py
@@ -250,6 +250,9 @@ class Genus(HammerSynthesisTool, CadenceTool):
                 verbose_append("set_db module:{top}/{mod} .preserve true".format(top=self.top_module, mod=ilm.module))
         verbose_append("init_design -top {}".format(self.top_module))
 
+        # Report timing constraint issues
+        verbose_append("report_timing -lint -verbose")
+
         # Setup power settings from cpf/upf
         # Difference from other tools: apply_power_intent after read
         power_cmds = self.generate_power_spec_commands()
@@ -357,12 +360,13 @@ set_db hinst:{inst} .preserve true
         self.verbose_append("set_db message:WSDF-201 .max_print 20")
         self.verbose_append("set_db use_tiehilo_for_const duplicate")
 
-        # If there is more than 1 corner or a certain type, use lib cells for only the active analysis view
-        corner_counts = Counter(list(map(lambda c: c.type, self.get_mmmc_corners())))
-        if any(cnt>1 for cnt in corner_counts.values()):
-            self.verbose_append("set ACTIVE_VIEW [string map { .setup_view {} .hold_view {} .extra_view {} } [get_db analysis_view:[get_analysis_views] .name]]")
-            self.verbose_append("set HI_TIEOFF [get_db base_cell:{TIE_HI_CELL} .lib_cells -if {{ .library.default_opcond == $ACTIVE_VIEW }}]".format(TIE_HI_CELL=tie_hi_cell))
-            self.verbose_append("set LO_TIEOFF [get_db base_cell:{TIE_LO_CELL} .lib_cells -if {{ .library.default_opcond == $ACTIVE_VIEW }}]".format(TIE_LO_CELL=tie_lo_cell))
+        # If MMMC corners specified, get the single lib cell for the active analysis view
+        # Else, Genus will complain that multiple objects match for the cell name
+        corners = self.get_mmmc_corners()
+        if corners:
+            self.verbose_append("set ACTIVE_SET [string map { .setup_view .setup_set .hold_view .hold_set .extra_view .extra_set } [get_db [get_analysis_views] .name]]")
+            self.verbose_append("set HI_TIEOFF [get_db base_cell:{TIE_HI_CELL} .lib_cells -if {{ .library.library_set.name == $ACTIVE_SET }}]".format(TIE_HI_CELL=tie_hi_cell))
+            self.verbose_append("set LO_TIEOFF [get_db base_cell:{TIE_LO_CELL} .lib_cells -if {{ .library.library_set.name == $ACTIVE_SET }}]".format(TIE_LO_CELL=tie_lo_cell))
             self.verbose_append("add_tieoffs -high $HI_TIEOFF -low $LO_TIEOFF -max_fanout 1 -verbose")
         else:
             self.verbose_append("add_tieoffs -high {HI_TIEOFF} -low {LO_TIEOFF} -max_fanout 1 -verbose".format(HI_TIEOFF=tie_hi_cell, LO_TIEOFF=tie_lo_cell))
diff --git a/hammer/synthesis/genus/defaults.yml b/hammer/synthesis/genus/defaults.yml
index a8c79b84..fee8294b 100644
--- a/hammer/synthesis/genus/defaults.yml
+++ b/hammer/synthesis/genus/defaults.yml
@@ -6,7 +6,7 @@ synthesis.genus:
 
   # Genus version to use.
   # Used to locate the binary - e.g. the '221' in ${cadence.cadence_home}/DDI/DDI221/GENUS221/bin/genus
-  version: "221"
+  version: "231"
 
   # Generate the TCL file but do not run it yet.
   generate_only: false
diff --git a/hammer/vlsi/constraints.py b/hammer/vlsi/constraints.py
index 87830178..e0c075e9 100644
--- a/hammer/vlsi/constraints.py
+++ b/hammer/vlsi/constraints.py
@@ -530,18 +530,21 @@ class ObstructionType(Enum):
 
 class PlacementConstraintType(Enum):
     Dummy = 1
-    Placement = 2
-    TopLevel = 3
-    HardMacro = 4
-    Hierarchical = 5
-    Obstruction = 6
-    Overlap = 7
+    SoftPlacement = 2
+    HardPlacement = 3
+    TopLevel = 4
+    HardMacro = 5
+    Hierarchical = 6
+    Obstruction = 7
+    Overlap = 8
 
     @classmethod
     def __mapping(cls) -> Dict[str, "PlacementConstraintType"]:
         return {
             "dummy": PlacementConstraintType.Dummy,
-            "placement": PlacementConstraintType.Placement,
+            "placement": PlacementConstraintType.SoftPlacement,
+            "soft_placement": PlacementConstraintType.SoftPlacement,
+            "hard_placement": PlacementConstraintType.HardPlacement,
             "toplevel": PlacementConstraintType.TopLevel,
             "hardmacro": PlacementConstraintType.HardMacro,
             "hierarchical": PlacementConstraintType.Hierarchical,
@@ -781,7 +784,7 @@ class PlacementConstraint(NamedTuple('PlacementConstraint', [
             assert isinstance(create_physical, bool)
 
         ### Width & height ###
-        # These fields are mandatory for Hierarchical, Dummy, Placement, TopLevel, and Obstruction constraints
+        # These fields are mandatory for Hierarchical, Dummy, Soft/HardPlacement, TopLevel, and Obstruction constraints
         # These fields are optional for HardMacro and Overlap constraints
         # TODO(ucb-bar/hammer#414) make them mandatory for HardMacro and Overlap once there's a more robust way of automatically getting that data into hammer
         # This is not None because we don't want to make width optional for the reason above
diff --git a/hammer/vlsi/hammer_tool.py b/hammer/vlsi/hammer_tool.py
index b3f80880..46136f10 100644
--- a/hammer/vlsi/hammer_tool.py
+++ b/hammer/vlsi/hammer_tool.py
@@ -1268,7 +1268,7 @@ class HammerTool(metaclass=ABCMeta):
         if viz_mode in ["all", "floorplan"]:
             macro_rects = macro_text = obs_rects = obs_text = orient_lines = '<g transform="{}">\n'.format(translate)
             for c in fp_consts:
-                if c.type in [PlacementConstraintType.Placement, PlacementConstraintType.HardMacro, PlacementConstraintType.Hierarchical]:
+                if c.type in [PlacementConstraintType.SoftPlacement, PlacementConstraintType.HardPlacement, PlacementConstraintType.HardMacro, PlacementConstraintType.Hierarchical]:
                     # macros & hierarchical not required to have width/height, will resolve to 0
                     (width, height) = (c.width, c.height)
                     if width == 0 or height == 0:


Test patch:
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
index f538f8c7..3e82268e 100644
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -621,7 +621,13 @@ class TestPlacementConstraint:
              "height": Decimal(20),
              "orientation": "r0"}
         tc = PlacementConstraint.from_dict(d)
-        assert tc.type == PlacementConstraintType.Placement
+        assert tc.type == PlacementConstraintType.SoftPlacement
+        d["type"] = "soft_placement"
+        tc = PlacementConstraint.from_dict(d)
+        assert tc.type == PlacementConstraintType.SoftPlacement
+        d["type"] = "hard_placement"
+        tc = PlacementConstraint.from_dict(d)
+        assert tc.type == PlacementConstraintType.HardPlacement
         assert tc.path == "path/to/placement"
         assert tc.x == Decimal(4)
         assert tc.y == Decimal(6)