source: src/Fragmentation/Automation/controller.cpp@ e48f5c

Action_Thermostats Add_AtomRandomPerturbation Add_FitFragmentPartialChargesAction Add_RotateAroundBondAction Add_SelectAtomByNameAction Added_ParseSaveFragmentResults AddingActions_SaveParseParticleParameters Adding_Graph_to_ChangeBondActions Adding_MD_integration_tests Adding_ParticleName_to_Atom Adding_StructOpt_integration_tests AtomFragments Automaking_mpqc_open AutomationFragmentation_failures Candidate_v1.5.4 Candidate_v1.6.0 Candidate_v1.6.1 ChangeBugEmailaddress ChangingTestPorts ChemicalSpaceEvaluator CombiningParticlePotentialParsing Combining_Subpackages Debian_Package_split Debian_package_split_molecuildergui_only Disabling_MemDebug Docu_Python_wait EmpiricalPotential_contain_HomologyGraph EmpiricalPotential_contain_HomologyGraph_documentation Enable_parallel_make_install Enhance_userguide Enhanced_StructuralOptimization Enhanced_StructuralOptimization_continued Example_ManyWaysToTranslateAtom Exclude_Hydrogens_annealWithBondGraph FitPartialCharges_GlobalError Fix_BoundInBox_CenterInBox_MoleculeActions Fix_ChargeSampling_PBC Fix_ChronosMutex Fix_FitPartialCharges Fix_FitPotential_needs_atomicnumbers Fix_ForceAnnealing Fix_IndependentFragmentGrids Fix_ParseParticles Fix_ParseParticles_split_forward_backward_Actions Fix_PopActions Fix_QtFragmentList_sorted_selection Fix_Restrictedkeyset_FragmentMolecule Fix_StatusMsg Fix_StepWorldTime_single_argument Fix_Verbose_Codepatterns Fix_fitting_potentials Fixes ForceAnnealing_goodresults ForceAnnealing_oldresults ForceAnnealing_tocheck ForceAnnealing_with_BondGraph ForceAnnealing_with_BondGraph_continued ForceAnnealing_with_BondGraph_continued_betteresults ForceAnnealing_with_BondGraph_contraction-expansion FragmentAction_writes_AtomFragments FragmentMolecule_checks_bonddegrees GeometryObjects Gui_Fixes Gui_displays_atomic_force_velocity ImplicitCharges IndependentFragmentGrids IndependentFragmentGrids_IndividualZeroInstances IndependentFragmentGrids_IntegrationTest IndependentFragmentGrids_Sole_NN_Calculation JobMarket_RobustOnKillsSegFaults JobMarket_StableWorkerPool JobMarket_unresolvable_hostname_fix MoreRobust_FragmentAutomation ODR_violation_mpqc_open PartialCharges_OrthogonalSummation PdbParser_setsAtomName PythonUI_with_named_parameters QtGui_reactivate_TimeChanged_changes Recreated_GuiChecks Rewrite_FitPartialCharges RotateToPrincipalAxisSystem_UndoRedo SaturateAtoms_findBestMatching SaturateAtoms_singleDegree StoppableMakroAction Subpackage_CodePatterns Subpackage_JobMarket Subpackage_LinearAlgebra Subpackage_levmar Subpackage_mpqc_open Subpackage_vmg Switchable_LogView ThirdParty_MPQC_rebuilt_buildsystem TrajectoryDependenant_MaxOrder TremoloParser_IncreasedPrecision TremoloParser_MultipleTimesteps TremoloParser_setsAtomName Ubuntu_1604_changes stable
Last change on this file since e48f5c was af9b9ff, checked in by Frederik Heber <heber@…>, 13 years ago

Controller now also uses boost::program_options to parse command line options.

  • also we now cleanly check whether each command has its required arguments before making any connections to the server. Hence, nothing can be lost.
  • TESTFIX: Changed regression tests due to changed controller calling signature.
  • TESTFIX: regression test Fragmenation/Automation mpqc-jobs uses removeall instead of giving kill.
  • Property mode set to 100644
File size: 18.4 KB
Line 
1/*
2 * Project: MoleCuilder
3 * Description: creates and alters molecular systems
4 * Copyright (C) 2011 University of Bonn. All rights reserved.
5 * Please see the LICENSE file or "Copyright notice" in builder.cpp for details.
6 */
7
8/*
9 * \file controller.cpp
10 *
11 * This file strongly follows the Serialization example from the boost::asio
12 * library (see client.cpp)
13 *
14 * Created on: Nov 27, 2011
15 * Author: heber
16 */
17
18
19// include config.h
20#ifdef HAVE_CONFIG_H
21#include <config.h>
22#endif
23
24// boost asio needs specific operator new
25#include <boost/asio.hpp>
26
27#include "CodePatterns/MemDebug.hpp"
28
29#include <boost/archive/text_oarchive.hpp>
30#include <boost/archive/text_iarchive.hpp>
31#include <boost/program_options.hpp>
32#include <fstream>
33#include <iostream>
34#include <map>
35#include <sstream>
36#include <streambuf>
37#include <vector>
38
39#include "atexit.hpp"
40#include "CodePatterns/Info.hpp"
41#include "CodePatterns/Log.hpp"
42#include "Fragmentation/EnergyMatrix.hpp"
43#include "Fragmentation/ForceMatrix.hpp"
44#include "Fragmentation/KeySetsContainer.hpp"
45#include "FragmentController.hpp"
46#include "Helpers/defs.hpp"
47#include "Jobs/MPQCCommandJob.hpp"
48#include "Jobs/MPQCCommandJob_MPQCData.hpp"
49#include "Jobs/SystemCommandJob.hpp"
50#include "Results/FragmentResult.hpp"
51
52enum CommandIndices {
53 UnknownCommandIndex = 0,
54 AddJobsIndex = 1,
55 CreateJobsIndex = 2,
56 CheckResultsIndex = 3,
57 ReceiveResultsIndex = 4,
58 ReceiveMPQCIndex = 5,
59 RemoveAllIndex = 6,
60 ShutdownIndex = 7,
61};
62
63
64/** Creates a SystemCommandJob out of give \a command with \a argument.
65 *
66 * @param jobs created job is added to this vector
67 * @param command command to execute for SystemCommandJob
68 * @param argument argument for command to execute
69 * @param nextid id for this job
70 */
71void createjobs(
72 std::vector<FragmentJob::ptr> &jobs,
73 const std::string &command,
74 const std::string &argument,
75 const JobId_t nextid)
76{
77
78 FragmentJob::ptr testJob( new SystemCommandJob(command, argument, nextid) );
79 jobs.push_back(testJob);
80 LOG(1, "INFO: Added one SystemCommandJob.");
81}
82
83/** Creates a MPQCCommandJob with argument \a filename.
84 *
85 * @param jobs created job is added to this vector
86 * @param command mpqc command to execute
87 * @param filename filename being argument to job
88 * @param nextid id for this job
89 */
90void parsejob(
91 std::vector<FragmentJob::ptr> &jobs,
92 const std::string &command,
93 const std::string &filename,
94 const JobId_t nextid)
95{
96 std::ifstream file;
97 file.open(filename.c_str());
98 ASSERT( file.good(), "parsejob() - file "+filename+" does not exist.");
99 std::string output((std::istreambuf_iterator<char>(file)),
100 std::istreambuf_iterator<char>());
101 FragmentJob::ptr testJob( new MPQCCommandJob(output, nextid, command) );
102 jobs.push_back(testJob);
103 file.close();
104 LOG(1, "INFO: Added MPQCCommandJob from file "+filename+".");
105}
106
107/** Print received results.
108 *
109 * @param results received results to print
110 */
111void printReceivedResults(std::vector<FragmentResult::ptr> &results)
112{
113 for (std::vector<FragmentResult::ptr>::const_iterator iter = results.begin();
114 iter != results.end(); ++iter)
115 LOG(1, "RESULT: job #"+toString((*iter)->getId())+": "+toString((*iter)->result));
116}
117
118/** Print MPQCData from received results.
119 *
120 * @param results received results to extract MPQCData from
121 * @param KeySetFilename filename with keysets to associate forces correctly
122 * @param NoAtoms total number of atoms
123 */
124bool printReceivedMPQCResults(
125 const std::vector<FragmentResult::ptr> &results,
126 const std::string &KeySetFilename,
127 size_t NoAtoms)
128{
129 EnergyMatrix Energy;
130 EnergyMatrix EnergyFragments;
131 ForceMatrix Force;
132 ForceMatrix ForceFragments;
133 KeySetsContainer KeySet;
134
135 // align fragments
136 std::map< JobId_t, size_t > MatrixNrLookup;
137 size_t FragmentCounter = 0;
138 {
139 // bring ids in order ...
140 typedef std::map< JobId_t, FragmentResult::ptr> IdResultMap_t;
141 IdResultMap_t IdResultMap;
142 for (std::vector<FragmentResult::ptr>::const_iterator iter = results.begin();
143 iter != results.end(); ++iter) {
144 #ifndef NDEBUG
145 std::pair< IdResultMap_t::iterator, bool> inserter =
146 #endif
147 IdResultMap.insert( make_pair((*iter)->getId(), *iter) );
148 ASSERT( inserter.second,
149 "printReceivedMPQCResults() - two results have same id "
150 +toString((*iter)->getId())+".");
151 }
152 // ... and fill lookup
153 for(IdResultMap_t::const_iterator iter = IdResultMap.begin();
154 iter != IdResultMap.end(); ++iter)
155 MatrixNrLookup.insert( make_pair(iter->first, FragmentCounter++) );
156 }
157 LOG(1, "INFO: There are " << FragmentCounter << " fragments.");
158
159 // extract results
160 std::vector<MPQCData> fragmentData(results.size());
161 MPQCData combinedData;
162
163 LOG(2, "DEBUG: Parsing now through " << results.size() << " results.");
164 for (std::vector<FragmentResult::ptr>::const_iterator iter = results.begin();
165 iter != results.end(); ++iter) {
166 LOG(1, "RESULT: job #"+toString((*iter)->getId())+": "+toString((*iter)->result));
167 MPQCData extractedData;
168 std::stringstream inputstream((*iter)->result);
169 LOG(2, "DEBUG: First 50 characters FragmentResult's string: "+(*iter)->result.substr(0, 50));
170 boost::archive::text_iarchive ia(inputstream);
171 ia >> extractedData;
172 LOG(1, "INFO: extracted data is " << extractedData << ".");
173
174 // place results into EnergyMatrix ...
175 {
176 MatrixContainer::MatrixArray matrix;
177 matrix.resize(1);
178 matrix[0].resize(1, extractedData.energy);
179 if (!Energy.AddMatrix(
180 std::string("MPQCJob ")+toString((*iter)->getId()),
181 matrix,
182 MatrixNrLookup[(*iter)->getId()])) {
183 ELOG(1, "Adding energy matrix failed.");
184 return false;
185 }
186 }
187 // ... and ForceMatrix (with two empty columns in front)
188 {
189 MatrixContainer::MatrixArray matrix;
190 const size_t rows = extractedData.forces.size();
191 matrix.resize(rows);
192 for (size_t i=0;i<rows;++i) {
193 const size_t columns = 2+extractedData.forces[i].size();
194 matrix[i].resize(columns, 0.);
195 // for (size_t j=0;j<2;++j)
196 // matrix[i][j] = 0.;
197 for (size_t j=2;j<columns;++j)
198 matrix[i][j] = extractedData.forces[i][j-2];
199 }
200 if (!Force.AddMatrix(
201 std::string("MPQCJob ")+toString((*iter)->getId()),
202 matrix,
203 MatrixNrLookup[(*iter)->getId()])) {
204 ELOG(1, "Adding force matrix failed.");
205 return false;
206 }
207 }
208 }
209 // add one more matrix (not required for energy)
210 MatrixContainer::MatrixArray matrix;
211 matrix.resize(1);
212 matrix[0].resize(1, 0.);
213 if (!Energy.AddMatrix(std::string("MPQCJob total"), matrix, FragmentCounter))
214 return false;
215 // but for energy because we need to know total number of atoms
216 matrix.resize(NoAtoms);
217 for (size_t i = 0; i< NoAtoms; ++i)
218 matrix[i].resize(2+NDIM, 0.);
219 if (!Force.AddMatrix(std::string("MPQCJob total"), matrix, FragmentCounter))
220 return false;
221
222
223 // combine all found data
224 if (!Energy.InitialiseIndices()) return false;
225
226 if (!Force.ParseIndices(KeySetFilename.c_str())) return false;
227
228 if (!KeySet.ParseKeySets(KeySetFilename.c_str(), Force.RowCounter, Force.MatrixCounter)) return false;
229
230 if (!KeySet.ParseManyBodyTerms()) return false;
231
232 if (!EnergyFragments.AllocateMatrix(Energy.Header, Energy.MatrixCounter, Energy.RowCounter, Energy.ColumnCounter)) return false;
233 if (!ForceFragments.AllocateMatrix(Force.Header, Force.MatrixCounter, Force.RowCounter, Force.ColumnCounter)) return false;
234
235 if(!Energy.SetLastMatrix(0., 0)) return false;
236 if(!Force.SetLastMatrix(0., 2)) return false;
237
238 for (int BondOrder=0;BondOrder<KeySet.Order;BondOrder++) {
239 // --------- sum up energy --------------------
240 LOG(1, "INFO: Summing energy of order " << BondOrder+1 << " ...");
241 if (!EnergyFragments.SumSubManyBodyTerms(Energy, KeySet, BondOrder)) return false;
242 if (!Energy.SumSubEnergy(EnergyFragments, NULL, KeySet, BondOrder, 1.)) return false;
243
244 // --------- sum up Forces --------------------
245 LOG(1, "INFO: Summing forces of order " << BondOrder+1 << " ...");
246 if (!ForceFragments.SumSubManyBodyTerms(Force, KeySet, BondOrder)) return false;
247 if (!Force.SumSubForces(ForceFragments, KeySet, BondOrder, 1.)) return false;
248 }
249
250 // for debugging print resulting energy and forces
251 LOG(1, "INFO: Resulting energy is " << Energy.Matrix[ FragmentCounter ][0][0]);
252 std::stringstream output;
253 for (int i=0; i< Force.RowCounter[FragmentCounter]; ++i) {
254 for (int j=0; j< Force.ColumnCounter[FragmentCounter]; ++j)
255 output << Force.Matrix[ FragmentCounter ][i][j] << " ";
256 output << "\n";
257 }
258 LOG(1, "INFO: Resulting forces are " << std::endl << output.str());
259
260 return true;
261}
262
263/** Helper function to get number of atoms somehow.
264 *
265 * Here, we just parse the number of lines in the adjacency file as
266 * it should correspond to the number of atoms, except when some atoms
267 * are not bonded, but then fragmentation makes no sense.
268 *
269 * @param path path to the adjacency file
270 */
271size_t getNoAtomsFromAdjacencyFile(const std::string &path)
272{
273 size_t NoAtoms = 0;
274
275 // parse in special file to get atom count (from line count)
276 std::string filename(path);
277 filename += FRAGMENTPREFIX;
278 filename += ADJACENCYFILE;
279 std::ifstream adjacency(filename.c_str());
280 if (adjacency.fail()) {
281 LOG(0, endl << "getNoAtomsFromAdjacencyFile() - Unable to open " << filename << ", is the directory correct?");
282 return false;
283 }
284 std::string buffer;
285 while (getline(adjacency, buffer))
286 NoAtoms++;
287 LOG(1, "INFO: There are " << NoAtoms << " atoms.");
288
289 return NoAtoms;
290}
291
292
293/** Returns a unique index for every command to allow switching over it.
294 *
295 * \param &commandmap map with command strings
296 * \param &cmd command string
297 * \return index from CommandIndices: UnkownCommandIndex - unknown command, else - command index
298 */
299CommandIndices getCommandIndex(std::map<std::string, CommandIndices> &commandmap, const std::string &cmd)
300{
301 std::map<std::string, CommandIndices>::const_iterator iter = commandmap.find(cmd);
302 if (iter != commandmap.end())
303 return iter->second;
304 else
305 return UnknownCommandIndex;
306}
307
308
309int main(int argc, char* argv[])
310{
311 // from this moment on, we need to be sure to deeinitialize in the correct order
312 // this is handled by the cleanup function
313 atexit(cleanUp);
314
315 setVerbosity(3);
316
317 size_t Exitflag = 0;
318 typedef std::map<std::string, CommandIndices> CommandsMap_t;
319 CommandsMap_t CommandsMap;
320 CommandsMap.insert( std::make_pair("addjobs", AddJobsIndex) );
321 CommandsMap.insert( std::make_pair("createjobs", CreateJobsIndex) );
322 CommandsMap.insert( std::make_pair("checkresults", CheckResultsIndex) );
323 CommandsMap.insert( std::make_pair("receiveresults", ReceiveResultsIndex) );
324 CommandsMap.insert( std::make_pair("receivempqc", ReceiveMPQCIndex) );
325 CommandsMap.insert( std::make_pair("removeall", RemoveAllIndex) );
326 CommandsMap.insert( std::make_pair("shutdown", ShutdownIndex) );
327 std::vector<std::string> Commands;
328 for (CommandsMap_t::const_iterator iter = CommandsMap.begin(); iter != CommandsMap.end(); ++iter)
329 Commands.push_back(iter->first);
330
331 // Declare the supported options.
332 boost::program_options::options_description desc("Allowed options");
333 desc.add_options()
334 ("help,h", "produce help message")
335 ("verbosity,v", boost::program_options::value<size_t>(), "set verbosity level")
336 ("server", boost::program_options::value< std::string >(), "connect to server at this address (host:port)")
337 ("command", boost::program_options::value< std::string >(), (std::string("command to send to server: ")+toString(Commands)).c_str())
338 ("executable", boost::program_options::value< std::string >(), "executable for commands 'addjobs' and 'createjobs'")
339 ("fragment-path", boost::program_options::value< std::string >(), "path to fragment files for 'receivempqcresults'")
340 ("jobcommand", boost::program_options::value< std::string >(), "command argument for executable for 'createjobs'")
341 ("jobfiles", boost::program_options::value< std::vector< std::string > >()->multitoken(), "list of files as single argument to executable for 'addjobs'")
342 ;
343
344 boost::program_options::variables_map vm;
345 boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm);
346 boost::program_options::notify(vm);
347
348 if (vm.count("help")) {
349 std::cout << desc << "\n";
350 return 1;
351 }
352
353 if (vm.count("verbosity")) {
354 LOG(0, "STATUS: Verbosity level was set to " << vm["verbosity"].as<size_t>() << ".");
355 setVerbosity(vm["verbosity"].as<size_t>());
356 } else {
357 LOG(0, "STATUS: Verbosity level was not set, defaulting to 5.");
358 setVerbosity(5);
359 }
360
361 std::string server;
362 std::string serverport;
363 if (vm.count("server")) {
364 server = vm["server"].as< std::string >();
365 serverport = server.substr(server.find_last_of(':')+1, std::string::npos);
366 server = server.substr(0, server.find_last_of(':'));
367 try {
368 boost::lexical_cast<size_t>(serverport);
369 } catch (boost::bad_lexical_cast) {
370 ELOG(1, "Could not interpret " << serverport << " as server:port.");
371 return 255;
372 }
373 LOG(1, "INFO: Using " << server << ":" << serverport << " as server's address.");
374 } else {
375 ELOG(1, "Requiring server's address (host:port) to connect to.");
376 return 255;
377 }
378
379 if (!vm.count("command")) {
380 ELOG(1, "Controller requires one of the following commands: "+toString(Commands));
381 return 255;
382 }
383 const std::string command = vm["command"].as< std::string >();
384 const CommandIndices commandIndex = getCommandIndex(CommandsMap, command);
385
386 // check arguments
387 switch(commandIndex) {
388 case AddJobsIndex:
389 if (!vm.count("executable") && !vm.count("jobfiles")) {
390 ELOG(1, "'"+command+"' requires at least two options: [executable] [list of input files ...].");
391 return 255;
392 }
393 break;
394 case CreateJobsIndex:
395 if (!vm.count("executable") && !vm.count("jobcommand")) {
396 ELOG(1, "'"+command+"' requires two options: [executable] [jobcommand].");
397 return 255;
398 }
399 break;
400 case CheckResultsIndex:
401 break;
402 case ReceiveResultsIndex:
403 break;
404 case ReceiveMPQCIndex:
405 if (!vm.count("fragment-path")) {
406 ELOG(1, "'"+command+"' require one option: [path to fragment files].");
407 return 255;
408 }
409 break;
410 case RemoveAllIndex:
411 break;
412 case ShutdownIndex:
413 break;
414 case UnknownCommandIndex:
415 default:
416 ELOG(1, "Unrecognized command '"+toString(command)+"'.");
417 return 255;
418 break;
419 }
420
421 try
422 {
423
424 boost::asio::io_service io_service;
425 FragmentController controller(io_service);
426
427 // Initial phase: information gathering from server
428
429 switch(commandIndex) {
430 case AddJobsIndex:
431 controller.requestIds(server, serverport, vm["jobfiles"].as< std::vector<std::string> >().size());
432 break;
433 case CreateJobsIndex:
434 controller.requestIds(server, serverport, 1);
435 break;
436 case CheckResultsIndex:
437 break;
438 case ReceiveResultsIndex:
439 break;
440 case ReceiveMPQCIndex:
441 break;
442 case RemoveAllIndex:
443 break;
444 case ShutdownIndex:
445 break;
446 case UnknownCommandIndex:
447 default:
448 ELOG(0, "Unrecognized command '"+toString(command)+"'.");
449 return 255;
450 break;
451 }
452
453 {
454 io_service.reset();
455 Info info("io_service: Phase One");
456 io_service.run();
457 }
458
459 // Second phase: Building jobs and sending information to server
460
461 switch(commandIndex) {
462 case AddJobsIndex:
463 {
464 std::vector<FragmentJob::ptr> jobs;
465 const std::string executable(vm["executable"].as< std::string >());
466 const std::vector< std::string > jobfiles = vm["jobfiles"].as< std::vector< std::string > >();
467 for (std::vector< std::string >::const_iterator iter = jobfiles.begin();
468 iter != jobfiles.end(); ++iter) {
469 const JobId_t next_id = controller.getAvailableId();
470 const std::string &filename = *iter;
471 LOG(1, "INFO: Creating MPQCCommandJob with filename'"
472 +filename+"', and id "+toString(next_id)+".");
473 parsejob(jobs, executable, filename, next_id);
474 }
475 controller.addJobs(jobs);
476 controller.sendJobs(server, serverport);
477 break;
478 }
479 case CreateJobsIndex:
480 {
481 const JobId_t next_id = controller.getAvailableId();
482 std::vector<FragmentJob::ptr> jobs;
483 const std::string executable = vm["executable"].as< std::string >();
484 const std::string jobcommand = vm["jobcommand"].as< std::string >();
485 createjobs(jobs, executable, jobcommand, next_id);
486 controller.addJobs(jobs);
487 controller.sendJobs(server, serverport);
488 break;
489 }
490 case CheckResultsIndex:
491 controller.checkResults(server, serverport);
492 break;
493 case ReceiveResultsIndex:
494 case ReceiveMPQCIndex:
495 controller.receiveResults(server, serverport);
496 break;
497 case RemoveAllIndex:
498 controller.removeall(server, serverport);
499 break;
500 case ShutdownIndex:
501 controller.shutdown(server, serverport);
502 break;
503 case UnknownCommandIndex:
504 default:
505 ELOG(0, "Unrecognized command '"+toString(command)+"'.");
506 return 255;
507 break;
508 }
509
510 {
511 io_service.reset();
512 Info info("io_service: Phase Two");
513 io_service.run();
514 }
515
516 // Final phase: Print result of command
517
518 switch(commandIndex) {
519 case AddJobsIndex:
520 case CreateJobsIndex:
521 break;
522 case CheckResultsIndex:
523 controller.printDoneJobs();
524 break;
525 case ReceiveResultsIndex:
526 {
527 std::vector<FragmentResult::ptr> results = controller.getReceivedResults();
528 printReceivedResults(results);
529 break;
530 }
531 case ReceiveMPQCIndex:
532 {
533 const std::string path = vm["fragment-path"].as< std::string >();
534 LOG(1, "INFO: Parsing fragment files from " << path << ".");
535 std::vector<FragmentResult::ptr> results = controller.getReceivedResults();
536 printReceivedMPQCResults(
537 results,
538 path,
539 getNoAtomsFromAdjacencyFile(path));
540 break;
541 }
542 case RemoveAllIndex:
543 break;
544 case ShutdownIndex:
545 break;
546 case UnknownCommandIndex:
547 default:
548 ELOG(0, "Unrecognized command '"+toString(command)+"'.");
549 return 255;
550 break;
551 }
552 Exitflag = controller.getExitflag();
553 }
554 catch (std::exception& e)
555 {
556 std::cerr << e.what() << std::endl;
557 }
558
559 return Exitflag;
560}
561
Note: See TracBrowser for help on using the repository browser.