1 /**
2  * This code is part of Qiskit.
3  *
4  * (C) Copyright IBM 2018, 2019.
5  *
6  * This code is licensed under the Apache License, Version 2.0. You may
7  * obtain a copy of this license in the LICENSE.txt file in the root directory
8  * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9  *
10  * Any modifications or derivative works of this code must retain this
11  * copyright notice, and modified files need to carry a notice indicating
12  * that they have been altered from the originals.
13  */
14 
15 #ifndef _aer_base_controller_hpp_
16 #define _aer_base_controller_hpp_
17 
18 #include <chrono>
19 #include <cstdint>
20 #include <iostream>
21 #include <random>
22 #include <sstream>
23 #include <stdexcept>
24 #include <string>
25 #include <vector>
26 
27 #if defined(__linux__) || defined(__APPLE__)
28    #include <unistd.h>
29 #elif defined(_WIN64)
30    // This is needed because windows.h redefine min()/max() so interferes with std::min/max
31    #define NOMINMAX
32    #include <windows.h>
33 #endif
34 
35 #ifdef _OPENMP
36 #include <omp.h>
37 #endif
38 
39 // Base Controller
40 #include "framework/qobj.hpp"
41 #include "framework/rng.hpp"
42 #include "framework/creg.hpp"
43 #include "framework/results/result.hpp"
44 #include "framework/results/experiment_data.hpp"
45 #include "noise/noise_model.hpp"
46 #include "transpile/basic_opts.hpp"
47 #include "transpile/truncate_qubits.hpp"
48 
49 
50 namespace AER {
51 namespace Base {
52 
53 //=========================================================================
54 // Controller base class
55 //=========================================================================
56 
57 // This is the top level controller for the Qiskit-Aer simulator
58 // It manages execution of all the circuits in a QOBJ, parallelization,
59 // noise sampling from a noise model, and circuit optimizations.
60 
61 /**************************************************************************
62  * ---------------
63  * Parallelization
64  * ---------------
65  * Parallel execution uses the OpenMP library. It may happen at three levels:
66  *
67  *  1. Parallel execution of circuits in a QOBJ
68  *  2. Parallel execution of shots in a Circuit
69  *  3. Parallelization used by the State class for performing gates.
70  *
71  * Options 1 and 2 are mutually exclusive: enabling circuit parallelization
72  * disables shot parallelization. Option 3 is available for both cases but
73  * conservatively limits the number of threads since these are subthreads
74  * spawned by the higher level threads. If no parallelization is used for
75  * 1 and 2, all available threads will be used for 3.
76  *
77  * -------------------------
78  * Config settings:
79  *
80  * - "noise_model" (json): A noise model to use for simulation [Default: null]
81  * - "max_parallel_threads" (int): Set the maximum OpenMP threads that may
82  *      be used across all levels of parallelization. Set to 0 for maximum
83  *      available. [Default : 0]
84  * - "max_parallel_experiments" (int): Set number of circuits that may be
85  *      executed in parallel. Set to 0 to automatically select a number of
86  *      parallel threads. [Default: 1]
87  * - "max_parallel_shots" (int): Set number of shots that maybe be executed
88  *      in parallel for each circuit. Set to 0 to automatically select a
89  *      number of parallel threads. [Default: 0].
90  * - "max_memory_mb" (int): Sets the maximum size of memory for a store.
91  *      If a state needs more, an error is thrown. If set to 0, the maximum
92  *      will be automatically set to the system memory size [Default: 0].
93  *
94  * Config settings from Data class:
95  *
96  * - "counts" (bool): Return counts objecy in circuit data [Default: True]
97  * - "snapshots" (bool): Return snapshots object in circuit data [Default: True]
98  * - "memory" (bool): Return memory array in circuit data [Default: False]
99  * - "register" (bool): Return register array in circuit data [Default: False]
100  * - "noise_model" (json): A noise model JSON dictionary for the simulator.
101  *                         [Default: null]
102  **************************************************************************/
103 
104 class Controller {
105 public:
106 
Controller()107   Controller() {clear_parallelization();}
108 
109   //-----------------------------------------------------------------------
110   // Execute qobj
111   //-----------------------------------------------------------------------
112 
113   // Load a QOBJ from a JSON file and execute on the State type
114   // class.
115   virtual Result execute(const json_t &qobj);
116 
117   virtual Result execute(std::vector<Circuit> &circuits,
118                          const Noise::NoiseModel &noise_model,
119                          const json_t &config);
120 
121   //-----------------------------------------------------------------------
122   // Config settings
123   //-----------------------------------------------------------------------
124 
125   // Load Controller, State and Data config from a JSON
126   // config settings will be passed to the State and Data classes
127   virtual void set_config(const json_t &config);
128 
129   // Clear the current config
130   void virtual clear_config();
131 
132 protected:
133 
134   //-----------------------------------------------------------------------
135   // Circuit Execution
136   //-----------------------------------------------------------------------
137 
138   // Parallel execution of a circuit
139   // This function manages parallel shot configuration and internally calls
140   // the `run_circuit` method for each shot thread
141   virtual ExperimentResult execute_circuit(Circuit &circ,
142                                            Noise::NoiseModel &noise,
143                                            const json_t &config);
144 
145   // Abstract method for executing a circuit.
146   // This method must initialize a state and return output data for
147   // the required number of shots.
148   virtual ExperimentData run_circuit(const Circuit &circ,
149                                      const Noise::NoiseModel &noise,
150                                      const json_t &config,
151                                      uint_t shots,
152                                      uint_t rng_seed) const = 0;
153 
154   //-------------------------------------------------------------------------
155   // State validation
156   //-------------------------------------------------------------------------
157 
158   // Return True if a given circuit (and internal noise model) are valid for
159   // execution on the given state. Otherwise return false.
160   // If throw_except is true an exception will be thrown on the return false
161   // case listing the invalid instructions in the circuit or noise model.
162   template <class state_t>
163   static bool validate_state(const state_t &state,
164                              const Circuit &circ,
165                              const Noise::NoiseModel &noise,
166                              bool throw_except = false);
167 
168   // Return True if a given circuit are valid for execution on the given state.
169   // Otherwise return false.
170   // If throw_except is true an exception will be thrown directly.
171   template <class state_t>
172   bool validate_memory_requirements(const state_t &state,
173                                     const Circuit &circ,
174                                     bool throw_except = false) const;
175 
176   //-----------------------------------------------------------------------
177   // Config
178   //-----------------------------------------------------------------------
179 
180   // Timer type
181   using myclock_t = std::chrono::high_resolution_clock;
182 
183   // Transpile pass override flags
184   bool truncate_qubits_ = true;
185 
186   // Validation threshold for validating states and operators
187   double validation_threshold_ = 1e-8;
188 
189   //-----------------------------------------------------------------------
190   // Parallelization Config
191   //-----------------------------------------------------------------------
192 
193   // Set OpenMP thread settings to default values
194   void clear_parallelization();
195 
196   // Set parallelization for experiments
197   virtual void set_parallelization_experiments(const std::vector<Circuit>& circuits,
198                                                const Noise::NoiseModel& noise);
199 
200   // Set parallelization for a circuit
201   virtual void set_parallelization_circuit(const Circuit& circuit,
202                                            const Noise::NoiseModel& noise);
203 
204   // Return an estimate of the required memory for a circuit.
205   virtual size_t required_memory_mb(const Circuit& circuit,
206                                     const Noise::NoiseModel& noise) const = 0;
207 
208   // Get system memory size
209   size_t get_system_memory_mb();
210 
211   // The maximum number of threads to use for various levels of parallelization
212   int max_parallel_threads_;
213 
214   // Parameters for parallelization management in configuration
215   int max_parallel_experiments_;
216   int max_parallel_shots_;
217   size_t max_memory_mb_;
218 
219   // use explicit parallelization
220   bool explicit_parallelization_;
221 
222   // Parameters for parallelization management for experiments
223   int parallel_experiments_;
224   int parallel_shots_;
225   int parallel_state_update_;
226 };
227 
228 
229 //=========================================================================
230 // Implementations
231 //=========================================================================
232 
233 //-------------------------------------------------------------------------
234 // Config settings
235 //-------------------------------------------------------------------------
236 
set_config(const json_t & config)237 void Controller::set_config(const json_t &config) {
238 
239   // Load validation threshold
240   JSON::get_value(validation_threshold_, "validation_threshold", config);
241 
242   #ifdef _OPENMP
243   // Load OpenMP maximum thread settings
244   if (JSON::check_key("max_parallel_threads", config))
245     JSON::get_value(max_parallel_threads_, "max_parallel_threads", config);
246   if (JSON::check_key("max_parallel_experiments", config))
247     JSON::get_value(max_parallel_experiments_, "max_parallel_experiments", config);
248   if (JSON::check_key("max_parallel_shots", config))
249     JSON::get_value(max_parallel_shots_, "max_parallel_shots", config);
250   // Limit max threads based on number of available OpenMP threads
251   auto omp_threads = omp_get_max_threads();
252   max_parallel_threads_ = (max_parallel_threads_ > 0)
253       ? std::min(max_parallel_threads_, omp_threads)
254       : std::max(1, omp_threads);
255   #else
256   // No OpenMP so we disable parallelization
257   max_parallel_threads_ = 1;
258   max_parallel_shots_ = 1;
259   max_parallel_experiments_ = 1;
260   #endif
261 
262   // Load configurations for parallelization
263 
264   if (JSON::check_key("max_memory_mb", config)) {
265     JSON::get_value(max_memory_mb_, "max_memory_mb", config);
266   }
267 
268   // for debugging
269   if (JSON::check_key("_parallel_experiments", config)) {
270     JSON::get_value(parallel_experiments_, "_parallel_experiments", config);
271     explicit_parallelization_ = true;
272   }
273 
274   // for debugging
275   if (JSON::check_key("_parallel_shots", config)) {
276     JSON::get_value(parallel_shots_, "_parallel_shots", config);
277     explicit_parallelization_ = true;
278   }
279 
280   // for debugging
281   if (JSON::check_key("_parallel_state_update", config)) {
282     JSON::get_value(parallel_state_update_, "_parallel_state_update", config);
283     explicit_parallelization_ = true;
284   }
285 
286   if (explicit_parallelization_) {
287     parallel_experiments_ = std::max<int>( { parallel_experiments_, 1 });
288     parallel_shots_ = std::max<int>( { parallel_shots_, 1 });
289     parallel_state_update_ = std::max<int>( { parallel_state_update_, 1 });
290   }
291 }
292 
clear_config()293 void Controller::clear_config() {
294   clear_parallelization();
295   validation_threshold_ = 1e-8;
296 }
297 
clear_parallelization()298 void Controller::clear_parallelization() {
299   max_parallel_threads_ = 0;
300   max_parallel_experiments_ = 1;
301   max_parallel_shots_ = 0;
302 
303   parallel_experiments_ = 1;
304   parallel_shots_ = 1;
305   parallel_state_update_ = 1;
306 
307   explicit_parallelization_ = false;
308   max_memory_mb_ = get_system_memory_mb() / 2;
309 }
310 
set_parallelization_experiments(const std::vector<Circuit> & circuits,const Noise::NoiseModel & noise)311 void Controller::set_parallelization_experiments(const std::vector<Circuit>& circuits,
312                                                  const Noise::NoiseModel& noise) {
313   // Use a local variable to not override stored maximum based
314   // on currently executed circuits
315   const auto max_experiments = (max_parallel_experiments_ > 0)
316     ? std::min({max_parallel_experiments_, max_parallel_threads_})
317     : max_parallel_threads_;
318 
319   if (max_experiments == 1) {
320     // No parallel experiment execution
321     parallel_experiments_ = 1;
322     return;
323   }
324 
325   // If memory allows, execute experiments in parallel
326   std::vector<size_t> required_memory_mb_list(circuits.size());
327   for (size_t j=0; j<circuits.size(); j++) {
328     required_memory_mb_list[j] = required_memory_mb(circuits[j], noise);
329   }
330   std::sort(required_memory_mb_list.begin(), required_memory_mb_list.end(), std::greater<>());
331   size_t total_memory = 0;
332   parallel_experiments_ = 0;
333   for (size_t required_memory_mb : required_memory_mb_list) {
334     total_memory += required_memory_mb;
335     if (total_memory > max_memory_mb_)
336       break;
337     ++parallel_experiments_;
338   }
339 
340   if (parallel_experiments_ <= 0)
341     throw std::runtime_error("a circuit requires more memory than max_memory_mb.");
342   parallel_experiments_ = std::min<int>({parallel_experiments_,
343                                          max_experiments,
344                                          max_parallel_threads_,
345                                          static_cast<int>(circuits.size())});
346 }
347 
set_parallelization_circuit(const Circuit & circ,const Noise::NoiseModel & noise)348 void Controller::set_parallelization_circuit(const Circuit& circ,
349                                              const Noise::NoiseModel& noise) {
350 
351   // Use a local variable to not override stored maximum based
352   // on currently executed circuits
353   const auto max_shots = (max_parallel_shots_ > 0)
354     ? std::min({max_parallel_shots_, max_parallel_threads_})
355     : max_parallel_threads_;
356 
357   // If we are executing circuits in parallel we disable
358   // parallel shots
359   if (max_shots == 1 || parallel_experiments_ > 1) {
360     parallel_shots_ = 1;
361   } else {
362     // Parallel shots is > 1
363     // Limit parallel shots by available memory and number of shots
364     // And assign the remaining threads to state update
365     int circ_memory_mb = required_memory_mb(circ, noise);
366     if (max_memory_mb_ < circ_memory_mb)
367       throw std::runtime_error("a circuit requires more memory than max_memory_mb.");
368     // If circ memory is 0, set it to 1 so that we don't divide by zero
369     circ_memory_mb = std::max<int>({1, circ_memory_mb});
370 
371     parallel_shots_ = std::min<int>({static_cast<int>(max_memory_mb_ / circ_memory_mb),
372                                      max_shots,
373                                      static_cast<int>(circ.shots)});
374   }
375   parallel_state_update_ = (parallel_shots_ > 1)
376     ? std::max<int>({1, max_parallel_threads_ / parallel_shots_})
377     : std::max<int>({1, max_parallel_threads_ / parallel_experiments_});
378 }
379 
380 
get_system_memory_mb()381 size_t Controller::get_system_memory_mb(){
382   size_t total_physical_memory = 0;
383 #if defined(__linux__) || defined(__APPLE__)
384    auto pages = sysconf(_SC_PHYS_PAGES);
385    auto page_size = sysconf(_SC_PAGE_SIZE);
386    total_physical_memory = pages * page_size;
387 #elif defined(_WIN64)
388    MEMORYSTATUSEX status;
389    status.dwLength = sizeof(status);
390    GlobalMemoryStatusEx(&status);
391    total_physical_memory = status.ullTotalPhys;
392 #endif
393    return total_physical_memory >> 20;
394 }
395 
396 //-------------------------------------------------------------------------
397 // State validation
398 //-------------------------------------------------------------------------
399 
400 template <class state_t>
validate_state(const state_t & state,const Circuit & circ,const Noise::NoiseModel & noise,bool throw_except)401 bool Controller::validate_state(const state_t &state,
402                                 const Circuit &circ,
403                                 const Noise::NoiseModel &noise,
404                                 bool throw_except) {
405   // First check if a noise model is valid a given state
406   bool noise_valid = noise.is_ideal() || state.validate_opset(noise.opset());
407   bool circ_valid = state.validate_opset(circ.opset());
408   if (noise_valid && circ_valid)
409   {
410     return true;
411   }
412 
413   // If we didn't return true then either noise model or circ has
414   // invalid instructions.
415   if (throw_except == false)
416     return false;
417 
418   // If we are throwing an exception we include information
419   // about the invalid operations
420   std::stringstream msg;
421   if (!noise_valid) {
422     msg << "Noise model contains invalid instructions (";
423     msg << state.invalid_opset_message(noise.opset()) << ")";
424   }
425   if (!circ_valid) {
426     msg << "Circuit contains invalid instructions (";
427     msg << state.invalid_opset_message(circ.opset()) << ")";
428   }
429   throw std::runtime_error(msg.str());
430 }
431 
432 template <class state_t>
validate_memory_requirements(const state_t & state,const Circuit & circ,bool throw_except) const433 bool Controller::validate_memory_requirements(const state_t &state,
434                                               const Circuit &circ,
435                                               bool throw_except) const {
436   if (max_memory_mb_ == 0)
437     return true;
438 
439   size_t required_mb = state.required_memory_mb(circ.num_qubits, circ.ops);
440   if(max_memory_mb_ < required_mb) {
441     if(throw_except) {
442       std::string name = "";
443       JSON::get_value(name, "name", circ.header);
444       throw std::runtime_error("Insufficient memory to run circuit \"" + name +
445                                "\" using the " + state.name() + " simulator.");
446     }
447     return false;
448   }
449   return true;
450 }
451 
452 //-------------------------------------------------------------------------
453 // Qobj execution
454 //-------------------------------------------------------------------------
execute(const json_t & qobj_js)455 Result Controller::execute(const json_t &qobj_js) {
456   // Load QOBJ in a try block so we can catch parsing errors and still return
457   // a valid JSON output containing the error message.
458   try {
459     // Start QOBJ timer
460     auto timer_start = myclock_t::now();
461 
462     Qobj qobj(qobj_js);
463     Noise::NoiseModel noise_model;
464     json_t config;
465     // Check for config
466     if (JSON::get_value(config, "config", qobj_js)) {
467       // Set config
468       set_config(config);
469       // Load noise model
470       JSON::get_value(noise_model, "noise_model", config);
471     }
472     auto result = execute(qobj.circuits, noise_model, config);
473     // Get QOBJ id and pass through header to result
474     result.qobj_id = qobj.id;
475     if (!qobj.header.empty()) {
476         result.header = qobj.header;
477     }
478     // Stop the timer and add total timing data including qobj parsing
479     auto timer_stop = myclock_t::now();
480     result.metadata["time_taken"] = std::chrono::duration<double>(timer_stop - timer_start).count();
481     return result;
482   } catch (std::exception &e) {
483     // qobj was invalid, return valid output containing error message
484     Result result;
485     result.status = Result::Status::error;
486     result.message = std::string("Failed to load qobj: ") + e.what();
487     return result;
488   }
489 }
490 
491 //-------------------------------------------------------------------------
492 // Experiment execution
493 //-------------------------------------------------------------------------
494 
execute(std::vector<Circuit> & circuits,const Noise::NoiseModel & noise_model,const json_t & config)495 Result Controller::execute(std::vector<Circuit> &circuits,
496                            const Noise::NoiseModel &noise_model,
497                            const json_t &config) {
498   // Start QOBJ timer
499   auto timer_start = myclock_t::now();
500 
501   // Initialize Result object for the given number of experiments
502   const auto num_circuits = circuits.size();
503   Result result(num_circuits);
504 
505   // Execute each circuit in a try block
506   try {
507     if (!explicit_parallelization_) {
508       // set parallelization for experiments
509       set_parallelization_experiments(circuits, noise_model);
510     }
511 
512   #ifdef _OPENMP
513     result.metadata["omp_enabled"] = true;
514   #else
515     result.metadata["omp_enabled"] = false;
516   #endif
517     result.metadata["parallel_experiments"] = parallel_experiments_;
518     result.metadata["max_memory_mb"] = max_memory_mb_;
519 
520 
521   #ifdef _OPENMP
522     if (parallel_shots_ > 1 || parallel_state_update_ > 1)
523       omp_set_nested(1);
524   #endif
525     if (parallel_experiments_ > 1) {
526       // Parallel circuit execution
527       #pragma omp parallel for num_threads(parallel_experiments_)
528       for (int j = 0; j < result.results.size(); ++j) {
529         // Make a copy of the noise model for each circuit execution
530         // so that it can be modified if required
531         auto circ_noise_model = noise_model;
532         result.results[j] = execute_circuit(circuits[j],
533                                             circ_noise_model,
534                                             config);
535       }
536     } else {
537       // Serial circuit execution
538       for (int j = 0; j < num_circuits; ++j) {
539         // Make a copy of the noise model for each circuit execution
540         auto circ_noise_model = noise_model;
541         result.results[j] = execute_circuit(circuits[j],
542                                             circ_noise_model,
543                                             config);
544       }
545     }
546 
547     // Check each experiment result for completed status.
548     // If only some experiments completed return partial completed status.
549     result.status = Result::Status::completed;
550     for (const auto& experiment: result.results) {
551       if (experiment.status != ExperimentResult::Status::completed) {
552         result.status = Result::Status::partial_completed;
553         break;
554       }
555     }
556     // Stop the timer and add total timing data
557     auto timer_stop = myclock_t::now();
558     result.metadata["time_taken"] = std::chrono::duration<double>(timer_stop - timer_start).count();
559   }
560   // If execution failed return valid output reporting error
561   catch (std::exception &e) {
562     result.status = Result::Status::error;
563     result.message = e.what();
564   }
565   return result;
566 }
567 
568 
execute_circuit(Circuit & circ,Noise::NoiseModel & noise,const json_t & config)569 ExperimentResult Controller::execute_circuit(Circuit &circ,
570                                              Noise::NoiseModel& noise,
571                                              const json_t &config) {
572 
573   // Start individual circuit timer
574   auto timer_start = myclock_t::now(); // state circuit timer
575 
576   // Initialize circuit json return
577   ExperimentResult exp_result;
578   ExperimentData data;
579   data.set_config(config);
580 
581   // Execute in try block so we can catch errors and return the error message
582   // for individual circuit failures.
583   try {
584     // Remove barriers from circuit
585     Transpile::ReduceBarrier barrier_pass;
586     barrier_pass.optimize_circuit(circ, noise, circ.opset(), data);
587 
588     // Truncate unused qubits from circuit and noise model
589     if (truncate_qubits_) {
590       Transpile::TruncateQubits truncate_pass;
591       truncate_pass.set_config(config);
592       truncate_pass.optimize_circuit(circ, noise, circ.opset(), data);
593     }
594 
595     // set parallelization for this circuit
596     if (!explicit_parallelization_) {
597       set_parallelization_circuit(circ, noise);
598     }
599 
600     // Single shot thread execution
601     if (parallel_shots_ <= 1) {
602       auto tmp_data = run_circuit(circ, noise, config, circ.shots, circ.seed);
603       data.combine(std::move(tmp_data));
604     // Parallel shot thread execution
605     } else {
606       // Calculate shots per thread
607       std::vector<unsigned int> subshots;
608       for (int j = 0; j < parallel_shots_; ++j) {
609         subshots.push_back(circ.shots / parallel_shots_);
610       }
611       // If shots is not perfectly divisible by threads, assign the remainder
612       for (int j=0; j < int(circ.shots % parallel_shots_); ++j) {
613         subshots[j] += 1;
614       }
615 
616       // Vector to store parallel thread output data
617       std::vector<ExperimentData> par_data(parallel_shots_);
618       std::vector<std::string> error_msgs(parallel_shots_);
619       #pragma omp parallel for if (parallel_shots_ > 1) num_threads(parallel_shots_)
620       for (int i = 0; i < parallel_shots_; i++) {
621         try {
622           par_data[i] = run_circuit(circ, noise, config, subshots[i], circ.seed + i);
623         } catch (std::runtime_error &error) {
624           error_msgs[i] = error.what();
625         }
626       }
627 
628       for (std::string error_msg: error_msgs)
629         if (error_msg != "")
630           throw std::runtime_error(error_msg);
631 
632       // Accumulate results across shots
633       // Use move semantics to avoid copying data
634       for (auto &datum : par_data) {
635         data.combine(std::move(datum));
636       }
637     }
638     // Report success
639     exp_result.data = data;
640     exp_result.status = ExperimentResult::Status::completed;
641 
642     // Pass through circuit header and add metadata
643     exp_result.header = circ.header;
644     exp_result.shots = circ.shots;
645     exp_result.seed = circ.seed;
646     // Move any metadata from the subclass run_circuit data
647     // to the experiment resultmetadata field
648     for(const auto& pair: exp_result.data.metadata()) {
649       exp_result.add_metadata(pair.first, pair.second);
650     }
651     // Remove the metatdata field from data
652     exp_result.data.metadata().clear();
653     exp_result.metadata["parallel_shots"] = parallel_shots_;
654     exp_result.metadata["parallel_state_update"] = parallel_state_update_;
655     // Add timer data
656     auto timer_stop = myclock_t::now(); // stop timer
657     double time_taken = std::chrono::duration<double>(timer_stop - timer_start).count();
658     exp_result.time_taken = time_taken;
659   }
660   // If an exception occurs during execution, catch it and pass it to the output
661   catch (std::exception &e) {
662     exp_result.status = ExperimentResult::Status::error;
663     exp_result.message = e.what();
664   }
665   return exp_result;
666 }
667 
668 //-------------------------------------------------------------------------
669 } // end namespace Base
670 //-------------------------------------------------------------------------
671 } // end namespace AER
672 //-------------------------------------------------------------------------
673 #endif
674