1059 lines
40 KiB
Python
1059 lines
40 KiB
Python
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Script Name : run_fpga_flow.py
|
|
# Description : This script designed to run different flows supported by
|
|
# OpensFPGA project.
|
|
# Args : python3 run_fpga_flow.py --help
|
|
# Author : Ganesh Gore
|
|
# Email : ganesh.gore@utah.edu
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import time
|
|
import traceback
|
|
from datetime import timedelta
|
|
import shlex
|
|
import glob
|
|
import json
|
|
import argparse
|
|
from configparser import ConfigParser, ExtendedInterpolation
|
|
import logging
|
|
from envyaml import EnvYAML
|
|
import glob
|
|
import subprocess
|
|
import threading
|
|
from string import Template
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
from importlib import util
|
|
|
|
if util.find_spec("humanize"):
|
|
import humanize
|
|
|
|
if sys.version_info[0] < 3:
|
|
raise Exception("run_fpga_task script must be using Python 3")
|
|
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Initialise general paths for the script
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Copy directory where flow file exist
|
|
flow_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
# Find OpenFPGA base directory
|
|
openfpga_base_dir = os.path.abspath(os.path.join(flow_script_dir, os.pardir, os.pardir))
|
|
# Copy directory from where script is laucnhed
|
|
# [req to resolve relative paths provided while launching script]
|
|
launch_dir = os.getcwd()
|
|
|
|
# Path section to append in configuration file to interpolate path
|
|
task_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
script_env_vars = {
|
|
"PATH": {
|
|
"OPENFPGA_FLOW_PATH": task_script_dir,
|
|
"ARCH_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "arch"),
|
|
"OPENFPGA_SHELLSCRIPT_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "OpenFPGAShellScripts"),
|
|
"BENCH_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "benchmarks"),
|
|
"TECH_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "tech"),
|
|
"SPICENETLIST_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "SpiceNetlists"),
|
|
"VERILOG_PATH": os.path.join("${PATH:OPENFPGA_PATH}", "VerilogNetlists"),
|
|
"OPENFPGA_PATH": os.path.abspath(os.path.join(task_script_dir, os.pardir, os.pardir)),
|
|
}
|
|
}
|
|
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Reading command-line argument
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
|
|
# Helper function to provide better alignment to help print
|
|
|
|
|
|
def formatter(prog):
|
|
return argparse.HelpFormatter(prog, max_help_position=60)
|
|
|
|
|
|
parser = argparse.ArgumentParser(formatter_class=formatter)
|
|
|
|
# Mandatory arguments
|
|
parser.add_argument("arch_file", type=str)
|
|
parser.add_argument("benchmark_files", type=str, nargs="+")
|
|
# parser.add_argument('extraArgs', nargs=argparse.REMAINDER)
|
|
parser.add_argument("otherthings", nargs="*")
|
|
|
|
# Optional arguments
|
|
parser.add_argument("--top_module", type=str, default="top")
|
|
parser.add_argument("--fpga_flow", type=str, default="yosys_vpr")
|
|
parser.add_argument("--flow_config", type=str, help="CAD tools path overrides default setting")
|
|
parser.add_argument(
|
|
"--run_dir",
|
|
type=str,
|
|
default=os.path.join(openfpga_base_dir, "tmp"),
|
|
help="Directory to store intermidiate file & final results",
|
|
)
|
|
parser.add_argument("--default_tool_path", type=str, default=os.path.join(flow_script_dir, os.pardir, "misc", "fpgaflow_default_tool_path.conf"), help="The configuraton file contains paths to tools as well as keywords to be extracted from logs")
|
|
parser.add_argument(
|
|
"--openfpga_shell_template",
|
|
type=str,
|
|
default=os.path.join("openfpga_flow", "openfpga_shell_scripts", "example_script.openfpga"),
|
|
help="Sample openfpga shell script",
|
|
)
|
|
parser.add_argument("--openfpga_arch_file", type=str, help="Openfpga architecture file for shell")
|
|
parser.add_argument(
|
|
"--arch_variable_file", type=str, default=None, help="Openfpga architecture file for shell"
|
|
)
|
|
# parser.add_argument('--openfpga_sim_setting_file', type=str,
|
|
# help="Openfpga simulation file for shell")
|
|
# parser.add_argument('--external_fabric_key_file', type=str,
|
|
# help="Key file for shell")
|
|
parser.add_argument(
|
|
"--yosys_tmpl",
|
|
type=str,
|
|
default=None,
|
|
help="Alternate yosys template, generates top_module.blif",
|
|
)
|
|
parser.add_argument(
|
|
"--ys_rewrite_tmpl",
|
|
type=str,
|
|
default=None,
|
|
help="Alternate yosys template, to rewrite verilog netlist",
|
|
)
|
|
parser.add_argument("--verific", action="store_true", help="Run yosys with verific enabled")
|
|
parser.add_argument("--disp", action="store_true", help="Open display while running VPR")
|
|
parser.add_argument("--debug", action="store_true", help="Run script in debug mode")
|
|
|
|
# Blif_VPR Only flow arguments
|
|
parser.add_argument("--activity_file", type=str, help="Activity file used while running yosys flow")
|
|
parser.add_argument(
|
|
"--base_verilog",
|
|
type=str,
|
|
help="Original Verilog file to run verification in " + "blif_VPR flow",
|
|
)
|
|
|
|
# ACE2 and power estimation related arguments
|
|
parser.add_argument("--K", type=int, help="LUT Size, if not specified extracted from arch file")
|
|
parser.add_argument("--power", action="store_true")
|
|
parser.add_argument("--power_tech", type=str, help="Power tech xml file for power calculation")
|
|
parser.add_argument("--ace_d", type=float, help="Specify the default signal density of PIs in ACE2")
|
|
parser.add_argument(
|
|
"--ace_p", type=float, help="Specify the default signal probablity of PIs in ACE2"
|
|
)
|
|
parser.add_argument("--black_box_ace", action="store_true")
|
|
|
|
# VPR Options
|
|
parser.add_argument("--min_route_chan_width", type=float, help="Turn on min_route_chan_width")
|
|
parser.add_argument(
|
|
"--max_route_width_retry",
|
|
type=int,
|
|
default=100,
|
|
help="Maximum iterations to perform to reroute",
|
|
)
|
|
parser.add_argument("--fix_route_chan_width", type=int, help="Turn on fix_route_chan_width")
|
|
parser.add_argument(
|
|
"--vpr_timing_pack_off", action="store_true", help="Turn off the timing-driven pack for vpr"
|
|
)
|
|
parser.add_argument(
|
|
"--vpr_place_clb_pin_remap", action="store_true", help="Turn on place_clb_pin_remap in VPR"
|
|
)
|
|
parser.add_argument(
|
|
"--vpr_max_router_iteration", type=int, help="Specify the max router iteration in VPR"
|
|
)
|
|
parser.add_argument(
|
|
"--vpr_route_breadthfirst",
|
|
action="store_true",
|
|
help="Use the breadth-first routing algorithm of VPR",
|
|
)
|
|
parser.add_argument(
|
|
"--vpr_use_tileable_route_chan_width",
|
|
action="store_true",
|
|
help="Turn on the conversion to " + "tileable_route_chan_width in VPR",
|
|
)
|
|
|
|
# VPR - FPGA-X2P Extension
|
|
X2PParse = parser.add_argument_group("VPR-FPGA-X2P Extension")
|
|
X2PParse.add_argument(
|
|
"--vpr_fpga_x2p_rename_illegal_port",
|
|
action="store_true",
|
|
help="Rename illegal ports option of VPR FPGA SPICE",
|
|
)
|
|
X2PParse.add_argument(
|
|
"--vpr_fpga_x2p_signal_density_weight",
|
|
type=float,
|
|
help="Specify the signal_density_weight of VPR FPGA SPICE",
|
|
)
|
|
X2PParse.add_argument(
|
|
"--vpr_fpga_x2p_sim_window_size",
|
|
type=float,
|
|
help="specify the sim_window_size of VPR FPGA SPICE",
|
|
)
|
|
X2PParse.add_argument(
|
|
"--vpr_fpga_x2p_compact_routing_hierarchy",
|
|
action="store_true",
|
|
help="Compact_routing_hierarchy",
|
|
)
|
|
X2PParse.add_argument(
|
|
"--vpr_fpga_x2p_duplicate_grid_pin", action="store_true", help="Added duplicated grid pin"
|
|
)
|
|
|
|
# VPR - FPGA-SPICE Extension
|
|
SPParse = parser.add_argument_group("FPGA-SPICE Extension")
|
|
SPParse.add_argument("--vpr_fpga_spice", action="store_true", help="Print SPICE netlists in VPR")
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_sim_mt_num", type=int, help="Specify the option sim_mt_num of VPR FPGA SPICE"
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_print_component_tb",
|
|
action="store_true",
|
|
help="Output component-level testbench",
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_print_grid_tb", action="store_true", help="Output grid-level testbench"
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_print_top_testbench",
|
|
action="store_true",
|
|
help="Output full-chip-level testbench",
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_leakage_only",
|
|
action="store_true",
|
|
help="Turn on leakage_only mode in VPR FPGA SPICE",
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_parasitic_net_estimation_off",
|
|
action="store_true",
|
|
help="Turn off parasitic_net_estimation in VPR FPGA SPICE",
|
|
)
|
|
SPParse.add_argument(
|
|
"--vpr_fpga_spice_testbench_load_extraction_off",
|
|
action="store_true",
|
|
help="Turn off testbench_load_extraction in VPR FPGA SPICE",
|
|
)
|
|
SPParse.add_argument("--vpr_fpga_spice_simulator_path", type=str, help="Specify simulator path")
|
|
|
|
# VPR - FPGA-Verilog Extension
|
|
VeriPar = parser.add_argument_group("FPGA-Verilog Extension")
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog", action="store_true", help="Generator verilog of VPR FPGA SPICE"
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_dir", type=str, help="path to store generated verilog files"
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_include_timing",
|
|
action="store_true",
|
|
help="Print delay specification in Verilog files",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_include_signal_init",
|
|
action="store_true",
|
|
help="Print signal initialization in Verilog files",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_autocheck_top_testbench",
|
|
action="store_true",
|
|
help="Print autochecked top-level " + "testbench for Verilog Generator of VPR FPGA SPICE",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_formal_verification_top_netlist",
|
|
action="store_true",
|
|
help="Print formal top Verilog files",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_include_icarus_simulator",
|
|
action="store_true",
|
|
help="dd syntax and definition" + " required to use Icarus Verilog simulator",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_user_defined_template", action="store_true", help="Unknown parameter"
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_report_timing_tcl",
|
|
action="store_true",
|
|
help="Generate tcl script useful " + "for timing report generation",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_report_timing_rpt_path",
|
|
type=str,
|
|
help="Specify path for report timing results",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_sdc_pnr",
|
|
action="store_true",
|
|
help="Generate sdc file to constraint Hardware P&R",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_sdc_analysis", action="store_true", help="Generate sdc file to do STA"
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_top_tb",
|
|
action="store_true",
|
|
help="Print top-level testbench for Verilog Generator " + "of VPR FPGA SPICE",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_input_blif_tb",
|
|
action="store_true",
|
|
help="Print testbench" + "for input blif file in Verilog Generator",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_print_simulation_ini",
|
|
action="store_true",
|
|
help="Create simulation INI file",
|
|
)
|
|
VeriPar.add_argument(
|
|
"--vpr_fpga_verilog_explicit_mapping", action="store_true", help="Explicit Mapping"
|
|
)
|
|
|
|
# VPR - FPGA-Bitstream Extension
|
|
BSparse = parser.add_argument_group("FPGA-Bitstream Extension")
|
|
BSparse.add_argument(
|
|
"--vpr_fpga_bitstream_generator", action="store_true", help="Generate FPGA-SPICE bitstream"
|
|
)
|
|
|
|
# Regression test option
|
|
RegParse = parser.add_argument_group("Regression Test Extension")
|
|
RegParse.add_argument(
|
|
"--end_flow_with_test", action="store_true", help="Run verification test at the end"
|
|
)
|
|
|
|
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Global varaibles declaration
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Setting up print and logging system
|
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(levelname)s - %(message)s")
|
|
logger = logging.getLogger("OpenFPGA_Flow_Logs")
|
|
|
|
# variable to store script_configuration and cad tool paths
|
|
config, cad_tools = None, None
|
|
ExecTime = {}
|
|
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Main program starts here
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
|
|
|
|
def main():
|
|
logger.debug("Script Launched in " + os.getcwd())
|
|
check_required_file(args.default_tool_path)
|
|
read_script_config(args.default_tool_path)
|
|
validate_command_line_arguments()
|
|
prepare_run_directory(args.run_dir)
|
|
if args.fpga_flow == "yosys_vpr":
|
|
logger.info('Running "yosys_vpr" Flow')
|
|
run_yosys_with_abc()
|
|
# TODO Make it optional if activity file is provided
|
|
if args.power:
|
|
run_ace2()
|
|
run_pro_blif_3arg()
|
|
else:
|
|
# Make a copy of the blif file to be compatible with vpr flow
|
|
shutil.copy(args.top_module + "_yosys_out.blif", args.top_module + ".blif")
|
|
|
|
# Always Generate the post-synthesis verilog files
|
|
run_rewrite_verilog()
|
|
|
|
if args.fpga_flow == "vpr_blif":
|
|
collect_files_for_vpr()
|
|
if args.fpga_flow == "yosys":
|
|
run_yosys_with_abc()
|
|
if not (args.fpga_flow == "yosys"):
|
|
logger.info("Running OpenFPGA Shell Engine ")
|
|
run_openfpga_shell()
|
|
if args.end_flow_with_test:
|
|
run_netlists_verification()
|
|
|
|
ExecTime["End"] = time.time()
|
|
|
|
def timestr(x):
|
|
return (
|
|
humanize.naturaldelta(timedelta(seconds=x))
|
|
if "humanize" in sys.modules
|
|
else str(int(x)) + " Sec "
|
|
)
|
|
|
|
if args.fpga_flow == "yosys":
|
|
TimeInfo = "Openfpga_flow completed, " + "Total Time Taken %s " % timestr(
|
|
ExecTime["End"] - ExecTime["Start"]
|
|
)
|
|
else:
|
|
TimeInfo = (
|
|
"Openfpga_flow completed, "
|
|
+ "Total Time Taken %s " % timestr(ExecTime["End"] - ExecTime["Start"])
|
|
+ "VPR Time %s " % timestr(ExecTime["VPREnd"] - ExecTime["VPRStart"])
|
|
)
|
|
TimeInfo += (
|
|
"Verification Time %s "
|
|
% timestr(ExecTime["VerificationEnd"] - ExecTime["VerificationStart"])
|
|
if args.end_flow_with_test
|
|
else ""
|
|
)
|
|
logger.info(TimeInfo)
|
|
exit()
|
|
|
|
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
# Subroutines starts here
|
|
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
|
|
|
|
|
|
def check_required_file(default_tool_path):
|
|
"""Function ensure existace of all required files for the script"""
|
|
files_dict = {
|
|
"CAD TOOL PATH": default_tool_path
|
|
,
|
|
}
|
|
for filename, filepath in files_dict.items():
|
|
if not os.path.isfile(filepath):
|
|
clean_up_and_exit("Not able to locate default file " + filename)
|
|
|
|
|
|
def read_script_config(default_tool_path):
|
|
"""This fucntion reads default CAD tools path from configuration file"""
|
|
global config, cad_tools
|
|
config = ConfigParser(interpolation=ExtendedInterpolation())
|
|
config.read_dict(script_env_vars)
|
|
default_cad_tool_conf = default_tool_path
|
|
config.read_file(open(default_cad_tool_conf))
|
|
if args.flow_config:
|
|
config.read_file(open(args.flow_config))
|
|
if not "CAD_TOOLS_PATH" in config.sections():
|
|
clean_up_and_exit("Missing CAD_TOOLS_PATH in openfpga_flow config")
|
|
cad_tools = config["CAD_TOOLS_PATH"]
|
|
|
|
if args.arch_variable_file:
|
|
_, file_extension = os.path.splitext(args.arch_variable_file)
|
|
if file_extension in [".yml", ".yaml"]:
|
|
script_env_vars["PATH"].update(
|
|
EnvYAML(args.arch_variable_file, include_environment=False)
|
|
)
|
|
if file_extension in [
|
|
".json",
|
|
]:
|
|
with open(args.arch_variable_file, "r") as fp:
|
|
script_env_vars["PATH"].update(json.load(fp))
|
|
|
|
|
|
def validate_command_line_arguments():
|
|
"""
|
|
This function validate the command line arguments
|
|
FLOW_SCRIPT_CONFIG->valid_flows :
|
|
Key is used to validate if the request flow is supported by the script
|
|
CMD_ARGUMENT_DEPENDANCY :
|
|
Validates the dependencies of the command arguments
|
|
|
|
Checks the following file exists and replaces them with an absolute path
|
|
- All architecture files
|
|
- Benchmark files
|
|
- Power tech files
|
|
- Run directory
|
|
- Activity file
|
|
- Base verilog file
|
|
"""
|
|
logger.info("Validating command line arguments")
|
|
|
|
if args.debug:
|
|
logger.info("Setting loggger in debug mode")
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Check if flow supported
|
|
if not args.fpga_flow in config.get("FLOW_SCRIPT_CONFIG", "valid_flows"):
|
|
clean_up_and_exit("%s Flow not supported" % args.fpga_flow)
|
|
|
|
# Check if argument list is consistant
|
|
for eacharg, dependent in config.items("CMD_ARGUMENT_DEPENDANCY"):
|
|
if getattr(args, eacharg, None):
|
|
dependent = dependent.split(",")
|
|
for eachdep in dependent:
|
|
if not any([getattr(args, i, 0) for i in eachdep.split("|")]):
|
|
clean_up_and_exit(
|
|
"'%s' argument depends on (%s) arguments"
|
|
% (eacharg, ", ".join(dependent).replace("|", " or "))
|
|
)
|
|
|
|
# Check if architecrue files exists
|
|
args.arch_file = os.path.abspath(args.arch_file)
|
|
if not os.path.isfile(args.arch_file):
|
|
clean_up_and_exit("VPR architecture file not found. -%s" % args.arch_file)
|
|
args.openfpga_arch_file = os.path.abspath(args.openfpga_arch_file)
|
|
if not os.path.isfile(args.openfpga_arch_file):
|
|
clean_up_and_exit("OpenFPGA architecture file not found. -%s" % args.openfpga_arch_file)
|
|
|
|
# Filter provided benchmark files
|
|
for index, everyinput in enumerate(args.benchmark_files):
|
|
args.benchmark_files[index] = os.path.abspath(everyinput)
|
|
if os.path.isdir(args.benchmark_files[index]):
|
|
logger.warning("Skipping directory in bench %s" % everyinput)
|
|
logger.warning(
|
|
"Directory is not support in benchmark list" + "use wildcard pattern to add files"
|
|
)
|
|
continue
|
|
for everyfile in glob.glob(args.benchmark_files[index]):
|
|
if not os.path.isfile(everyfile):
|
|
clean_up_and_exit("Failed to copy benchmark file -%s" % args.arch_file)
|
|
|
|
# Filter provided powertech files
|
|
if args.power_tech:
|
|
args.power_tech = os.path.abspath(args.power_tech)
|
|
if not os.path.isfile(args.power_tech):
|
|
clean_up_and_exit("Power Tech file not found. -%s" % args.power_tech)
|
|
|
|
# Expand run directory to absolute path
|
|
args.run_dir = os.path.abspath(args.run_dir)
|
|
if args.power:
|
|
if args.activity_file:
|
|
args.activity_file = os.path.abspath(args.activity_file)
|
|
if args.base_verilog:
|
|
args.base_verilog = os.path.abspath(args.base_verilog)
|
|
|
|
|
|
def prepare_run_directory(run_dir):
|
|
"""
|
|
Prepares run directory to run
|
|
1. Change current directory to run_dir
|
|
2. Copy architecture XML file to run_dir
|
|
3. Copy circuit files to run_dir
|
|
"""
|
|
logger.info("Run directory : %s" % run_dir)
|
|
if os.path.isdir(run_dir):
|
|
no_of_files = len(next(os.walk(run_dir))[2])
|
|
shutil.rmtree(run_dir)
|
|
os.makedirs(run_dir)
|
|
# Clean run_dir is created change working directory
|
|
os.chdir(run_dir)
|
|
|
|
# Create arch dir in run_dir and copy flattened architecture file
|
|
os.mkdir("arch")
|
|
tmpl = Template(open(args.arch_file, encoding="utf-8").read())
|
|
arch_filename = os.path.basename(args.arch_file)
|
|
args.arch_file = os.path.join(run_dir, "arch", arch_filename)
|
|
with open(args.arch_file, "w", encoding="utf-8") as archfile:
|
|
archfile.write(tmpl.safe_substitute(script_env_vars["PATH"]))
|
|
|
|
if args.openfpga_arch_file:
|
|
tmpl = Template(open(args.openfpga_arch_file, encoding="utf-8").read())
|
|
arch_filename = os.path.basename(args.openfpga_arch_file)
|
|
args.openfpga_arch_file = os.path.join(run_dir, "arch", arch_filename)
|
|
with open(args.openfpga_arch_file, "w", encoding="utf-8") as archfile:
|
|
archfile.write(tmpl.safe_substitute(script_env_vars["PATH"]))
|
|
|
|
# Sanitize provided openshell template, if provided
|
|
if args.openfpga_shell_template:
|
|
if not os.path.isfile(args.openfpga_shell_template or ""):
|
|
logger.error("Openfpga shell file - %s" % args.openfpga_shell_template)
|
|
clean_up_and_exit(
|
|
"Provided openfpga_shell_template"
|
|
+ f" {args.openfpga_shell_template} file not found"
|
|
)
|
|
else:
|
|
shutil.copy(args.openfpga_shell_template, args.top_module + "_template.openfpga")
|
|
|
|
# Create benchmark dir in run_dir and copy flattern architecture file
|
|
os.mkdir("benchmark")
|
|
try:
|
|
for index, eachfile in enumerate(args.benchmark_files):
|
|
args.benchmark_files[index] = shutil.copy2(
|
|
eachfile, os.path.join(os.curdir, "benchmark")
|
|
)
|
|
except:
|
|
logger.exception("Failed to copy all benchmark file to run_dir")
|
|
|
|
|
|
def clean_up_and_exit(msg, clean=False):
|
|
logger.error("Current working directory : " + os.getcwd())
|
|
logger.error(msg)
|
|
logger.error("Exiting . . . . . .")
|
|
exit(1)
|
|
|
|
|
|
def create_yosys_params():
|
|
tree = ET.parse(args.arch_file)
|
|
root = tree.getroot()
|
|
try:
|
|
lut_size = max(
|
|
[
|
|
int(pb_type.find("input").get("num_pins"))
|
|
for pb_type in root.iter("pb_type")
|
|
if pb_type.get("class") == "lut"
|
|
]
|
|
)
|
|
logger.info("Extracted lut_size size from arch XML = %s", lut_size)
|
|
logger.info("Running Yosys with lut_size = %s", lut_size)
|
|
except:
|
|
logger.exception("Failed to extract lut_size from XML file")
|
|
clean_up_and_exit("")
|
|
args.K = lut_size
|
|
# Yosys script parameter mapping
|
|
ys_params = script_env_vars["PATH"]
|
|
|
|
for indx in range(0, len(OpenFPGAArgs), 2):
|
|
tmpVar = OpenFPGAArgs[indx][2:].upper()
|
|
ys_params[tmpVar] = OpenFPGAArgs[indx + 1]
|
|
|
|
if not args.verific:
|
|
ys_params["VERILOG_FILES"] = " ".join(
|
|
[shlex.quote(eachfile) for eachfile in args.benchmark_files]
|
|
)
|
|
if not "READ_VERILOG_OPTIONS" in ys_params:
|
|
ys_params["READ_VERILOG_OPTIONS"] = ""
|
|
else:
|
|
if "ADD_INCLUDE_DIR" not in ys_params:
|
|
ys_params["ADD_INCLUDE_DIR"] = ""
|
|
if "ADD_LIBRARY_DIR" not in ys_params:
|
|
ys_params["ADD_LIBRARY_DIR"] = ""
|
|
if "ADD_BLACKBOX_MODULES" not in ys_params:
|
|
ys_params["ADD_BLACKBOX_MODULES"] = ""
|
|
if "READ_HDL_FILE" not in ys_params:
|
|
ys_params["READ_HDL_FILE"] = ""
|
|
if "READ_LIBRARY" not in ys_params:
|
|
ys_params["READ_LIBRARY"] = ""
|
|
if "VERIFIC_VERILOG_STANDARD" not in ys_params:
|
|
ys_params["VERIFIC_VERILOG_STANDARD"] = "-vlog2k"
|
|
if "VERIFIC_SYSTEMVERILOG_STANDARD" not in ys_params:
|
|
ys_params["VERIFIC_SYSTEMVERILOG_STANDARD"] = "-sv"
|
|
if "VERIFIC_VHDL_STANDARD" not in ys_params:
|
|
ys_params["VERIFIC_VHDL_STANDARD"] = "-vhdl"
|
|
ext_to_standard_map = {
|
|
".v": ys_params["VERIFIC_VERILOG_STANDARD"],
|
|
".vh": ys_params["VERIFIC_VERILOG_STANDARD"],
|
|
".verilog": ys_params["VERIFIC_VERILOG_STANDARD"],
|
|
".vlg": ys_params["VERIFIC_VERILOG_STANDARD"],
|
|
".sv": ys_params["VERIFIC_SYSTEMVERILOG_STANDARD"],
|
|
".svh": ys_params["VERIFIC_SYSTEMVERILOG_STANDARD"],
|
|
".vhd": ys_params["VERIFIC_VHDL_STANDARD"],
|
|
".vhdl": ys_params["VERIFIC_VHDL_STANDARD"],
|
|
}
|
|
lib_files = []
|
|
include_dirs = set([os.path.dirname(eachfile) for eachfile in args.benchmark_files])
|
|
if "VERIFIC_INCLUDE_DIR" in ys_params:
|
|
include_dirs.update(ys_params["VERIFIC_INCLUDE_DIR"].split(","))
|
|
if include_dirs and not ys_params["ADD_INCLUDE_DIR"]:
|
|
ys_params["ADD_INCLUDE_DIR"] = "\n".join(
|
|
["verific -vlog-incdir " + shlex.quote(eachdir) for eachdir in include_dirs]
|
|
)
|
|
if "VERIFIC_LIBRARY_DIR" in ys_params:
|
|
ys_params["ADD_LIBRARY_DIR"] = "\n".join(
|
|
[
|
|
"verific -vlog-libdir " + shlex.quote(eachdir)
|
|
for eachdir in ys_params["VERIFIC_LIBRARY_DIR"].split(",")
|
|
]
|
|
)
|
|
try:
|
|
for param, value in ys_params.items():
|
|
if param.startswith("VERIFIC_READ_LIB_NAME"):
|
|
index = param[len("VERIFIC_READ_LIB_NAME") :]
|
|
src_param = "VERIFIC_READ_LIB_SRC" + index
|
|
if src_param in ys_params:
|
|
src_files = []
|
|
for name in ys_params[src_param].split(","):
|
|
for eachfile in args.benchmark_files:
|
|
if name.strip() in eachfile:
|
|
src_files.append(eachfile)
|
|
break
|
|
if not src_files:
|
|
clean_up_and_exit("Failed to locate verific library files")
|
|
lib_files.extend(src_files)
|
|
filename, file_extension = os.path.splitext(src_files[0])
|
|
ys_params["READ_LIBRARY"] += " ".join(
|
|
["verific -work", ys_params[param], ext_to_standard_map[file_extension]]
|
|
+ [shlex.quote(eachfile) for eachfile in src_files]
|
|
+ ["\n"]
|
|
)
|
|
standard_to_sources = {}
|
|
for eachfile in args.benchmark_files:
|
|
if eachfile in lib_files:
|
|
continue
|
|
filename, file_extension = os.path.splitext(eachfile)
|
|
if ext_to_standard_map[file_extension] in standard_to_sources:
|
|
standard_to_sources[ext_to_standard_map[file_extension]].append(eachfile)
|
|
else:
|
|
standard_to_sources[ext_to_standard_map[file_extension]] = [eachfile]
|
|
for standard, sources in standard_to_sources.items():
|
|
ys_params["READ_HDL_FILE"] += " ".join(
|
|
[
|
|
"verific",
|
|
"-L " + ys_params["VERIFIC_SEARCH_LIB"]
|
|
if "VERIFIC_SEARCH_LIB" in ys_params
|
|
else "",
|
|
standard,
|
|
" ".join([shlex.quote(src) for src in sources]),
|
|
"\n",
|
|
]
|
|
)
|
|
except:
|
|
logger.exception("Failed to determine design file type")
|
|
clean_up_and_exit("")
|
|
if "YOSYS_CELL_SIM_VERILOG" in ys_params:
|
|
ys_params["READ_HDL_FILE"] += " ".join(
|
|
[
|
|
"verific",
|
|
ys_params["VERIFIC_VERILOG_STANDARD"],
|
|
ys_params["YOSYS_CELL_SIM_VERILOG"],
|
|
"\n",
|
|
]
|
|
)
|
|
if "YOSYS_CELL_SIM_SYSTEMVERILOG" in ys_params:
|
|
ys_params["READ_HDL_FILE"] += " ".join(
|
|
[
|
|
"verific",
|
|
ys_params["VERIFIC_SYSTEMVERILOG_STANDARD"],
|
|
ys_params["YOSYS_CELL_SIM_SYSTEMVERILOG"],
|
|
"\n",
|
|
]
|
|
)
|
|
if "YOSYS_CELL_SIM_VHDL" in ys_params:
|
|
ys_params["READ_HDL_FILE"] += " ".join(
|
|
[
|
|
"verific",
|
|
ys_params["VERIFIC_VHDL_STANDARD"],
|
|
ys_params["YOSYS_CELL_SIM_VHDL"],
|
|
"\n",
|
|
]
|
|
)
|
|
if "YOSYS_BLACKBOX_MODULES" in ys_params:
|
|
ys_params["ADD_BLACKBOX_MODULES"] = "blackbox " + " ".join(
|
|
["\\" + mod for mod in ys_params["YOSYS_BLACKBOX_MODULES"].split(",")]
|
|
)
|
|
|
|
ys_params["TOP_MODULE"] = args.top_module
|
|
ys_params["LUT_SIZE"] = lut_size
|
|
ys_params["OUTPUT_BLIF"] = args.top_module + "_yosys_out.blif"
|
|
ys_params["OUTPUT_VERILOG"] = args.top_module + "_output_verilog.v"
|
|
|
|
return ys_params
|
|
|
|
|
|
def run_yosys_with_abc():
|
|
"""
|
|
Execute yosys with ABC and optional blackbox support
|
|
"""
|
|
ys_params = create_yosys_params()
|
|
yosys_template = (
|
|
args.yosys_tmpl
|
|
if args.yosys_tmpl
|
|
else os.path.join(cad_tools["misc_dir"], "ys_tmpl_yosys_vpr_flow.ys")
|
|
)
|
|
tmpl = Template(open(yosys_template, encoding="utf-8").read())
|
|
with open("yosys.ys", "w") as archfile:
|
|
archfile.write(tmpl.safe_substitute(ys_params))
|
|
|
|
run_command("Run yosys", "yosys_output.log", [cad_tools["yosys_path"], "yosys.ys"])
|
|
|
|
|
|
def run_odin2():
|
|
pass
|
|
|
|
|
|
def run_abc_vtr():
|
|
pass
|
|
|
|
|
|
def run_abc_for_standarad():
|
|
pass
|
|
|
|
|
|
def run_ace2():
|
|
if args.black_box_ace:
|
|
with open(args.top_module + "_yosys_out.blif", "r") as fp:
|
|
blif_lines = fp.readlines()
|
|
|
|
with open(args.top_module + "_bb.blif", "w") as fp:
|
|
for eachline in blif_lines:
|
|
if ".names" in eachline:
|
|
input_nets = eachline.split()[1:]
|
|
if len(input_nets) - 1 > args.K:
|
|
logger.error("One module in blif have more inputs" + " than K value")
|
|
# Map CEll to each logic in blif
|
|
map_nets = (input_nets[:-1] + ["unconn"] * args.K)[: args.K]
|
|
map_nets = [
|
|
"I[%d]=%s" % (indx, eachnet) for indx, eachnet in enumerate(map_nets)
|
|
]
|
|
map_nets += ["O[0]=%s\n" % input_nets[-1]]
|
|
fp.write(".subckt CELL ")
|
|
fp.write(" ".join(map_nets))
|
|
else:
|
|
fp.write(eachline)
|
|
|
|
declar_input = " ".join(["I[%d]" % i for i in range(args.K)])
|
|
model_tmpl = (
|
|
"\n"
|
|
+ ".model CELL\n"
|
|
+ ".inputs "
|
|
+ declar_input
|
|
+ " \n"
|
|
+ ".outputs O[0]\n"
|
|
+ ".blackbox\n"
|
|
+ ".end\n"
|
|
)
|
|
fp.write(model_tmpl)
|
|
# Prepare ACE run command
|
|
command = [
|
|
"-b",
|
|
args.top_module + ("_bb.blif" if args.black_box_ace else "_yosys_out.blif"),
|
|
"-o",
|
|
args.top_module + "_ace_out.act",
|
|
"-n",
|
|
args.top_module + "_ace_out.blif",
|
|
"-c",
|
|
"clk",
|
|
]
|
|
command += ["-d", "%.4f" % args.ace_d] if args.ace_d else [""]
|
|
command += ["-p", "%.4f" % args.ace_d] if args.ace_p else [""]
|
|
try:
|
|
filename = args.top_module + "_ace2_output.txt"
|
|
with open(filename, "w+") as output:
|
|
process = subprocess.run(
|
|
[cad_tools["ace_path"]] + command,
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
output.write(process.stdout)
|
|
if process.returncode:
|
|
logger.info("ACE2 failed with returncode %d", process.returncode)
|
|
raise subprocess.CalledProcessError(0, command)
|
|
except:
|
|
logger.exception("Failed to run ACE2")
|
|
clean_up_and_exit("")
|
|
logger.info("ACE2 output is written in file %s" % filename)
|
|
|
|
|
|
def run_pro_blif_3arg():
|
|
command = [
|
|
"-i",
|
|
args.top_module + "_ace_out.blif",
|
|
"-o",
|
|
args.top_module + ".blif",
|
|
"-initial_blif",
|
|
args.top_module + "_yosys_out.blif",
|
|
]
|
|
try:
|
|
filename = args.top_module + "_blif_3args_output.txt"
|
|
with open(filename, "w+") as output:
|
|
process = subprocess.run(
|
|
["perl", cad_tools["pro_blif_path"]] + command,
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
output.write(process.stdout)
|
|
if process.returncode:
|
|
logger.info("blif_3args script failed with returncode %d", process.returncode)
|
|
except:
|
|
logger.exception("Failed to run blif_3args")
|
|
clean_up_and_exit("")
|
|
logger.info("blif_3args output is written in file %s" % filename)
|
|
|
|
|
|
def collect_files_for_vpr():
|
|
# Sanitize provided Benchmark option
|
|
if len(args.benchmark_files) > 1:
|
|
logger.error("Expecting Single Benchmark Blif file.")
|
|
if not os.path.isfile(args.benchmark_files[0] or ""):
|
|
clean_up_and_exit("Provided Blif file not found")
|
|
shutil.copy(args.benchmark_files[0], args.top_module + ".blif")
|
|
|
|
# Sanitize provided Activity file option
|
|
if args.power:
|
|
if not os.path.isfile(args.activity_file or ""):
|
|
logger.error("Activity File - %s" % args.activity_file)
|
|
clean_up_and_exit("Provided activity file not found")
|
|
shutil.copy(args.activity_file, args.top_module + "_ace_out.act")
|
|
else:
|
|
if os.path.isfile(args.activity_file):
|
|
shutil.copy(args.activity_file, args.top_module + "_ace_out.act")
|
|
|
|
# Sanitize provided Benchmark option
|
|
if not os.path.isfile(args.base_verilog or ""):
|
|
logger.error("Base Verilog File - %s" % args.base_verilog)
|
|
clean_up_and_exit("Provided base_verilog file not found")
|
|
shutil.copy(args.base_verilog, args.top_module + "_output_verilog.v")
|
|
|
|
|
|
def run_openfpga_shell():
|
|
ExecTime["VPRStart"] = time.time()
|
|
# bench_blif, fixed_chan_width, logfile, route_only=False
|
|
tmpl = Template(open(args.top_module + "_template.openfpga", encoding="utf-8").read())
|
|
|
|
path_variables = script_env_vars["PATH"]
|
|
path_variables["TOP_MODULE"] = args.top_module
|
|
path_variables["VPR_ARCH_FILE"] = args.arch_file
|
|
path_variables["OPENFPGA_ARCH_FILE"] = args.openfpga_arch_file
|
|
path_variables["VPR_TESTBENCH_BLIF"] = args.top_module + ".blif"
|
|
path_variables["ACTIVITY_FILE"] = args.top_module + "_ace_out.act"
|
|
path_variables["REFERENCE_VERILOG_TESTBENCH"] = args.top_module + "_output_verilog.v"
|
|
|
|
for indx in range(0, len(OpenFPGAArgs), 2):
|
|
tmpVar = OpenFPGAArgs[indx][2:].upper()
|
|
path_variables[tmpVar] = OpenFPGAArgs[indx + 1]
|
|
|
|
with open(args.top_module + "_run.openfpga", "w", encoding="utf-8") as archfile:
|
|
archfile.write(tmpl.safe_substitute(path_variables))
|
|
command = [cad_tools["openfpga_shell_path"], "-batch", "-f", args.top_module + "_run.openfpga"]
|
|
run_command("OpenFPGA Shell Run", "openfpgashell.log", command)
|
|
ExecTime["VPREnd"] = time.time()
|
|
extract_vpr_stats("openfpgashell.log")
|
|
|
|
|
|
def extract_vpr_stats(logfile, r_filename="vpr_stat", parse_section="vpr"):
|
|
section = (
|
|
"DEFAULT_PARSE_RESULT_POWER" if parse_section == "power" else "DEFAULT_PARSE_RESULT_VPR"
|
|
)
|
|
vpr_log = open(logfile).read()
|
|
resultDict = {}
|
|
for name, value in config.items(section):
|
|
reg_string, filt_function = value.split(",")
|
|
match = re.search(reg_string[1:-1], vpr_log)
|
|
if match:
|
|
try:
|
|
if "lambda" in filt_function.strip():
|
|
eval("ParseFunction = " + filt_function.strip())
|
|
extract_val = ParseFunction(**match.groups())
|
|
elif filt_function.strip() == "int":
|
|
extract_val = int(match.group(1))
|
|
elif filt_function.strip() == "float":
|
|
extract_val = float(match.group(1))
|
|
elif filt_function.strip() == "str":
|
|
extract_val = str(match.group(1))
|
|
elif filt_function.strip() == "scientific":
|
|
try:
|
|
mult = {
|
|
"m": 1e-3,
|
|
"u": 1e-6,
|
|
"n": 1e-9,
|
|
"K": 1e-3,
|
|
"M": 1e-6,
|
|
"G": 1e-9,
|
|
}.get(match.group(2)[0], 1)
|
|
except:
|
|
mult = 1
|
|
extract_val = float(match.group(1)) * mult
|
|
else:
|
|
extract_val = match.group(1)
|
|
except:
|
|
logger.exception("Filter failed")
|
|
extract_val = "Filter Failed"
|
|
resultDict[name] = extract_val
|
|
|
|
dummyparser = ConfigParser()
|
|
dummyparser.read_dict({"RESULTS": resultDict})
|
|
|
|
with open(r_filename + ".result", "w") as configfile:
|
|
dummyparser.write(configfile)
|
|
logger.info("%s result extracted in file %s" % (parse_section, r_filename + ".result"))
|
|
|
|
|
|
def run_rewrite_verilog():
|
|
# Rewrite the verilog after optimization
|
|
# If there is no template script provided, use a default template
|
|
# If there is a template script provided, replace parameters from configuration
|
|
if not args.ys_rewrite_tmpl:
|
|
script_cmd = [
|
|
"read_blif %s" % args.top_module + ".blif",
|
|
"write_verilog %s" % args.top_module + "_output_verilog.v",
|
|
]
|
|
command = [cad_tools["yosys_path"], "-p", "; ".join(script_cmd)]
|
|
run_command("Yosys", "yosys_rewrite.log", command)
|
|
else:
|
|
# Yosys script parameter mapping
|
|
ys_rewrite_params = create_yosys_params()
|
|
|
|
# Split a series of scripts by delim ';'
|
|
# And execute the scripts serially
|
|
for iteration_idx, curr_rewrite_tmpl in enumerate(args.ys_rewrite_tmpl.split(";")):
|
|
tmpl = Template(open(curr_rewrite_tmpl, encoding="utf-8").read())
|
|
logger.info("Yosys rewrite iteration: " + str(iteration_idx))
|
|
with open("yosys_rewrite_" + str(iteration_idx) + ".ys", "w") as archfile:
|
|
archfile.write(tmpl.safe_substitute(ys_rewrite_params))
|
|
run_command(
|
|
"Run yosys",
|
|
"yosys_rewrite_output_" + str(iteration_idx) + ".log",
|
|
[cad_tools["yosys_path"], "yosys_rewrite_" + str(iteration_idx) + ".ys"],
|
|
)
|
|
|
|
|
|
def run_netlists_verification(exit_if_fail=True):
|
|
ExecTime["VerificationStart"] = time.time()
|
|
compiled_file = "compiled_" + args.top_module
|
|
# include_netlists = args.top_module+"_include_netlists.v"
|
|
tb_top_formal = args.top_module + "_top_formal_verification_random_tb"
|
|
tb_top_autochecked = args.top_module + "_autocheck_top_tb"
|
|
# netlists_path = args.vpr_fpga_verilog_dir_val+"/SRC/"
|
|
|
|
command = [cad_tools["iverilog_path"]]
|
|
command += ["-o", compiled_file]
|
|
|
|
command += ["./SRC/%s_include_netlists.v" % args.top_module]
|
|
command += ["-s"]
|
|
if args.vpr_fpga_verilog_formal_verification_top_netlist:
|
|
command += [tb_top_formal]
|
|
else:
|
|
command += [tb_top_autochecked]
|
|
# TODO: This is NOT flexible!!! We should consider to make the include directory customizable through options
|
|
# Add source directory to the include dir
|
|
command += ["-I", "./SRC"]
|
|
run_command("iverilog_verification", "iverilog_output.txt", command)
|
|
|
|
vvp_command = ["vvp", compiled_file]
|
|
output = run_command("vvp_verification", "vvp_sim_output.txt", vvp_command)
|
|
if "Succeed" in output:
|
|
logger.info("VVP Simulation Successful")
|
|
else:
|
|
logger.error(str(output).split("\n")[-1])
|
|
if exit_if_fail:
|
|
clean_up_and_exit("Failed to run VVP verification")
|
|
ExecTime["VerificationEnd"] = time.time()
|
|
|
|
|
|
def run_command(taskname, logfile, command, exit_if_fail=True):
|
|
logger.info("Launching %s " % taskname)
|
|
with open(logfile, "w") as output:
|
|
try:
|
|
output.write(" ".join(command) + "\n")
|
|
process = subprocess.run(
|
|
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
|
|
)
|
|
output.write(process.stdout)
|
|
output.write(process.stderr)
|
|
output.write(str(process.returncode))
|
|
if "openfpgashell" in logfile:
|
|
filter_openfpga_output(process.stdout)
|
|
if process.returncode:
|
|
logger.error("%s run failed with returncode %d" % (taskname, process.returncode))
|
|
logger.error("command %s" % " ".join(command))
|
|
filter_failed_process_output(process.stderr)
|
|
if exit_if_fail:
|
|
clean_up_and_exit("Failed to run %s task" % taskname)
|
|
except Exception:
|
|
logger.exception("%s failed to execute" % (taskname))
|
|
traceback.print_exc(file=output)
|
|
if exit_if_fail:
|
|
clean_up_and_exit("Failed to run %s task" % taskname)
|
|
logger.info("%s is written in file %s" % (taskname, logfile))
|
|
return process.stdout
|
|
|
|
|
|
def filter_openfpga_output(vpr_output):
|
|
stdout = iter(vpr_output.split("\n"))
|
|
try:
|
|
for i in range(50):
|
|
if "Version:" in next(stdout):
|
|
logger.info("OpenFPGAShell %s %s" % (next(stdout), next(stdout)))
|
|
break
|
|
except StopIteration:
|
|
pass
|
|
|
|
|
|
def filter_failed_process_output(vpr_output):
|
|
for line in vpr_output.split("\n"):
|
|
elements_to_log = ["error", "what()"]
|
|
if any(match in line.lower() for match in elements_to_log):
|
|
logger.error("-->>" + line)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ExecTime["Start"] = time.time()
|
|
# args = parser.parse_args()
|
|
args, OpenFPGAArgs = parser.parse_known_args()
|
|
main()
|