diff --git a/doc/src/vpr/command_line_usage.rst b/doc/src/vpr/command_line_usage.rst index 04b0e543538..6342f89fbea 100644 --- a/doc/src/vpr/command_line_usage.rst +++ b/doc/src/vpr/command_line_usage.rst @@ -626,6 +626,12 @@ For people not working on CAD, you can probably leave all the options to their d **Default:** ``2`` +.. option:: --write_block_usage + + Writes out to the file under path cluster-level block usage summary in machine + readable (JSON or XML) or human readable (TXT) format. Format is selected + based on the extension of . + .. _placer_options: Placer Options @@ -1035,6 +1041,17 @@ VPR uses a negotiated congestion algorithm (based on Pathfinder) to perform rout **Default:** ``off`` +.. option:: --write_timing_summary + + Writes out to the file under path final timing summary in machine + readable (JSON or XML) or human readable (TXT) format. Format is selected + based on the extension of . The summary consists of parameters: + + * `cpd` - Final critical path delay (least slack) [ns] + * `fmax` - Maximal frequency of the implemented circuit [MHz] + * `swns` - setup Worst Negative Slack (sWNS) [ns] + * `stns` - Setup Total Negative Slack (sTNS) [ns] + .. _timing_driven_router_options: Timing-Driven Router Options diff --git a/doc/src/vpr/file_formats.rst b/doc/src/vpr/file_formats.rst index 3fcad556dce..dc215aed888 100644 --- a/doc/src/vpr/file_formats.rst +++ b/doc/src/vpr/file_formats.rst @@ -1046,3 +1046,159 @@ An example of what a generated routing resource graph file would look like is sh .. _end: + +Block types usage summary (.txt .xml or .json) +----------------------------------------- + +Block types usage summary is a file written in human or machine readable format. +It describes types and the amount of cluster-level FPGA resources that are used +by implemented design. This file is generated after the placement step with +option: `--write_block_usage `. It can be saved as a human readable +text file or in XML or JSON file to provide machine readable output. Format is +selected based on the extension of the ``. + +The summary consists of 4 parameters: + +* `nets number` - the amount of created nets +* `blocks number` - sum of blocks used to implement the design +* `input pins` - sum of input pins +* `output pins` - sum of output pins + +and a list of `block types` followed by the number of specific block types that +are used in the design. + +TXT +~~~ + +Presents the information in human readable format, the same as in log output: + +.. code-block:: none + :caption: TXT format of block types usage summary + :linenos: + + Netlist num_nets: + Netlist num_blocks: + Netlist blocks: + Netlist blocks: + ... + Netlist blocks: + Netlist inputs pins: + Netlist output pins: + +.. _end: + +JSON +~~~~ + +One of two available machine readable formats. The information is written as follows: + +.. code-block:: json + :caption: JSON format of block types usage summary + :linenos: + + { + "num_nets": "", + "num_blocks": "", + "input_pins": "", + "output_pins": "", + "blocks": { + "": , + "": , + ... + "": + } + } + +.. _end: + +XML +~~~ + +Second machine readable format. The information is written as follows: + +.. code-block:: xml + :caption: XML format of block types usage summary + :linenos: + + + + + + + + ... + + + + + + +.. _end: + +Timing summary (.txt .xml or .json) +----------------------------------------- + +Timing summary is a file written in human or machine readable format. +It describes final timing parameters of design implemented for the FPGA device. +This file is generated after the routing step with option: `--write_timing_summary `. +It can be saved as a human readable text file or in XML or JSON file to provide +machine readable output. Format is selected based on the extension of the ``. + +The summary consists of 4 parameters: + +* `Critical Path Delay (cpd) [ns]` +* `Max Circuit Frequency (Fmax) [MHz]` +* `setup Worst Negative Slack (sWNS) [ns]` +* `setup Total Negative Slack (sTNS) [ns]` + +TXT +~~~ + +Presents the information in human readable format, the same as in log output: + +.. code-block:: none + :caption: TXT format of timing summary + :linenos: + + Final critical path delay (least slack): ns, Fmax: MHz + Final setup Worst Negative Slack (sWNS): ns + Final setup Total Negative Slack (sTNS): ns + +.. _end: + +JSON +~~~~ + +One of two available machine readable formats. The information is written as follows: + +.. code-block:: json + :caption: JSON format of timing summary + :linenos: + + { + "cpd": , + "fmax": , + "swns": , + "stns": + } + +.. _end: + +XML +~~~ + +Second machine readable format. The information is written as follows: + +.. code-block:: xml + :caption: XML format of timing summary + :linenos: + + + + + + + + + +.. _end: diff --git a/vpr/src/analysis/timing_reports.cpp b/vpr/src/analysis/timing_reports.cpp index 1b759c6ecb2..eb6861dbafc 100644 --- a/vpr/src/analysis/timing_reports.cpp +++ b/vpr/src/analysis/timing_reports.cpp @@ -16,7 +16,7 @@ void generate_setup_timing_stats(const std::string& prefix, const SetupTimingInf auto& timing_ctx = g_vpr_ctx.timing(); auto& atom_ctx = g_vpr_ctx.atom(); - print_setup_timing_summary(*timing_ctx.constraints, *timing_info.setup_analyzer(), "Final "); + print_setup_timing_summary(*timing_ctx.constraints, *timing_info.setup_analyzer(), "Final ", analysis_opts.write_timing_summary); VprTimingGraphResolver resolver(atom_ctx.nlist, atom_ctx.lookup, *timing_ctx.graph, delay_calc); resolver.set_detail_level(analysis_opts.timing_report_detail); diff --git a/vpr/src/base/SetupVPR.cpp b/vpr/src/base/SetupVPR.cpp index 8810edb0c6c..e1599c4ae1d 100644 --- a/vpr/src/base/SetupVPR.cpp +++ b/vpr/src/base/SetupVPR.cpp @@ -98,6 +98,7 @@ void SetupVPR(const t_options* Options, FileNameOpts->out_file_prefix = Options->out_file_prefix; FileNameOpts->read_vpr_constraints_file = Options->read_vpr_constraints_file; FileNameOpts->write_vpr_constraints_file = Options->write_vpr_constraints_file; + FileNameOpts->write_block_usage = Options->write_block_usage; FileNameOpts->verify_file_digests = Options->verify_file_digests; @@ -630,6 +631,7 @@ static void SetupAnalysisOpts(const t_options& Options, t_analysis_opts& analysi analysis_opts.post_synth_netlist_unconn_output_handling = Options.post_synth_netlist_unconn_output_handling; analysis_opts.timing_update_type = Options.timing_update_type; + analysis_opts.write_timing_summary = Options.write_timing_summary; } static void SetupPowerOpts(const t_options& Options, t_power_opts* power_opts, t_arch* Arch) { diff --git a/vpr/src/base/ShowSetup.cpp b/vpr/src/base/ShowSetup.cpp index c50afc01a68..782038c4a31 100644 --- a/vpr/src/base/ShowSetup.cpp +++ b/vpr/src/base/ShowSetup.cpp @@ -1,3 +1,5 @@ +#include + #include "vtr_assert.h" #include "vtr_log.h" #include "vtr_memory.h" @@ -61,21 +63,70 @@ void ShowSetup(const t_vpr_setup& vpr_setup) { } } -void printClusteredNetlistStats() { - auto& device_ctx = g_vpr_ctx.device(); - auto& cluster_ctx = g_vpr_ctx.clustering(); +void ClusteredNetlistStats::writeHuman(std::ostream& output) const { + output << "Cluster level netlist and block usage statistics\n"; + output << "Netlist num_nets: " << num_nets << "\n"; + output << "Netlist num_blocks: " << num_blocks << "\n"; + for (const auto& type : logical_block_types) { + output << "Netlist " << type.name << " blocks: " << num_blocks_type[type.index] << ".\n"; + } + + output << "Netlist inputs pins: " << L_num_p_inputs << "\n"; + output << "Netlist output pins: " << L_num_p_outputs << "\n"; +} +void ClusteredNetlistStats::writeJSON(std::ostream& output) const { + output << "{\n"; - int j, L_num_p_inputs, L_num_p_outputs; - std::vector num_blocks_type(device_ctx.logical_block_types.size(), 0); + output << " \"num_nets\": \"" << num_nets << "\",\n"; + output << " \"num_blocks\": \"" << num_blocks << "\",\n"; - VTR_LOG("\n"); - VTR_LOG("Netlist num_nets: %d\n", (int)cluster_ctx.clb_nlist.nets().size()); - VTR_LOG("Netlist num_blocks: %d\n", (int)cluster_ctx.clb_nlist.blocks().size()); + output << " \"input_pins\": \"" << L_num_p_inputs << "\",\n"; + output << " \"output_pins\": \"" << L_num_p_outputs << "\",\n"; - /* Count I/O input and output pads */ + output << " \"blocks\": {\n"; + + for (const auto& type : logical_block_types) { + output << " \"" << type.name << "\": " << num_blocks_type[type.index]; + if ((int)type.index < (int)logical_block_types.size() - 1) + output << ",\n"; + else + output << "\n"; + } + output << " }\n"; + output << "}\n"; +} + +void ClusteredNetlistStats::writeXML(std::ostream& output) const { + output << "\n"; + output << "\n"; + + output << " \n"; + output << " \n"; + + for (const auto& type : logical_block_types) { + output << " \n"; + } + output << " \n"; + + output << " \n"; + output << " \n"; + + output << "\n"; +} + +ClusteredNetlistStats::ClusteredNetlistStats() { + auto& device_ctx = g_vpr_ctx.device(); + auto& cluster_ctx = g_vpr_ctx.clustering(); + + int j; L_num_p_inputs = 0; L_num_p_outputs = 0; + num_blocks_type = std::vector(device_ctx.logical_block_types.size(), 0); + num_nets = (int)cluster_ctx.clb_nlist.nets().size(); + num_blocks = (int)cluster_ctx.clb_nlist.blocks().size(); + logical_block_types = device_ctx.logical_block_types; + /* Count I/O input and output pads */ for (auto blk_id : cluster_ctx.clb_nlist.blocks()) { auto logical_block = cluster_ctx.clb_nlist.block_type(blk_id); auto physical_tile = pick_physical_type(logical_block); @@ -97,16 +148,52 @@ void printClusteredNetlistStats() { } } } +} - for (const auto& type : device_ctx.logical_block_types) { - VTR_LOG("Netlist %s blocks: %d.\n", type.name, num_blocks_type[type.index]); +void ClusteredNetlistStats::write(OutputFormat fmt, std::ostream& output) const { + switch (fmt) { + case HumanReadable: + writeHuman(output); + break; + case JSON: + writeJSON(output); + break; + case XML: + writeXML(output); + break; + default: + VPR_FATAL_ERROR(VPR_ERROR_PACK, + "Unknown extension on in block usage summary file"); + break; } +} - /* Print out each block separately instead */ - VTR_LOG("Netlist inputs pins: %d\n", L_num_p_inputs); - VTR_LOG("Netlist output pins: %d\n", L_num_p_outputs); - VTR_LOG("\n"); - num_blocks_type.clear(); +void writeClusteredNetlistStats(std::string block_usage_filename) { + const auto stats = ClusteredNetlistStats(); + + // Print out the human readable version to stdout + + stats.write(ClusteredNetlistStats::OutputFormat::HumanReadable, std::cout); + + if (block_usage_filename.size() > 0) { + ClusteredNetlistStats::OutputFormat fmt; + + if (vtr::check_file_name_extension(block_usage_filename.c_str(), ".json")) { + fmt = ClusteredNetlistStats::OutputFormat::JSON; + } else if (vtr::check_file_name_extension(block_usage_filename.c_str(), ".xml")) { + fmt = ClusteredNetlistStats::OutputFormat::XML; + } else if (vtr::check_file_name_extension(block_usage_filename.c_str(), ".txt")) { + fmt = ClusteredNetlistStats::OutputFormat::HumanReadable; + } else { + VPR_FATAL_ERROR(VPR_ERROR_PACK, "Unknown extension on output %s", block_usage_filename.c_str()); + } + + std::fstream fp; + + fp.open(block_usage_filename, std::fstream::out | std::fstream::trunc); + stats.write(fmt, fp); + fp.close(); + } } static void ShowAnnealSched(const t_annealing_sched& AnnealSched) { diff --git a/vpr/src/base/ShowSetup.h b/vpr/src/base/ShowSetup.h index 07e71591f14..f843b0c442f 100644 --- a/vpr/src/base/ShowSetup.h +++ b/vpr/src/base/ShowSetup.h @@ -1,7 +1,32 @@ #ifndef SHOWSETUP_H #define SHOWSETUP_H +struct ClusteredNetlistStats { + private: + void writeHuman(std::ostream& output) const; + void writeJSON(std::ostream& output) const; + void writeXML(std::ostream& output) const; + + public: + ClusteredNetlistStats(); + + enum OutputFormat { + HumanReadable, + JSON, + XML + }; + + int num_nets; + int num_blocks; + int L_num_p_inputs; + int L_num_p_outputs; + std::vector num_blocks_type; + std::vector logical_block_types; + + void write(OutputFormat fmt, std::ostream& output) const; +}; + void ShowSetup(const t_vpr_setup& vpr_setup); -void printClusteredNetlistStats(); +void writeClusteredNetlistStats(std::string block_usage_filename); #endif diff --git a/vpr/src/base/read_options.cpp b/vpr/src/base/read_options.cpp index 09f4647ff41..5ffaace39a4 100644 --- a/vpr/src/base/read_options.cpp +++ b/vpr/src/base/read_options.cpp @@ -1560,6 +1560,10 @@ argparse::ArgumentParser create_arg_parser(std::string prog_name, t_options& arg .help("Prefix for output files") .show_in(argparse::ShowIn::HELP_ONLY); + file_grp.add_argument(args.write_block_usage, "--write_block_usage") + .help("Writes the cluster-level block types usage summary to the specified JSON, XML or TXT file.") + .show_in(argparse::ShowIn::HELP_ONLY); + auto& netlist_grp = parser.add_argument_group("netlist options"); netlist_grp.add_argument(args.absorb_buffer_luts, "--absorb_buffer_luts") @@ -2564,6 +2568,10 @@ argparse::ArgumentParser create_arg_parser(std::string prog_name, t_options& arg .default_value("unconnected") .show_in(argparse::ShowIn::HELP_ONLY); + analysis_grp.add_argument(args.write_timing_summary, "--write_timing_summary") + .help("Writes implemented design final timing summary to the specified JSON, XML or TXT file.") + .show_in(argparse::ShowIn::HELP_ONLY); + auto& power_grp = parser.add_argument_group("power analysis options"); power_grp.add_argument(args.do_power, "--power") diff --git a/vpr/src/base/read_options.h b/vpr/src/base/read_options.h index 922699d2c93..94e8ac797c0 100644 --- a/vpr/src/base/read_options.h +++ b/vpr/src/base/read_options.h @@ -36,6 +36,8 @@ struct t_options { argparse::ArgValue write_router_lookahead; argparse::ArgValue read_router_lookahead; + argparse::ArgValue write_block_usage; + /* Stage Options */ argparse::ArgValue do_packing; argparse::ArgValue do_placement; @@ -210,6 +212,7 @@ struct t_options { argparse::ArgValue echo_dot_timing_graph_node; argparse::ArgValue post_synth_netlist_unconn_input_handling; argparse::ArgValue post_synth_netlist_unconn_output_handling; + argparse::ArgValue write_timing_summary; }; argparse::ArgumentParser create_arg_parser(std::string prog_name, t_options& args); diff --git a/vpr/src/base/vpr_api.cpp b/vpr/src/base/vpr_api.cpp index 529d0cc60f0..dc589d75b8d 100644 --- a/vpr/src/base/vpr_api.cpp +++ b/vpr/src/base/vpr_api.cpp @@ -516,8 +516,8 @@ bool vpr_pack_flow(t_vpr_setup& vpr_setup, const t_arch& arch) { /* Sanity check the resulting netlist */ check_netlist(packer_opts.pack_verbosity); - /* Output the netlist stats to console. */ - printClusteredNetlistStats(); + /* Output the netlist stats to console and optionally to file. */ + writeClusteredNetlistStats(vpr_setup.FileNameOpts.write_block_usage.c_str()); // print the total number of used physical blocks for each // physical block type after finishing the packing stage diff --git a/vpr/src/base/vpr_types.h b/vpr/src/base/vpr_types.h index 094841d2a1f..8668ed32b16 100644 --- a/vpr/src/base/vpr_types.h +++ b/vpr/src/base/vpr_types.h @@ -753,6 +753,7 @@ struct t_file_name_opts { std::string out_file_prefix; std::string read_vpr_constraints_file; std::string write_vpr_constraints_file; + std::string write_block_usage; bool verify_file_digests; }; @@ -1298,6 +1299,7 @@ struct t_analysis_opts { e_timing_report_detail timing_report_detail; bool timing_report_skew; std::string echo_dot_timing_graph_node; + std::string write_timing_summary; e_timing_update_type timing_update_type; }; diff --git a/vpr/src/place/place.cpp b/vpr/src/place/place.cpp index 7b509ca7bb4..6f838add308 100644 --- a/vpr/src/place/place.cpp +++ b/vpr/src/place/place.cpp @@ -950,7 +950,7 @@ void try_place(const t_placer_opts& placer_opts, /* Print critical path delay metrics */ VTR_LOG("\n"); print_setup_timing_summary(*timing_ctx.constraints, - *timing_info->setup_analyzer(), "Placement estimated "); + *timing_info->setup_analyzer(), "Placement estimated ", ""); } sprintf(msg, diff --git a/vpr/src/timing/timing_util.cpp b/vpr/src/timing/timing_util.cpp index d1da2fbc164..0885c6b3638 100644 --- a/vpr/src/timing/timing_util.cpp +++ b/vpr/src/timing/timing_util.cpp @@ -191,22 +191,118 @@ std::vector create_criticality_histogram(const SetupTimingInfo& return histogram; } -void print_setup_timing_summary(const tatum::TimingConstraints& constraints, const tatum::SetupTimingAnalyzer& setup_analyzer, std::string prefix) { +void TimingStats::writeHuman(std::ostream& output) const { + output << prefix << "critical path delay (least slack): " << least_slack_cpd_delay << " ns"; + + //Fmax is only meaningful for a single-clock circuit + output << ", Fmax: " << fmax << " MHz"; + output << "\n"; + + output << prefix << "setup Worst Negative Slack (sWNS): " << setup_worst_neg_slack << " ns\n"; + output << prefix << "setup Total Negative Slack (sTNS): " << setup_total_neg_slack << " ns\n"; + output << "\n"; +} +void TimingStats::writeJSON(std::ostream& output) const { + output << "{\n"; + + output << " \"cpd\": " << least_slack_cpd_delay << ",\n"; + output << " \"fmax\": " << fmax << ",\n"; + + output << " \"swns\": " << setup_worst_neg_slack << ",\n"; + output << " \"stns\": " << setup_total_neg_slack << "\n"; + output << "}\n"; +} + +void TimingStats::writeXML(std::ostream& output) const { + output << "\n"; + output << "\n"; + + output << " \n"; + output << " \n"; + output << " \n"; + output << " \n"; + + output << "\n"; +} + +TimingStats::TimingStats(std::string pref, double cpd, double f_max, double swns, double stns) { + least_slack_cpd_delay = cpd; + fmax = f_max; + setup_worst_neg_slack = swns; + setup_total_neg_slack = stns; + prefix = pref; +} + +void TimingStats::write(OutputFormat fmt, std::ostream& output) const { + switch (fmt) { + case HumanReadable: + writeHuman(output); + break; + case JSON: + writeJSON(output); + break; + case XML: + writeXML(output); + break; + default: + VPR_FATAL_ERROR(VPR_ERROR_PACK, + "Unknown extension on in timing summary file"); + break; + } +} + +void write_setup_timing_summary(std::string timing_summary_filename, const TimingStats& stats) { + if (timing_summary_filename.size() > 0) { + TimingStats::OutputFormat fmt; + + if (vtr::check_file_name_extension(timing_summary_filename.c_str(), ".json")) { + fmt = TimingStats::OutputFormat::JSON; + } else if (vtr::check_file_name_extension(timing_summary_filename.c_str(), ".xml")) { + fmt = TimingStats::OutputFormat::XML; + } else if (vtr::check_file_name_extension(timing_summary_filename.c_str(), ".txt")) { + fmt = TimingStats::OutputFormat::HumanReadable; + } else { + VPR_FATAL_ERROR(VPR_ERROR_PACK, "Unknown extension on output %s", timing_summary_filename.c_str()); + } + + std::fstream fp; + + fp.open(timing_summary_filename, std::fstream::out | std::fstream::trunc); + stats.write(fmt, fp); + fp.close(); + } +} + +void print_setup_timing_summary(const tatum::TimingConstraints& constraints, + const tatum::SetupTimingAnalyzer& setup_analyzer, + std::string prefix, + std::string timing_summary_filename) { auto& timing_ctx = g_vpr_ctx.timing(); auto crit_paths = tatum::find_critical_paths(*timing_ctx.graph, constraints, setup_analyzer); auto least_slack_cpd = find_least_slack_critical_path_delay(constraints, setup_analyzer); - VTR_LOG("%scritical path delay (least slack): %g ns", prefix.c_str(), sec_to_nanosec(least_slack_cpd.delay())); + + double least_slack_cpd_delay = sec_to_nanosec(least_slack_cpd.delay()); + double fmax = sec_to_mhz(least_slack_cpd.delay()); + double setup_worst_neg_slack = sec_to_nanosec(find_setup_worst_negative_slack(setup_analyzer)); + double setup_total_neg_slack = sec_to_nanosec(find_setup_total_negative_slack(setup_analyzer)); + + const auto stats = TimingStats(prefix, least_slack_cpd_delay, fmax, + setup_worst_neg_slack, setup_total_neg_slack); + if (!timing_summary_filename.empty()) + write_setup_timing_summary(timing_summary_filename, stats); + + VTR_LOG("%scritical path delay (least slack): %g ns", prefix.c_str(), least_slack_cpd_delay); if (crit_paths.size() == 1) { //Fmax is only meaningful for a single-clock circuit - VTR_LOG(", Fmax: %g MHz", sec_to_mhz(least_slack_cpd.delay())); + VTR_LOG(", Fmax: %g MHz", fmax); } VTR_LOG("\n"); - VTR_LOG("%ssetup Worst Negative Slack (sWNS): %g ns\n", prefix.c_str(), sec_to_nanosec(find_setup_worst_negative_slack(setup_analyzer))); - VTR_LOG("%ssetup Total Negative Slack (sTNS): %g ns\n", prefix.c_str(), sec_to_nanosec(find_setup_total_negative_slack(setup_analyzer))); + VTR_LOG("%ssetup Worst Negative Slack (sWNS): %g ns\n", prefix.c_str(), setup_worst_neg_slack); + VTR_LOG("%ssetup Total Negative Slack (sTNS): %g ns\n", prefix.c_str(), setup_total_neg_slack); VTR_LOG("\n"); VTR_LOG("%ssetup slack histogram:\n", prefix.c_str()); diff --git a/vpr/src/timing/timing_util.h b/vpr/src/timing/timing_util.h index 682771e9763..add3c1cd60a 100644 --- a/vpr/src/timing/timing_util.h +++ b/vpr/src/timing/timing_util.h @@ -48,7 +48,7 @@ std::vector create_criticality_histogram(const SetupTimingInfo& size_t num_bins = 10); //Print a useful summary of timing information -void print_setup_timing_summary(const tatum::TimingConstraints& constraints, const tatum::SetupTimingAnalyzer& setup_analyzer, std::string prefix); +void print_setup_timing_summary(const tatum::TimingConstraints& constraints, const tatum::SetupTimingAnalyzer& setup_analyzer, std::string prefix, std::string timing_summary_filename); /* * Hold-time related statistics @@ -213,4 +213,31 @@ tatum::NodeId pin_name_to_tnode(std::string name); void write_setup_timing_graph_dot(std::string filename, SetupTimingInfo& timing_info, tatum::NodeId debug_node = tatum::NodeId::INVALID()); void write_hold_timing_graph_dot(std::string filename, HoldTimingInfo& timing_info, tatum::NodeId debug_node = tatum::NodeId::INVALID()); +struct TimingStats { + private: + void writeHuman(std::ostream& output) const; + void writeJSON(std::ostream& output) const; + void writeXML(std::ostream& output) const; + + public: + TimingStats(std::string prefix, double least_slack_cpd_delay, double fmax, double setup_worst_neg_slack, double setup_total_neg_slack); + + enum OutputFormat { + HumanReadable, + JSON, + XML + }; + + double least_slack_cpd_delay; + double fmax; + double setup_worst_neg_slack; + double setup_total_neg_slack; + std::string prefix; + + void write(OutputFormat fmt, std::ostream& output) const; +}; + +//Write a useful summary of timing information to JSON file +void write_setup_timing_summary(std::string timing_summary_filename, const TimingStats& stats); + #endif