diff --git a/align/pnr/main.py b/align/pnr/main.py index 4f7ac6ce0..f2382bd0f 100644 --- a/align/pnr/main.py +++ b/align/pnr/main.py @@ -13,6 +13,7 @@ from .placer import placer_driver, startup_gui from .router import router_driver from .cap_placer import cap_placer_driver +from .placer_pythonic import pythonic_placer_driver import shutil @@ -154,6 +155,7 @@ def gen_leaf_cell_info( verilog_d, pnr_const_ds): logger.debug( f'leaves: {leaves}') return leaves, capacitors + def gen_leaf_collateral( leaves, primitives, primitive_dir): # Check if collateral files exist @@ -175,6 +177,7 @@ def gen_leaf_collateral( leaves, primitives, primitive_dir): return leaf_collateral + def write_verilog_d(verilog_d): return {"modules":[{"name":m.name, "parameters": list(m.parameters), @@ -183,6 +186,7 @@ def write_verilog_d(verilog_d): } for m in verilog_d.modules], "global_signals":verilog_d.global_signals} + def generate_pnr(topology_dir, primitive_dir, pdk_dir, output_dir, subckt, *, primitives, nvariants=1, effort=0, extract=False, gds_json=False, PDN_mode=False, router_mode='top_down', gui=False, skipGDS=False, steps_to_run,lambda_coeff, nroutings=1, select_in_ILP=False, place_using_ILP=False, seed=0, use_analytical_placer=False, ilp_solver='symphony', @@ -211,7 +215,7 @@ def generate_pnr(topology_dir, primitive_dir, pdk_dir, output_dir, subckt, *, pr input_dir.mkdir(exist_ok=True) verilog_d = VerilogJsonTop.parse_file(topology_dir / verilog_file) - + manipulate_hierarchy(verilog_d, subckt) logger.debug(f"updated verilog: {verilog_d}") @@ -341,16 +345,22 @@ def generate_pnr(topology_dir, primitive_dir, pdk_dir, output_dir, subckt, *, pr 'nvariants': nvariants, 'effort': effort} - top_level, leaf_map, placement_verilog_alternatives, metrics = \ - placer_driver(cap_map=cap_map, cap_lef_s=cap_lef_s, - lambda_coeff=lambda_coeff, scale_factor=scale_factor, - select_in_ILP=select_in_ILP, place_using_ILP=place_using_ILP, seed=seed, - use_analytical_placer=use_analytical_placer, ilp_solver=ilp_solver, primitives=primitives, - toplevel_args_d=toplevel_args_d, results_dir=None, - placer_sa_iterations=placer_sa_iterations, placer_ilp_runtime=placer_ilp_runtime) + if placer == 'python': + pva = pythonic_placer_driver(subckt, input_dir, scale_factor=scale_factor) + with open("__placement_verilog_alternatives__.json", "wt") as fp2: + json.dump(pva, fp=fp2, indent=2) - with open("__placer_dump__.json", "wt") as fp: - json.dump((top_level, leaf_map, [(nm, verilog_d.dict()) for nm, verilog_d in placement_verilog_alternatives.items()],metrics), fp=fp, indent=2) + else: + top_level, leaf_map, placement_verilog_alternatives, metrics = \ + placer_driver(cap_map=cap_map, cap_lef_s=cap_lef_s, + lambda_coeff=lambda_coeff, scale_factor=scale_factor, + select_in_ILP=select_in_ILP, place_using_ILP=place_using_ILP, seed=seed, + use_analytical_placer=use_analytical_placer, ilp_solver=ilp_solver, primitives=primitives, + toplevel_args_d=toplevel_args_d, results_dir=None, + placer_sa_iterations=placer_sa_iterations, placer_ilp_runtime=placer_ilp_runtime) + + with open("__placer_dump__.json", "wt") as fp: + json.dump((top_level, leaf_map, [(nm, verilog_d.dict()) for nm, verilog_d in placement_verilog_alternatives.items()],metrics), fp=fp, indent=2) os.chdir(current_working_dir) diff --git a/align/pnr/placer_pythonic.py b/align/pnr/placer_pythonic.py index 46dd195a9..fe09936f9 100644 --- a/align/pnr/placer_pythonic.py +++ b/align/pnr/placer_pythonic.py @@ -1,4 +1,5 @@ -import logging +import json +import pathlib from align.schema.hacks import VerilogJsonTop from align.pnr.checker import check_placement @@ -115,3 +116,33 @@ def pythonic_placer(top_level, input_data, scale_factor=1): placement_data = trim_placement_data(placement_data, top_level) return placement_data + + +def pythonic_placer_driver(top_level, input_dir: pathlib.Path, scale_factor=1): + + with (input_dir / f'{top_level}.verilog.json').open('rt') as fp: + input_data = json.load(fp) + + input_data['leaves'] = list() + + modules = {module['name']: module for module in input_data['modules']} + _, _, found_leaves = compute_topoorder(modules, top_level) + + for abstract_template_name in found_leaves: + json_files = input_dir.glob(f'{abstract_template_name}*.json') + for filename in json_files: + if not filename.name.endswith('gds.json'): + with (input_dir / filename).open('rt') as fp: + leaf_data = json.load(fp) + del leaf_data['globalRoutes'] + del leaf_data['globalRouteGrid'] + assert 'bbox' in leaf_data + assert 'terminals' in leaf_data + leaf_data['abstract_template_name'] = abstract_template_name + leaf_data['concrete_template_name'] = filename.stem + leaf_data['constraints'] = list() + # TODO: Append PlaceOnGrid if exists + input_data['leaves'].append(leaf_data) + + placement_data = pythonic_placer(top_level, input_data, scale_factor=scale_factor) + return placement_data diff --git a/align/pnr/placer_pythonic_sp.py b/align/pnr/placer_pythonic_sp.py index 7519ba826..5d4dac131 100644 --- a/align/pnr/placer_pythonic_sp.py +++ b/align/pnr/placer_pythonic_sp.py @@ -1,6 +1,7 @@ import mip import time import copy +import logging import itertools import more_itertools import networkx as nx @@ -11,6 +12,8 @@ from align.pnr.grid_constraints import gen_constraints_for_module from align.cell_fabric.transformation import Transformation, Rect +logger = logging.getLogger(__name__) + class HyperParameters: max_sequence_pairs = 100 @@ -44,7 +47,7 @@ def wrapper(*args, **kwargs): s = time.time() returned_value = func(*args, **kwargs) e = time.time() - s - print(f'Elapsed time: {e:.3f} secs') + logger.debug(f'Elapsed time: {e:.3f} secs') return returned_value return wrapper @@ -218,7 +221,13 @@ def place_sequence_pair(constraints, instance_map, instance_sizes, sequence_pair net_priority = dict() for constraint in constraint_schema.expand_user_constraints(constraints): - if isinstance(constraint, constraint_schema.Boundary): + if isinstance(constraint, constraint_schema.AspectRatio): + if ratio_low := getattr(constraint, 'ratio_low', False): + model += model.var_by_name('W') >= ratio_low * model.var_by_name('H') + if ratio_high := getattr(constraint, 'ratio_high', False): + model += model.var_by_name('W') <= ratio_high * model.var_by_name('H') + + elif isinstance(constraint, constraint_schema.Boundary): if max_width := getattr(constraint, 'max_width', False): model += model.var_by_name('W') <= max_width if max_height := getattr(constraint, 'max_height', False): @@ -250,6 +259,29 @@ def place_sequence_pair(constraints, instance_map, instance_sizes, sequence_pair for i1 in instances[1:]: model += model.var_by_name(f'{i0}_{axis}') == model.var_by_name(f'{i1}_{axis}') + elif isinstance(constraint, constraint_schema.Order): + instances = getattr(constraint, 'instances') + direction = getattr(constraint, 'direction') + abut = getattr(constraint, 'abut') + + def cc(i0, i1, axis, abut): + if abut: + return model.var_by_name(f'{i0}_ur{axis}') == model.var_by_name(f'{i1}_ll{axis}') + else: + return model.var_by_name(f'{i0}_ur{axis}') <= model.var_by_name(f'{i1}_ll{axis}') + + for i0, i1 in more_itertools.pairwise(instances): + if direction == 'top_to_bottom': + model += cc(i1, i0, 'y', abut) + elif direction == 'bottom_to_top': + model += cc(i0, i1, 'y', abut) + elif direction == 'left_to_right': + model += cc(i0, i1, 'x', abut) + elif direction == 'right_to_left': + model += cc(i1, i0, 'x', abut) + else: + assert False, f'{direction} not supported' + elif isinstance(constraint, constraint_schema.SymmetricBlocks): pairs = getattr(constraint, 'pairs') axis = 'x' if getattr(constraint, 'direction') == 'V' else 'y' @@ -304,21 +336,21 @@ def place_sequence_pair(constraints, instance_map, instance_sizes, sequence_pair model.write('model.lp') # Solve - status = model.optimize(max_seconds_same_incumbent=60.0, max_seconds=300) + status = model.optimize(max_seconds_same_incumbent=60.0, max_seconds=min(10*len(instance_map), 300)) if status == mip.OptimizationStatus.OPTIMAL: - print(f'optimal solution found: cost={model.objective_value}') + logger.debug(f'optimal solution found: cost={model.objective_value}') elif status == mip.OptimizationStatus.FEASIBLE: - print(f'solution with cost {model.objective_value} current lower bound: {model.objective_bound}') + logger.debug(f'solution with cost {model.objective_value} current lower bound: {model.objective_bound}') else: - print('No solution to ILP') + logger.debug('No solution to ILP') return False # if status == mip.OptimizationStatus.OPTIMAL or status == mip.OptimizationStatus.FEASIBLE: # if model.verbose: - # print('Solution:') + # logger.debug('Solution:') # for v in model.vars: # print('\t', v.name, v.x) - # print(f'Number of solutions: {model.num_solutions}') + # logger.debug(f'Number of solutions: {model.num_solutions}') # Extract solution transformations = dict() @@ -337,7 +369,7 @@ def place_sequence_pair(constraints, instance_map, instance_sizes, sequence_pair 'width': w, 'height': h, 'area': w*h, - 'hpwl': model.var_by_name('HPWL').x / scale_hpwl, + 'hpwl': model.var_by_name('HPWL').x, 'transformations': transformations, 'model': model } @@ -351,6 +383,9 @@ def place_using_sequence_pairs(placement_data, module, top_level): hyper_params = HyperParameters() + if 'global_signals' not in module: + module['global_signals'] = set() + instances = {i['instance_name']: i for i in module['instances']} instance_map = dict() @@ -393,7 +428,7 @@ def place_using_sequence_pairs(placement_data, module, top_level): for formal_actual in instances[instance_name]['fa_map']: formal, actual = formal_actual['formal'], formal_actual['actual'] - if actual not in module['global_signals']: + if 'global_signals' in module and actual not in module['global_signals']: wires[actual].append((instance_name, tuple(x for x in concrete_template['pin_bbox'][formal]))) solution = place_sequence_pair(constraints, instance_map, instance_sizes, sequence_pair, wires=wires, place_on_grid=place_on_grid, @@ -449,6 +484,7 @@ def place_using_sequence_pairs(placement_data, module, top_level): new_module['abstract_name'] = new_module['name'] new_module['concrete_name'] = new_module['name'] + f'_{i}' new_module['pin_bbox'] = pin_bbox + new_module['metrics'] = {k: solution[k] for k in ['cost', 'width', 'height', 'area', 'hpwl']} del new_module['name'] placement_data['modules'].append(new_module) diff --git a/tests/pdk/finfet_pdk/test_placement_grid.py b/tests/pdk/finfet_pdk/test_placement_grid.py index 986dca8c5..0a02957fd 100644 --- a/tests/pdk/finfet_pdk/test_placement_grid.py +++ b/tests/pdk/finfet_pdk/test_placement_grid.py @@ -51,7 +51,7 @@ def place_on_grid_v_half(monkeypatch): @pytest.fixture def disable_tap(monkeypatch): monkeypatch.setenv('ALIGN_DISABLE_TAP', "true") - print(f"DISABLED TAP INSERTION") + print("DISABLED TAP INSERTION") def test_scalings(place_on_grid_h): @@ -273,10 +273,11 @@ def test_one_to_four_missing_power(): ckt_dir, run_dir = run_example(example, cleanup=False, log_level="INFO", n=1) -#@pytest.mark.skip(reason="To be enabled in another PR for triage and debug") def test_bias_generator(disable_tap): name = f'ckt_{get_test_id()}' netlist = circuits.bias_generator(name) + generate_legal_size_y2 = {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}} + generate_legal_size_y4 = {"name": "MOS", "parameters": {"legal_sizes": [{"y": 4}]}} constraints = { "nbias_gen": [ { @@ -284,23 +285,23 @@ def test_bias_generator(disable_tap): "fix_source_drain": False, "merge_series_devices": False, "merge_parallel_devices": False, "remove_dummy_devices": False, "remove_dummy_hierarchies": False }, - {"constraint": "PowerPorts", "ports": ["vcca"], "propagate": True}, - {"constraint": "GroundPorts", "ports": ["vssx"], "propagate": True}, + {"constraint": "PowerPorts", "ports": ["vcca"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, {"constraint": "DoNotRoute", "nets": ["vssx", "vcca"]}, - {"constraint": "GroupBlocks", "instances": ["mp0"], "instance_name": "xmp0", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qp1"], "instance_name": "xqp1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["mp1"], "instance_name": "xmp1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["mn0"], "instance_name": "xmn0", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn1"], "instance_name": "xqn1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, + {"constraint": "GroupBlocks", "instances": ["mp0"], "instance_name": "xmp0", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qp1"], "instance_name": "xqp1", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["mp1"], "instance_name": "xmp1", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["mn0"], "instance_name": "xmn0", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn1"], "instance_name": "xqn1", "generator": generate_legal_size_y2}, { "constraint": "Floorplan", "order": True, "symmetrize": False, "regions": [ - ["i0", "i6"], - ["xmp0", "xqp1", "xmp1"], - ["xmn0", "xqn1"], - ["i9"] + ["i0", "i6"], + ["xmp0", "xqp1", "xmp1"], + ["xmn0", "xqn1"], + ["i9"] ] }, {"constraint": "Order", "direction": "left_to_right", "instances": ["r0", "i0"]}, @@ -312,23 +313,23 @@ def test_bias_generator(disable_tap): "fix_source_drain": False, "merge_series_devices": False, "merge_parallel_devices": False, "remove_dummy_devices": False, "remove_dummy_hierarchies": False }, - {"constraint": "PowerPorts", "ports": ["vcca"], "propagate": True}, - {"constraint": "GroundPorts", "ports": ["vssx"], "propagate": True}, + {"constraint": "PowerPorts", "ports": ["vcca"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, {"constraint": "DoNotRoute", "nets": ["vssx", "vcca"]}, - {"constraint": "GroupBlocks", "instances": ["mp0"], "instance_name": "xmp0", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qp1"], "instance_name": "xqp1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["mn0"], "instance_name": "xmn0", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn1"], "instance_name": "xqn1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["mn1"], "instance_name": "xmn1", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, + {"constraint": "GroupBlocks", "instances": ["mp0"], "instance_name": "xmp0", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qp1"], "instance_name": "xqp1", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["mn0"], "instance_name": "xmn0", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn1"], "instance_name": "xqn1", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["mn1"], "instance_name": "xmn1", "generator": generate_legal_size_y2}, { "constraint": "Floorplan", "order": True, "symmetrize": False, "regions": [ - ["i0", "i6"], - ["xmn0", "xqn1", "xmn1"], - ["xmp0", "xqp1"], - ["i9"] + ["i0", "i6"], + ["xmn0", "xqn1", "xmn1"], + ["xmp0", "xqp1"], + ["i9"] ] }, {"constraint": "Order", "direction": "left_to_right", "instances": ["r0", "i0"]}, @@ -340,14 +341,14 @@ def test_bias_generator(disable_tap): "fix_source_drain": False, "merge_series_devices": False, "merge_parallel_devices": False, "remove_dummy_devices": False, "remove_dummy_hierarchies": False }, - {"constraint": "PowerPorts", "ports": ["vcca"], "propagate": True}, - {"constraint": "GroundPorts", "ports": ["vssx"], "propagate": True}, + {"constraint": "PowerPorts", "ports": ["vcca"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, {"constraint": "DoNotRoute", "nets": ["vssx", "vcca"]}, - {"constraint": "GroupBlocks", "instances": ["qp4", "qp3"], "instance_name": "xqp43", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qp2", "qp1"], "instance_name": "xqp21", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn4", "qn3"], "instance_name": "xqn43", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn6", "qn5"], "instance_name": "xqn65", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 4}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn1", "qn2"], "instance_name": "xqn12", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, + {"constraint": "GroupBlocks", "instances": ["qp4", "qp3"], "instance_name": "xqp43", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qp2", "qp1"], "instance_name": "xqp21", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn4", "qn3"], "instance_name": "xqn43", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn6", "qn5"], "instance_name": "xqn65", "generator": generate_legal_size_y4}, + {"constraint": "GroupBlocks", "instances": ["qn1", "qn2"], "instance_name": "xqn12", "generator": generate_legal_size_y2}, {"constraint": "SameTemplate", "instances": ["qp5<0>", "qp5<1>"]}, {"constraint": "SameTemplate", "instances": ["qp6<0>", "qp6<1>"]}, { @@ -355,11 +356,11 @@ def test_bias_generator(disable_tap): "order": True, "symmetrize": True, "regions": [ - ["xqn12"], - ["qp6<0>", "xqn65", "qp6<1>"], - ["qp5<0>", "xqn43", "qp5<1>"], - ["xqp21"], - ["xqp43"] + ["xqn12"], + ["qp6<0>", "xqn65", "qp6<1>"], + ["qp5<0>", "xqn43", "qp5<1>"], + ["xqp21"], + ["xqp43"] ] } ], @@ -369,14 +370,14 @@ def test_bias_generator(disable_tap): "fix_source_drain": False, "merge_series_devices": False, "merge_parallel_devices": False, "remove_dummy_devices": False, "remove_dummy_hierarchies": False }, - {"constraint": "PowerPorts", "ports": ["vcca"], "propagate": True}, - {"constraint": "GroundPorts", "ports": ["vssx"], "propagate": True}, + {"constraint": "PowerPorts", "ports": ["vcca"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, {"constraint": "DoNotRoute", "nets": ["vssx", "vcca"]}, - {"constraint": "GroupBlocks", "instances": ["qp4", "qp3"], "instance_name": "xqp43", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qp2", "qp1"], "instance_name": "xqp21", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn4", "qn3"], "instance_name": "xqn43", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn6", "qn5"], "instance_name": "xqn56", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, - {"constraint": "GroupBlocks", "instances": ["qn1", "qn2"], "instance_name": "xqn12", "generator": {"name": "MOS", "parameters": {"legal_sizes": [{"y": 2}]}}}, + {"constraint": "GroupBlocks", "instances": ["qp4", "qp3"], "instance_name": "xqp43", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qp2", "qp1"], "instance_name": "xqp21", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn4", "qn3"], "instance_name": "xqn43", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn6", "qn5"], "instance_name": "xqn56", "generator": generate_legal_size_y2}, + {"constraint": "GroupBlocks", "instances": ["qn1", "qn2"], "instance_name": "xqn12", "generator": generate_legal_size_y2}, {"constraint": "SameTemplate", "instances": ["qp5<0>", "qp5<1>"]}, {"constraint": "SameTemplate", "instances": ["qp6<0>", "qp6<1>"]}, { @@ -384,11 +385,11 @@ def test_bias_generator(disable_tap): "order": True, "symmetrize": True, "regions": [ - ["xqn56"], - ["xqn43"], - ["qp6<0>", "xqp21", "qp6<1>"], - ["qp5<0>", "xqp43", "qp5<1>"], - ["xqn12"] + ["xqn56"], + ["xqn43"], + ["qp6<0>", "xqp21", "qp6<1>"], + ["qp5<0>", "xqp43", "qp5<1>"], + ["xqn12"] ] } ], @@ -398,24 +399,46 @@ def test_bias_generator(disable_tap): "fix_source_drain": False, "merge_series_devices": False, "merge_parallel_devices": False, "remove_dummy_devices": False, "remove_dummy_hierarchies": False }, - {"constraint": "PowerPorts", "ports": ["vccd", "vcca"], "propagate": True}, - {"constraint": "GroundPorts", "ports": ["vssx"], "propagate": True}, + {"constraint": "PowerPorts", "ports": ["vccd", "vcca"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, {"constraint": "DoNotRoute", "nets": ["vssx", "vccd", "vcca"]}, {"constraint": "GroupBlocks", "instances": ["nand0", "nand1", "inv09"], "instance_name": "xdig"}, - {"constraint": "GroupBlocks", "instances": ["R0", "i12", "i21", "i13", "i20", "qn3"], "instance_name": "xoutp"}, - {"constraint": "GroupBlocks", "instances": ["R6", "i15", "i35", "i36", "i22", "i16"], "instance_name": "xoutn"}, - {"constraint": "GroupBlocks", "instances": ["i25", "i32"], "instance_name": "xbiasp"}, - {"constraint": "GroupBlocks", "instances": ["i24", "i27"], "instance_name": "xbiasn"}, - { - "constraint": "Floorplan", - "order": True, - "symmetrize": False, - "regions": [ - ["xdig"], - ["xbiasp", "i0", "xoutp"], - ["xbiasn", "i14", "xoutn"] - ] - } + + {"constraint": "GroupBlocks", "instances": ["R0", "i12", "i21", "i13", "i20", "qn3"], "instance_name": "xoutp", "template_name": "outp", + "constraints": [ + {"constraint": "ConfigureCompiler", "auto_constraint": False, "propagate": True}, + {"constraint": "DoNotIdentify", "instances": ["i12", "i21", "i13", "i20", "qn3"]}, + {"constraint": "Floorplan", "order": True, "regions": [['R0'], ["i12", "i21", "i13"], ["i20", "qn3"]]} + ] + }, + {"constraint": "GroupBlocks", "instances": ["R6", "i15", "i35", "i36", "i22", "i16"], "instance_name": "xoutn", "template_name": "outn", + "constraints": [ + {"constraint": "ConfigureCompiler", "auto_constraint": False, "propagate": True}, + {"constraint": "DoNotIdentify", "instances": ["i15", "i35", "i36", "i22", "i16"]}, + {"constraint": "Floorplan", "order": True, "regions": [['R6'], ["i35", "i36"], ["i15", "i22", "i16"]]} + ] + }, + {"constraint": "GroupBlocks", "instances": ["i25", "i32"], "instance_name": "xbiasp", + "constraints": [ + {"constraint": "ConfigureCompiler", "auto_constraint": False, "propagate": True}, + {"constraint": "Floorplan", "regions": [["i25"], ["i32"]]} + ] + }, + {"constraint": "GroupBlocks", "instances": ["i24", "i27"], "instance_name": "xbiasn", + "constraints": [ + {"constraint": "ConfigureCompiler", "auto_constraint": False, "propagate": True}, + {"constraint": "Floorplan", "regions": [["i24"], ["i27"]]} + ] + }, + {"constraint": "Floorplan", + "order": True, + "symmetrize": False, + "regions": [ + ["xbiasp", "i0", "xoutp"], + ["xdig"], + ["xbiasn", "i14", "xoutn"] + ] + } ] } example = build_example(name, netlist, constraints) diff --git a/tests/placer_pythonic/__init__.py b/tests/placer_pythonic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/placer_pythonic/circuits.py b/tests/placer_pythonic/circuits.py new file mode 100644 index 000000000..29a8091a5 --- /dev/null +++ b/tests/placer_pythonic/circuits.py @@ -0,0 +1,11 @@ +import textwrap + + +def common_source(name): + netlist = textwrap.dedent(f"""\ + .subckt {name} vin vop vccx vssx + mp0 vop vop vccx vccx p w=720e-9 nf=4 m=4 + mn0 vop vin vssx vssx n w=720e-9 nf=4 m=4 + .ends {name} + """) + return netlist diff --git a/tests/placer_pythonic/test_placer_circuits.py b/tests/placer_pythonic/test_placer_circuits.py new file mode 100644 index 000000000..039ca5d8b --- /dev/null +++ b/tests/placer_pythonic/test_placer_circuits.py @@ -0,0 +1,22 @@ +import os +import logging + +from . import circuits +from .utils import get_test_id, build_example, run_example + +logger = logging.getLogger(__name__) + +CLEANUP = False if os.getenv("CLEANUP", None) else True +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + + +def test_common_source(): + name = f'ckt_{get_test_id()}' + netlist = circuits.common_source(name) + constraints = [ + {"constraint": "PowerPorts", "ports": ["vccx"]}, + {"constraint": "GroundPorts", "ports": ["vssx"]}, + {"constraint": "AlignInOrder", "line": "left", "instances": ["mp0", "mn0"]} + ] + example = build_example(name, netlist, constraints) + run_example(example, cleanup=CLEANUP, log_level=LOG_LEVEL, additional_args=['--placer', 'python']) diff --git a/tests/placer_pythonic/utils.py b/tests/placer_pythonic/utils.py new file mode 100644 index 000000000..bf70a6b24 --- /dev/null +++ b/tests/placer_pythonic/utils.py @@ -0,0 +1,269 @@ +import os +import json +import pathlib +from copy import deepcopy +import shutil +import align.pdk.finfet +import re +import math +from collections import Counter + +try: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt +except ImportError: + plt = None + +ALIGN_HOME = pathlib.Path(__file__).resolve().parent.parent.parent.parent + +PDK_DIR = pathlib.Path(align.pdk.finfet.__file__).parent + +MY_DIR = pathlib.Path(__file__).resolve().parent + +if 'ALIGN_WORK_DIR' in os.environ: + WORK_DIR = pathlib.Path(os.environ['ALIGN_WORK_DIR']).resolve() / "pdk_finfet" +else: + WORK_DIR = MY_DIR / "pdk_finfet" + +WORK_DIR.mkdir(exist_ok=True, parents=True) + + +def _canvas_to_data(c): + if hasattr(c, "computeBbox"): + c.computeBbox() + data = {'bbox': c.bbox.toList(), 'globalRoutes': [], 'globalRouteGrid': [], 'terminals': c.removeDuplicates(allow_opens=True)} + else: + data = c + for k in ['globalRoutes', 'globalRouteGrid']: + if k not in data: + data[k] = [] + return data + + +def export_to_viewer(fn, c): + if ALIGN_HOME: + data = _canvas_to_data(c) + with open(pathlib.Path(ALIGN_HOME)/'Viewer'/'INPUT'/f'{fn}.json', "wt") as fp: + fp.write(json.dumps(data, indent=2) + '\n') + return data + + +def compare_with_golden(fn, c): + MY_DIR = pathlib.Path(__file__).resolve().parent + data = _canvas_to_data(c) + + export_to_viewer(fn, data) + + with open(MY_DIR / (fn + "-cand.json"), "wt") as fp: + fp.write(json.dumps(data, indent=2) + '\n') + + with open(MY_DIR / (fn + "-freeze.json"), "rt") as fp: + data2 = json.load(fp) + + assert data == data2 + + +def place(cv, c, ox, oy): + data = _canvas_to_data(c) + for term in data["terminals"]: + new_term = deepcopy(term) + x0, y0, x1, y1 = term['rect'] + new_term['rect'] = [x0+ox, y0+oy, x1+ox, y1+oy] + cv.terminals.append(new_term) + + +def get_test_id(): + try: + t = os.environ.get('PYTEST_CURRENT_TEST') + t = t.split(' ')[0].split(':')[-1] + t = t.replace('[', '_').replace(']', '').replace('-', '_') + t = t[5:] + except BaseException: + t = 'debug' + return t + + +def build_example(name, netlist, constraints): + example = WORK_DIR / name + if example.exists() and example.is_dir(): + shutil.rmtree(example) + example.mkdir(parents=True) + with open(example / f'{name}.sp', 'w') as fp: + fp.write(netlist) + if isinstance(constraints, dict): + for k, v in constraints.items(): + with open(example / f'{k}.const.json', 'w') as fp: + fp.write(json.dumps(v, indent=2)) + else: + with open(example / f'{name}.const.json', 'w') as fp: + fp.write(json.dumps(constraints, indent=2)) + return example + + +def run_example(example, n=4, cleanup=True, max_errors=0, log_level='INFO', area=None, additional_args=None): + run_dir = WORK_DIR / f'run_{example.name}' + if run_dir.exists() and run_dir.is_dir(): + shutil.rmtree(run_dir) + run_dir.mkdir(parents=True) + os.chdir(run_dir) + + args = [str(example), '-p', str(PDK_DIR), '-l', log_level, '-n', str(n)] + + if additional_args: + for elem in additional_args: + if elem: + args.append(elem) + + print(f"\nCOMMAND LINE: schematic2layout.py {' '.join(args)}") + results = align.CmdlineParser().parse_args(args) + + assert results is not None, f"{example.name}: No results generated" + + for result in results: + _, variants = result + for (k, v) in variants.items(): + assert 'errors' in v, f"No Layouts were generated for {example.name} ({k})" + assert v['errors'] <= max_errors, f"{example.name} ({k}):Number of DRC errors: {str(v['errors'])}" + + name = example.name.upper() + + verify_abstract_names(name, run_dir) + + verify_area(name, run_dir, area=area) + + if cleanup: + shutil.rmtree(run_dir) + shutil.rmtree(example) + else: + return (example, run_dir) + + +def verify_abstract_names(name, run_dir): + """ Make sure that there are no unused abstract_template_name's """ + with (run_dir / '1_topology' / '__primitives_library__.json').open('rt') as fp: + primitives = json.load(fp) + name_counts = Counter([v['name'] for v in primitives]) + for pname in name_counts: + assert name_counts[pname] < 2, f'Multiple primitive definitions for {pname}' + abstract_names = {v['name'] for v in primitives if 'generator' in v} + + with (run_dir / '1_topology' / f'{name}.verilog.json').open('rt') as fp: + verilog_json = json.load(fp) + abstract_names_used = list() + for module in verilog_json['modules']: + abstract_names_used += [i['abstract_template_name'] for i in module['instances']] + abstract_names_used = set(abstract_names_used) + assert abstract_names.issubset(abstract_names_used), ( + f'__primitives_library__.json has unused primitives: {set.difference(abstract_names, abstract_names_used)}\n' + ) + + +def verify_area(name, run_dir, area=None): + json_file = run_dir / '3_pnr' / f'{name}_0.json' + if json_file.exists(): + with json_file.open('rt') as fp: + layout = json.load(fp) + x0, y0, x1, y1 = layout['bbox'] + area_0 = (x1-x0)*(y1-y0) + print(f'{name}: area is {area_0}') + if area is not None and area > 0: + # assert area_0 <= area, (f'Placer found a suboptimal solution: area: {area_0} target: {area} ratio: {area_0/area}') + print(f'Target area: {area} Current area: {area_0} Current/Target: {area_0/area}') + + +def _parse_pattern(pattern): + """ + logger->debug("sa__cost name={0} t_index={1} effort={2} cost={3} temp={4}", designData.name, T_index, 0, curr_cost, T); + logger->debug("sa__seq__hash name={0} {1} cost={2} temp={3} t_index={4}", designData.name, trial_sp.getLexIndex(designData), trial_cost, T, T_index); + """ + data = dict() + with open(WORK_DIR / 'LOG' / 'align.log', 'r') as fp: + for line in fp: + if re.search(pattern, line): + line = line.split(pattern)[1] + line = line.strip().split() + for item in line: + k, v = item.split('=') + if k not in data: + data[k] = [] + else: + data[k].append(float(v)) + # for k, v in data.items(): + # print(f'{k}: min={min(v)} max={max(v)}') + return data + + +def plot_sa_cost(name): + assert plt is not None, "Need to install matplotlib to use this feature" + data = _parse_pattern(f'sa__cost name={name}') + + init = -1 + for i in data['cost']: + if i > 0: + init = i + break + assert init > 0 + + cost = [math.exp(k-init) if k > 0 else -1 for k in data['cost']] + + fig, ax = plt.subplots(2, 1) + + ax[0].plot(range(len(cost)), cost, '-o') + ax[0].set_xlim(0, len(cost)+1) + ax[0].set_ylabel('Cost norm. to initial') + ax[0].set_xlabel('Iteration') + ax[0].legend([f'initial={cost[0]:.3f} final={cost[-1]:.3f} min={min(cost):.3f}']) + ax[0].grid() + + ax[1].plot(data['temp'], cost, '-o') + ax[1].set_xlim(data['temp'][0], data['temp'][-1]) + ax[1].set_ylabel('Cost norm. to initial') + ax[1].set_xlabel('Temperature') + ax[1].legend([f'initial={cost[0]:.3f} final={cost[-1]:.3f} min={min(cost):.3f}']) + ax[1].grid() + + fig.set_size_inches(14, 8) + fig.savefig(f'{WORK_DIR}/{name}_cost_trajectory.png', dpi=300, pad_inches=0.1) + + +def plot_sa_seq(name): + assert plt is not None, "Need to install matplotlib to use this feature" + data = _parse_pattern(f'sa__seq__hash name={name}') + + init = -1 + for i in data['cost']: + if i > 0: + init = i + break + assert init > 0 + + cost = [math.exp(k-init) if k > 0 else -1 for k in data['cost']] + cm = plt.cm.get_cmap('cool') + + fig, ax = plt.subplots(1, 2) + + if len(data['pos_pair']) == len(data['selected']): + for i in range(len(data['pos_pair'])): + data['pos_pair'][i] = float(f"{int(data['pos_pair'][i])}.{int(data['selected'][i])}") + data['neg_pair'][i] = float(f"{int(data['neg_pair'][i])}.{int(data['selected'][i])}") + + max_val = max(max(data['neg_pair']), max(data['pos_pair'])) + + im0 = ax[0].scatter(data['pos_pair'], data['neg_pair'], c=data['temp'], cmap=cm, marker='.') + im1 = ax[1].scatter(data['pos_pair'], data['neg_pair'], c=cost, cmap=cm, marker='.') + + ax[0].set_title('Temperature') + ax[1].set_title('Cost (Norm. to initial. -1 if infeasible)') + + for i in range(2): + ax[i].set_ylabel('Neg pair') + ax[i].set_xlabel('Pos pair') + ax[i].set_xlim(-0.5, max_val+0.5) + ax[i].set_ylim(-0.5, max_val+0.5) + ax[i].grid() + + fig.colorbar(im0, ax=ax[0]) + fig.colorbar(im1, ax=ax[1]) + fig.set_size_inches(14, 6) + fig.savefig(f'{WORK_DIR}/{name}_seqpair_scatter.png', dpi=300, pad_inches=0.001)