source: src/Dynamics/MinimiseConstrainedPotential.cpp@ ee4f2d

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 ee4f2d was 94d5ac6, checked in by Frederik Heber <heber@…>, 12 years ago

FIX: As we use GSL internally, we are as of now required to use GPL v2 license.

  • GNU Scientific Library is used at every place in the code, especially the sub-package LinearAlgebra is based on it which in turn is used really everywhere in the remainder of MoleCuilder. Hence, we have to use the GPL license for the whole of MoleCuilder. In effect, GPL's COPYING was present all along and stated the terms of the GPL v2 license.
  • Hence, I added the default GPL v2 disclaimer to every source file and removed the note about a (actually missing) LICENSE file.
  • also, I added a help-redistribute action which again gives the disclaimer of the GPL v2.
  • also, I changed in the disclaimer that is printed at every program start in builder_init.cpp.
  • TEST: Added check on GPL statement present in every module to test CodeChecks project-disclaimer.
  • Property mode set to 100644
File size: 18.3 KB
Line 
1/*
2 * Project: MoleCuilder
3 * Description: creates and alters molecular systems
4 * Copyright (C) 2010-2012 University of Bonn. All rights reserved.
5 *
6 *
7 * This file is part of MoleCuilder.
8 *
9 * MoleCuilder is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * MoleCuilder is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with MoleCuilder. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/*
24 * MinimiseConstrainedPotential.cpp
25 *
26 * Created on: Feb 23, 2011
27 * Author: heber
28 */
29
30// include config.h
31#ifdef HAVE_CONFIG_H
32#include <config.h>
33#endif
34
35#include "CodePatterns/MemDebug.hpp"
36
37#include <gsl/gsl_matrix.h>
38#include <gsl/gsl_vector.h>
39#include <gsl/gsl_linalg.h>
40
41#include "Atom/atom.hpp"
42#include "Element/element.hpp"
43#include "CodePatterns/enumeration.hpp"
44#include "CodePatterns/Info.hpp"
45#include "CodePatterns/Verbose.hpp"
46#include "CodePatterns/Log.hpp"
47#include "Fragmentation/ForceMatrix.hpp"
48#include "Helpers/helpers.hpp"
49#include "molecule.hpp"
50#include "LinearAlgebra/Plane.hpp"
51#include "World.hpp"
52
53#include "Dynamics/MinimiseConstrainedPotential.hpp"
54
55
56MinimiseConstrainedPotential::MinimiseConstrainedPotential(
57 World::AtomComposite &_atoms,
58 std::map<atom*, atom *> &_PermutationMap) :
59 atoms(_atoms),
60 PermutationMap(_PermutationMap)
61{}
62
63MinimiseConstrainedPotential::~MinimiseConstrainedPotential()
64{}
65
66double MinimiseConstrainedPotential::operator()(int _startstep, int _endstep, bool IsAngstroem)
67{
68 double Potential, OldPotential, OlderPotential;
69 int round;
70 atom *Sprinter = NULL;
71 DistanceMap::iterator Rider, Strider;
72
73 // set to zero
74 PermutationMap.clear();
75 DoubleList.clear();
76 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
77 DistanceList[*iter].clear();
78 }
79 DistanceList.clear();
80 DistanceIterators.clear();
81 DistanceIterators.clear();
82
83 /// Minimise the potential
84 // set Lagrange multiplier constants
85 PenaltyConstants[0] = 10.;
86 PenaltyConstants[1] = 1.;
87 PenaltyConstants[2] = 1e+7; // just a huge penalty
88 // generate the distance list
89 LOG(1, "Allocating, initializting and filling the distance list ... ");
90 FillDistanceList();
91
92 // create the initial PermutationMap (source -> target)
93 CreateInitialLists();
94
95 // make the PermutationMap injective by checking whether we have a non-zero constants[2] term in it
96 LOG(1, "Making the PermutationMap injective ... ");
97 MakeInjectivePermutation();
98 DoubleList.clear();
99
100 // argument minimise the constrained potential in this injective PermutationMap
101 LOG(1, "Argument minimising the PermutationMap.");
102 OldPotential = 1e+10;
103 round = 0;
104 do {
105 LOG(2, "Starting round " << ++round << ", at current potential " << OldPotential << " ... ");
106 OlderPotential = OldPotential;
107 World::AtomComposite::const_iterator iter;
108 do {
109 iter = atoms.begin();
110 for (; iter != atoms.end(); ++iter) {
111 CalculateDoubleList();
112 PrintPermutationMap();
113 Sprinter = DistanceIterators[(*iter)]->second; // store initial partner
114 Strider = DistanceIterators[(*iter)]; //remember old iterator
115 DistanceIterators[(*iter)] = StepList[(*iter)];
116 if (DistanceIterators[(*iter)] == DistanceList[(*iter)].end()) {// stop, before we run through the list and still on
117 DistanceIterators[(*iter)] == DistanceList[(*iter)].begin();
118 break;
119 }
120 //LOG(2, "Current Walker: " << *(*iter) << " with old/next candidate " << *Sprinter << "/" << *DistanceIterators[(*iter)]->second << ".");
121 // find source of the new target
122 World::AtomComposite::const_iterator runner = atoms.begin();
123 for (; runner != atoms.end(); ++runner) { // find the source whose toes we might be stepping on (Walker's new target should be in use by another already)
124 if (PermutationMap[(*runner)] == DistanceIterators[(*iter)]->second) {
125 //LOG(2, "Found the corresponding owner " << *(*runner) << " to " << *PermutationMap[(*runner)] << ".");
126 break;
127 }
128 }
129 if (runner != atoms.end()) { // we found the other source
130 // then look in its distance list for Sprinter
131 Rider = DistanceList[(*runner)].begin();
132 for (; Rider != DistanceList[(*runner)].end(); Rider++)
133 if (Rider->second == Sprinter)
134 break;
135 if (Rider != DistanceList[(*runner)].end()) { // if we have found one
136 //LOG(2, "Current Other: " << *(*runner) << " with old/next candidate " << *PermutationMap[(*runner)] << "/" << *Rider->second << ".");
137 // exchange both
138 PermutationMap[(*iter)] = DistanceIterators[(*iter)]->second; // put next farther distance into PermutationMap
139 PermutationMap[(*runner)] = Sprinter; // and hand the old target to its respective owner
140 CalculateDoubleList();
141 PrintPermutationMap();
142 // calculate the new potential
143 //LOG(2, "Checking new potential ...");
144 Potential = ConstrainedPotential();
145 if (Potential > OldPotential) { // we made everything worse! Undo ...
146 //LOG(3, "Nay, made the potential worse: " << Potential << " vs. " << OldPotential << "!");
147 //LOG(3, "Setting " << *(*runner) << "'s source to " << *DistanceIterators[(*runner)]->second << ".");
148 // Undo for Runner (note, we haven't moved the iteration yet, we may use this)
149 PermutationMap[(*runner)] = DistanceIterators[(*runner)]->second;
150 // Undo for Walker
151 DistanceIterators[(*iter)] = Strider; // take next farther distance target
152 //LOG(3, "Setting " << *(*iter) << "'s source to " << *DistanceIterators[(*iter)]->second << ".");
153 PermutationMap[(*iter)] = DistanceIterators[(*iter)]->second;
154 } else {
155 DistanceIterators[(*runner)] = Rider; // if successful also move the pointer in the iterator list
156 LOG(3, "Found a better permutation, new potential is " << Potential << " vs." << OldPotential << ".");
157 OldPotential = Potential;
158 }
159 if (Potential > PenaltyConstants[2]) {
160 ELOG(1, "The two-step permutation procedure did not maintain injectivity!");
161 exit(255);
162 }
163 } else {
164 ELOG(1, **runner << " was not the owner of " << *Sprinter << "!");
165 exit(255);
166 }
167 } else {
168 PermutationMap[(*iter)] = DistanceIterators[(*iter)]->second; // new target has no source!
169 }
170 StepList[(*iter)]++; // take next farther distance target
171 }
172 } while (++iter != atoms.end());
173 } while ((OlderPotential - OldPotential) > 1e-3);
174 LOG(1, "done.");
175
176
177 return ConstrainedPotential();
178};
179
180void MinimiseConstrainedPotential::FillDistanceList()
181{
182 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
183 for (World::AtomComposite::const_iterator runner = atoms.begin(); runner != atoms.end(); ++runner) {
184 DistanceList[(*iter)].insert( DistancePair((*iter)->getPositionAtStep(startstep).distance((*runner)->getPositionAtStep(endstep)), (*runner)) );
185 }
186 }
187};
188
189void MinimiseConstrainedPotential::CreateInitialLists()
190{
191 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
192 StepList[(*iter)] = DistanceList[(*iter)].begin(); // stores the step to the next iterator that could be a possible next target
193 PermutationMap[(*iter)] = DistanceList[(*iter)].begin()->second; // always pick target with the smallest distance
194 DoubleList[DistanceList[(*iter)].begin()->second]++; // increase this target's source count (>1? not injective)
195 DistanceIterators[(*iter)] = DistanceList[(*iter)].begin(); // and remember which one we picked
196 LOG(2, **iter << " starts with distance " << DistanceList[(*iter)].begin()->first << ".");
197 }
198};
199
200void MinimiseConstrainedPotential::MakeInjectivePermutation()
201{
202 World::AtomComposite::const_iterator iter = atoms.begin();
203 DistanceMap::iterator NewBase;
204 double Potential = fabs(ConstrainedPotential());
205
206 if (atoms.empty()) {
207 ELOG(1, "Molecule is empty.");
208 return;
209 }
210 while ((Potential) > PenaltyConstants[2]) {
211 CalculateDoubleList();
212 PrintPermutationMap();
213 iter++;
214 if (iter == atoms.end()) // round-robin at the end
215 iter = atoms.begin();
216 if (DoubleList[DistanceIterators[(*iter)]->second] <= 1) // no need to make those injective that aren't
217 continue;
218 // now, try finding a new one
219 Potential = TryNextNearestNeighbourForInjectivePermutation((*iter), Potential);
220 }
221 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
222 // now each single entry in the DoubleList should be <=1
223 if (DoubleList[*iter] > 1) {
224 ELOG(0, "Failed to create an injective PermutationMap!");
225 performCriticalExit();
226 }
227 }
228 LOG(1, "done.");
229};
230
231unsigned int MinimiseConstrainedPotential::CalculateDoubleList()
232{
233 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter)
234 DoubleList[*iter] = 0;
235 unsigned int doubles = 0;
236 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter)
237 DoubleList[ PermutationMap[*iter] ]++;
238 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter)
239 if (DoubleList[*iter] > 1)
240 doubles++;
241 if (doubles >0)
242 LOG(2, "Found " << doubles << " Doubles.");
243 return doubles;
244};
245
246void MinimiseConstrainedPotential::PrintPermutationMap() const
247{
248 stringstream zeile1, zeile2;
249 int doubles = 0;
250 zeile1 << "PermutationMap: ";
251 zeile2 << " ";
252 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
253 zeile1 << (*iter)->getName() << " ";
254 zeile2 << (PermutationMap[*iter])->getName() << " ";
255 }
256 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
257 std::map<atom *, unsigned int>::const_iterator value_iter = DoubleList.find(*iter);
258 if (value_iter->second > (unsigned int)1)
259 doubles++;
260 }
261 if (doubles >0)
262 LOG(2, "Found " << doubles << " Doubles.");
263// LOG(2, zeile1.str() << endl << zeile2.str());
264};
265
266double MinimiseConstrainedPotential::ConstrainedPotential()
267{
268 double tmp = 0.;
269 double result = 0.;
270 // go through every atom
271 atom *Runner = NULL;
272 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
273 // first term: distance to target
274 Runner = PermutationMap[(*iter)]; // find target point
275 tmp = ((*iter)->getPositionAtStep(startstep).distance(Runner->getPositionAtStep(endstep)));
276 tmp *= IsAngstroem ? 1. : 1./AtomicLengthToAngstroem;
277 result += PenaltyConstants[0] * tmp;
278 //LOG(4, "Adding " << tmp*constants[0] << ".");
279
280 // second term: sum of distances to other trajectories
281 result += SumDistanceOfTrajectories((*iter));
282
283 // third term: penalty for equal targets
284 result += PenalizeEqualTargets((*iter));
285 }
286
287 return result;
288};
289
290double MinimiseConstrainedPotential::TryNextNearestNeighbourForInjectivePermutation(atom *Walker, double &OldPotential)
291{
292 double Potential = 0;
293 DistanceMap::iterator NewBase = DistanceIterators[Walker]; // store old base
294 do {
295 NewBase++; // take next further distance in distance to targets list that's a target of no one
296 } while ((DoubleList[NewBase->second] != 0) && (NewBase != DistanceList[Walker].end()));
297 if (NewBase != DistanceList[Walker].end()) {
298 PermutationMap[Walker] = NewBase->second;
299 Potential = fabs(ConstrainedPotential());
300 if (Potential > OldPotential) { // undo
301 PermutationMap[Walker] = DistanceIterators[Walker]->second;
302 } else { // do
303 DoubleList[DistanceIterators[Walker]->second]--; // decrease the old entry in the doubles list
304 DoubleList[NewBase->second]++; // increase the old entry in the doubles list
305 DistanceIterators[Walker] = NewBase;
306 OldPotential = Potential;
307 LOG(3, "Found a new permutation, new potential is " << OldPotential << ".");
308 }
309 }
310 return Potential;
311};
312
313double MinimiseConstrainedPotential::PenalizeEqualTargets(atom *Walker)
314{
315 double result = 0.;
316 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
317 if ((PermutationMap[Walker] == PermutationMap[(*iter)]) && (Walker < (*iter))) {
318// atom *Sprinter = PermutationMap[Walker->nr];
319// if (DoLog(0)) {
320// std::stringstream output;
321// output << *Walker << " and " << *(*iter) << " are heading to the same target at ";
322// output << Sprinter->getPosition(endstep);
323// output << ", penalting.";
324// LOG(0, output.str());
325// }
326 result += PenaltyConstants[2];
327 //LOG(4, "INFO: Adding " << constants[2] << ".");
328 }
329 }
330 return result;
331};
332
333double MinimiseConstrainedPotential::SumDistanceOfTrajectories(atom *Walker)
334{
335 gsl_matrix *A = gsl_matrix_alloc(NDIM,NDIM);
336 gsl_vector *x = gsl_vector_alloc(NDIM);
337 atom *Sprinter = NULL;
338 Vector trajectory1, trajectory2, normal, TestVector;
339 double Norm1, Norm2, tmp, result = 0.;
340
341 for (World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
342 if ((*iter) == Walker) // hence, we only go up to the Walker, not beyond (similar to i=0; i<j; i++)
343 break;
344 // determine normalized trajectories direction vector (n1, n2)
345 Sprinter = PermutationMap[Walker]; // find first target point
346 trajectory1 = Sprinter->getPositionAtStep(endstep) - Walker->getPositionAtStep(startstep);
347 trajectory1.Normalize();
348 Norm1 = trajectory1.Norm();
349 Sprinter = PermutationMap[(*iter)]; // find second target point
350 trajectory2 = Sprinter->getPositionAtStep(endstep) - (*iter)->getPositionAtStep(startstep);
351 trajectory2.Normalize();
352 Norm2 = trajectory1.Norm();
353 // check whether either is zero()
354 if ((Norm1 < MYEPSILON) && (Norm2 < MYEPSILON)) {
355 tmp = Walker->getPositionAtStep(startstep).distance((*iter)->getPositionAtStep(startstep));
356 } else if (Norm1 < MYEPSILON) {
357 Sprinter = PermutationMap[Walker]; // find first target point
358 trajectory1 = Sprinter->getPositionAtStep(endstep) - (*iter)->getPositionAtStep(startstep);
359 trajectory2 *= trajectory1.ScalarProduct(trajectory2); // trajectory2 is scaled to unity, hence we don't need to divide by anything
360 trajectory1 -= trajectory2; // project the part in norm direction away
361 tmp = trajectory1.Norm(); // remaining norm is distance
362 } else if (Norm2 < MYEPSILON) {
363 Sprinter = PermutationMap[(*iter)]; // find second target point
364 trajectory2 = Sprinter->getPositionAtStep(endstep) - Walker->getPositionAtStep(startstep); // copy second offset
365 trajectory1 *= trajectory2.ScalarProduct(trajectory1); // trajectory1 is scaled to unity, hence we don't need to divide by anything
366 trajectory2 -= trajectory1; // project the part in norm direction away
367 tmp = trajectory2.Norm(); // remaining norm is distance
368 } else if ((fabs(trajectory1.ScalarProduct(trajectory2)/Norm1/Norm2) - 1.) < MYEPSILON) { // check whether they're linear dependent
369// std::stringstream output;
370// output << "Both trajectories of " << *Walker << " and " << *Runner << " are linear dependent: ";
371// output << trajectory1 << " and " << trajectory2;
372// LOG(3, output.str());
373 tmp = Walker->getPositionAtStep(startstep).distance((*iter)->getPositionAtStep(startstep));
374// LOG(0, " with distance " << tmp << ".");
375 } else { // determine distance by finding minimum distance
376// std::stringstream output;
377// output "Both trajectories of " << *Walker << " and " << *(*iter) << " are linear independent -- ";
378// output "First Trajectory: " << trajectory1 << ". Second Trajectory: " << trajectory2);
379// LOG(3, output.str());
380 // determine normal vector for both
381 normal = Plane(trajectory1, trajectory2,0).getNormal();
382 // print all vectors for debugging
383// LOG(3, "INFO: Normal vector in between: " << normal);
384 // setup matrix
385 for (int i=NDIM;i--;) {
386 gsl_matrix_set(A, 0, i, trajectory1[i]);
387 gsl_matrix_set(A, 1, i, trajectory2[i]);
388 gsl_matrix_set(A, 2, i, normal[i]);
389 gsl_vector_set(x,i, (Walker->getPositionAtStep(startstep)[i] - (*iter)->getPositionAtStep(startstep)[i]));
390 }
391 // solve the linear system by Householder transformations
392 gsl_linalg_HH_svx(A, x);
393 // distance from last component
394 tmp = gsl_vector_get(x,2);
395// LOG(0, " with distance " << tmp << ".");
396 // test whether we really have the intersection (by checking on c_1 and c_2)
397 trajectory1.Scale(gsl_vector_get(x,0));
398 trajectory2.Scale(gsl_vector_get(x,1));
399 normal.Scale(gsl_vector_get(x,2));
400 TestVector = (*iter)->getPositionAtStep(startstep) + trajectory2 + normal
401 - (Walker->getPositionAtStep(startstep) + trajectory1);
402 if (TestVector.Norm() < MYEPSILON) {
403// LOG(2, "Test: ok.\tDistance of " << tmp << " is correct.");
404 } else {
405// LOG(2, "Test: failed.\tIntersection is off by " << TestVector << ".");
406 }
407 }
408 // add up
409 tmp *= IsAngstroem ? 1. : 1./AtomicLengthToAngstroem;
410 if (fabs(tmp) > MYEPSILON) {
411 result += PenaltyConstants[1] * 1./tmp;
412 //LOG(4, "Adding " << 1./tmp*constants[1] << ".");
413 }
414 }
415 return result;
416};
417
418void MinimiseConstrainedPotential::EvaluateConstrainedForces(ForceMatrix *Force)
419{
420 double constant = 10.;
421
422 /// evaluate forces (only the distance to target dependent part) with the final PermutationMap
423 LOG(1, "Calculating forces and adding onto ForceMatrix ... ");
424 for(World::AtomComposite::const_iterator iter = atoms.begin(); iter != atoms.end(); ++iter) {
425 atom *Sprinter = PermutationMap[(*iter)];
426 // set forces
427 for (int i=NDIM;i++;)
428 Force->Matrix[0][(*iter)->getNr()][5+i] += 2.*constant*sqrt((*iter)->getPositionAtStep(startstep).distance(Sprinter->getPositionAtStep(endstep)));
429 }
430 LOG(1, "done.");
431};
432
Note: See TracBrowser for help on using the repository browser.