Line data Source code
1 : /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 : Copyright (c) 2017-2023 The plumed team
3 : (see the PEOPLE file at the root of the distribution for a list of names)
4 :
5 : See http://www.plumed.org for more information.
6 :
7 : This file is part of plumed, version 2.
8 :
9 : plumed is free software: you can redistribute it and/or modify
10 : it under the terms of the GNU Lesser General Public License as published by
11 : the Free Software Foundation, either version 3 of the License, or
12 : (at your option) any later version.
13 :
14 : plumed 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 Lesser General Public License for more details.
18 :
19 : You should have received a copy of the GNU Lesser General Public License
20 : along with plumed. If not, see <http://www.gnu.org/licenses/>.
21 : +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
22 : /*
23 :
24 : */
25 : #include "bias/Bias.h"
26 : #include "core/ActionRegister.h"
27 : #include "core/PlumedMain.h"
28 : #include "core/Value.h"
29 : #include "tools/File.h"
30 : #include "tools/Random.h"
31 : #include "tools/Communicator.h"
32 : #include <ctime>
33 :
34 : namespace PLMD {
35 : namespace isdb {
36 :
37 : //+PLUMEDOC ISDB_BIAS RESCALE
38 : /*
39 : Scales the value of an another action, being a Collective Variable or a Bias.
40 :
41 : The rescaling factor is determined by a parameter defined on a logarithmic grid of dimension NBIN in the range
42 : from 1 to MAX_RESCALE. The current value of the rescaling parameter is stored and shared across
43 : other actions using a [SELECTOR](SELECTOR.md). A Monte Carlo procedure is used to update the value
44 : of the rescaling factor every MC_STRIDE steps of molecular dynamics. Well-tempered metadynamics, defined by the
45 : parameters W0 and BIASFACTOR, is used to enhance the sampling in the space of the rescaling factor.
46 : The well-tempered metadynamics bias potential is written to the file BFILE every BSTRIDE steps and read
47 : when restarting the simulation using the directive [RESTART](RESTART.md).
48 :
49 : !!! note "scaling arguments"
50 :
51 : Additional arguments not to be scaled, one for each bin in the rescaling parameter ladder, can be
52 : provided at the end of the ARG list. The number of such arguments is specified by the option NOT_RESCALED.
53 : These arguments will be not be scaled, but they will be
54 : considered as bias potentials and used in the computation of the Metropolis
55 : acceptance probability when proposing a move in the rescaling parameter. See example below.
56 :
57 : !!! note "multiple replicas"
58 :
59 : If PLUMED is running in a multiple-replica framework (for example using the -multi option in GROMACS),
60 : the arguments will be summed across replicas, unless the NOT_SHARED option is used. Also, the value of the
61 : [SELECTOR](SELECTOR.md) will be shared and thus will be the same in all replicas.
62 :
63 : ## Examples
64 :
65 : In this example we use [RESCALE](RESCALE.md) to implement a simulated-tempering like approach.
66 : The total potential energy of the system is scaled by a parameter defined on a logarithmic grid
67 : of 5 bins in the range from 1 to 1.5.
68 : A well-tempered metadynamics bias potential is used to ensure diffusion in the space of the rescaling
69 : parameter.
70 :
71 : ```plumed
72 : ene: ENERGY
73 :
74 : SELECTOR NAME=GAMMA VALUE=0
75 :
76 : RESCALE ...
77 : LABEL=res ARG=ene TEMP=300
78 : SELECTOR=GAMMA MAX_RESCALE=1.5 NBIN=5
79 : W0=1000 BIASFACTOR=100.0 BSTRIDE=2000 BFILE=bias.dat
80 : ...
81 :
82 : PRINT FILE=COLVAR ARG=* STRIDE=100
83 : ```
84 :
85 : In this second example, we add to the simulated-tempering approach introduced above
86 : one Parallel Bias metadynamics simulation (see [PBMETAD](PBMETAD.md)) for each value of the rescaling parameter.
87 : At each moment of the simulation, only one of the [PBMETAD](PBMETAD.md)
88 : actions is activated, based on the current value of the associated [SELECTOR](SELECTOR.md).
89 : The [PBMETAD](PBMETAD.md) bias potentials are not scaled, but just used in the calculation of
90 : the Metropolis acceptance probability when proposing a move in the rescaling parameter.
91 :
92 : ```plumed
93 : ene: ENERGY
94 : d: DISTANCE ATOMS=1,2
95 :
96 : SELECTOR NAME=GAMMA VALUE=0
97 :
98 : pbmetad0: PBMETAD ARG=d SELECTOR=GAMMA SELECTOR_ID=0 SIGMA=0.1 PACE=500 HEIGHT=1 BIASFACTOR=8 FILE=HILLS.0
99 : pbmetad1: PBMETAD ARG=d SELECTOR=GAMMA SELECTOR_ID=1 SIGMA=0.1 PACE=500 HEIGHT=1 BIASFACTOR=8 FILE=HILLS.1
100 : pbmetad2: PBMETAD ARG=d SELECTOR=GAMMA SELECTOR_ID=2 SIGMA=0.1 PACE=500 HEIGHT=1 BIASFACTOR=8 FILE=HILLS.2
101 : pbmetad3: PBMETAD ARG=d SELECTOR=GAMMA SELECTOR_ID=3 SIGMA=0.1 PACE=500 HEIGHT=1 BIASFACTOR=8 FILE=HILLS.3
102 : pbmetad4: PBMETAD ARG=d SELECTOR=GAMMA SELECTOR_ID=4 SIGMA=0.1 PACE=500 HEIGHT=1 BIASFACTOR=8 FILE=HILLS.4
103 :
104 : RESCALE ...
105 : LABEL=res TEMP=300
106 : ARG=ene,pbmetad0.bias,pbmetad1.bias,pbmetad2.bias,pbmetad3.bias,pbmetad4.bias
107 : SELECTOR=GAMMA MAX_RESCALE=1.5 NOT_RESCALED=5 NBIN=5
108 : W0=1000 BIASFACTOR=100.0 BSTRIDE=2000 BFILE=bias.dat
109 : ...
110 :
111 : PRINT FILE=COLVAR ARG=* STRIDE=100
112 : ```
113 :
114 :
115 : */
116 : //+ENDPLUMEDOC
117 :
118 : class Rescale : public bias::Bias {
119 : // gamma parameter
120 : std::vector<double> gamma_;
121 : double w0_;
122 : double biasf_;
123 : std::vector<double> bias_;
124 : std::vector<double> expo_;
125 : std::vector<unsigned> shared_;
126 : unsigned nores_;
127 : // bias
128 : unsigned int Biasstride_;
129 : unsigned int Biaspace_;
130 : std::string Biasfilename_;
131 : bool first_bias_;
132 : OFile Biasfile_;
133 : // temperature in kbt
134 : double kbt_;
135 : // Monte Carlo stuff
136 : unsigned MCsteps_;
137 : unsigned MCstride_;
138 : long long int MCfirst_;
139 : long long unsigned MCaccgamma_;
140 : Random random_;
141 : // replica stuff
142 : unsigned nrep_;
143 : unsigned replica_;
144 : // selector
145 : std::string selector_;
146 :
147 : // Monte Carlo
148 : void doMonteCarlo(unsigned igamma, double oldE, const std::vector<double> & args, const std::vector<double> & bargs);
149 : unsigned proposeMove(unsigned x, unsigned xmin, unsigned xmax);
150 : // current value of the SELECTOR, checked against the number of bins
151 : unsigned getSelector();
152 : bool doAccept(double oldE, double newE);
153 : // read and print bias
154 : void read_bias();
155 : void print_bias(long long int step);
156 :
157 : public:
158 : explicit Rescale(const ActionOptions&);
159 : ~Rescale();
160 : void calculate();
161 : static void registerKeywords(Keywords& keys);
162 : };
163 :
164 :
165 : PLUMED_REGISTER_ACTION(Rescale,"RESCALE")
166 :
167 6 : void Rescale::registerKeywords(Keywords& keys) {
168 6 : Bias::registerKeywords(keys);
169 6 : keys.add("compulsory","TEMP","temperature");
170 6 : keys.add("compulsory","SELECTOR", "name of the SELECTOR used for rescaling");
171 6 : keys.add("compulsory","MAX_RESCALE","maximum values for rescaling");
172 6 : keys.add("compulsory","NBIN","number of bins for gamma grid");
173 6 : keys.add("compulsory","W0", "initial bias height");
174 6 : keys.add("compulsory","BIASFACTOR", "bias factor");
175 6 : keys.add("compulsory","BSTRIDE", "stride for writing bias");
176 6 : keys.add("compulsory","BFILE", "file name for bias");
177 6 : keys.add("optional","NOT_SHARED", "list of arguments (from 1 to N) not summed across replicas");
178 6 : keys.add("optional","NOT_RESCALED", "these last N arguments will not be scaled");
179 6 : keys.add("optional","MC_STEPS","number of MC steps");
180 6 : keys.add("optional","MC_STRIDE","MC stride");
181 6 : keys.add("optional","PACE", "Pace for adding bias, in MC stride unit");
182 6 : keys.add("optional","SEED","seed of the random number generator used by the Monte Carlo sampling (by default it is taken from the clock at every run)");
183 12 : keys.addOutputComponent("igamma", "default","scalar","gamma parameter");
184 12 : keys.addOutputComponent("accgamma","default","scalar","MC acceptance for gamma");
185 12 : keys.addOutputComponent("wtbias", "default","scalar","well-tempered bias");
186 6 : }
187 :
188 2 : Rescale::Rescale(const ActionOptions&ao):
189 : PLUMED_BIAS_INIT(ao),
190 2 : nores_(0), Biaspace_(1), first_bias_(true),
191 2 : MCsteps_(1), MCstride_(1), MCfirst_(-1), MCaccgamma_(0) {
192 : // set up replica stuff
193 2 : nrep_ = getNumberOfReplicas();
194 2 : replica_ = getReplicaIndex();
195 :
196 : // wt-parameters
197 2 : parse("W0", w0_);
198 2 : parse("BIASFACTOR", biasf_);
199 :
200 : // selector name
201 2 : parse("SELECTOR", selector_);
202 :
203 : // number of bins for gamma ladder
204 : unsigned nbin;
205 2 : parse("NBIN", nbin);
206 :
207 : // number of bias
208 2 : parse("NOT_RESCALED", nores_);
209 2 : if(nores_>0 && nores_!=nbin) {
210 0 : error("The number of non scaled arguments must be equal to either 0 or the number of bins");
211 : }
212 :
213 : // maximum value of rescale
214 2 : std::vector<double> max_rescale;
215 4 : parseVector("MAX_RESCALE", max_rescale);
216 : // check dimension of max_rescale
217 2 : if(max_rescale.size()!=(getNumberOfArguments()-nores_)) {
218 0 : error("Size of MAX_RESCALE array must be equal to the number of arguments that will to be scaled");
219 : }
220 :
221 : // calculate exponents
222 2 : double igamma_max = static_cast<double>(nbin);
223 6 : for(unsigned i=0; i<max_rescale.size(); ++i) {
224 4 : expo_.push_back(std::log(max_rescale[i])/std::log(igamma_max));
225 : }
226 :
227 : // allocate gamma grid and set bias to zero
228 10 : for(unsigned i=0; i<nbin; ++i) {
229 : // bias grid
230 8 : bias_.push_back(0.0);
231 : // gamma ladder
232 8 : double gamma = std::exp( static_cast<double>(i) / static_cast<double>(nbin-1) * std::log(igamma_max) );
233 8 : gamma_.push_back(gamma);
234 : }
235 : // print bias to file
236 2 : parse("BSTRIDE", Biasstride_);
237 2 : parse("BFILE", Biasfilename_);
238 :
239 : // create vectors of shared arguments
240 : // by default they are all shared
241 6 : for(unsigned i=0; i<getNumberOfArguments(); ++i) {
242 4 : shared_.push_back(1);
243 : }
244 : // share across replicas or not
245 2 : std::vector<unsigned> not_shared;
246 2 : parseVector("NOT_SHARED", not_shared);
247 : // and change the non-shared
248 2 : for(unsigned i=0; i<not_shared.size(); ++i) {
249 0 : if((not_shared[i]-1)>=(getNumberOfArguments()-nores_) && nrep_>1) {
250 0 : error("NOT_RESCALED args must always be shared when using multiple replicas");
251 : }
252 0 : if((not_shared[i]-1)>=getNumberOfArguments()) {
253 0 : error("NOT_SHARED args should be lower than total number of arguments");
254 : }
255 0 : shared_[not_shared[i]-1] = 0;
256 : }
257 :
258 : // monte carlo stuff
259 2 : parse("MC_STEPS",MCsteps_);
260 2 : parse("MC_STRIDE",MCstride_);
261 : // adjust for multiple-time steps
262 2 : MCstride_ *= getStride();
263 : // read bias deposition pace
264 2 : parse("PACE", Biaspace_);
265 2 : int seed=-1;
266 2 : parse("SEED",seed);
267 2 : if(seed<-1) {
268 0 : error("SEED must be a non-negative integer");
269 : }
270 : // multiply by MCstride
271 2 : Biaspace_ *= MCstride_;
272 :
273 : // get temperature
274 2 : kbt_=getkBT();
275 :
276 2 : checkRead();
277 :
278 2 : log.printf(" temperature of the system in energy unit %f\n",kbt_);
279 2 : log.printf(" name of the SELECTOR use for this action %s\n",selector_.c_str());
280 2 : log.printf(" number of bins in grid %u\n",nbin);
281 2 : log.printf(" number of arguments that will not be scaled %u\n",nores_);
282 2 : if(nrep_>1) {
283 2 : log<<" number of arguments that will not be summed across replicas "<<not_shared.size()<<"\n";
284 : }
285 2 : log.printf(" biasfactor %f\n",biasf_);
286 2 : log.printf(" initial hills height %f\n",w0_);
287 2 : log.printf(" stride to write bias to file %u\n",Biasstride_);
288 2 : log.printf(" write bias to file : %s\n",Biasfilename_.c_str());
289 2 : log.printf(" number of replicas %u\n",nrep_);
290 2 : log.printf(" number of MC steps %d\n",MCsteps_);
291 2 : log.printf(" do MC every %d steps\n", MCstride_);
292 2 : log.printf("\n");
293 :
294 2 : log << " Bibliography" << plumed.cite("Bonomi, Camilloni, Bioinformatics, 33, 3999 (2017)") << "\n";
295 :
296 :
297 : // add components
298 4 : addComponent("igamma");
299 4 : componentIsNotPeriodic("igamma");
300 4 : addComponent("accgamma");
301 4 : componentIsNotPeriodic("accgamma");
302 4 : addComponent("wtbias");
303 2 : componentIsNotPeriodic("wtbias");
304 :
305 : // initialize random seed
306 : // each replica gets its own stream, between 1 and 2^31-1
307 : unsigned iseed;
308 2 : if(seed>=0) {
309 2 : iseed = static_cast<unsigned>(1 + (static_cast<unsigned long long>(seed)*nrep_+replica_) % 2147483646ULL);
310 : } else {
311 0 : iseed = static_cast<unsigned>(time(NULL))+replica_;
312 : }
313 2 : random_.setSeed(-static_cast<int>(iseed));
314 :
315 : // read bias if restarting
316 2 : if(getRestart()) {
317 0 : read_bias();
318 : }
319 4 : }
320 :
321 4 : Rescale::~Rescale() {
322 2 : Biasfile_.close();
323 8 : }
324 :
325 0 : void Rescale::read_bias() {
326 : // open file
327 : auto ifile=Tools::make_unique<IFile>();
328 0 : ifile->link(*this);
329 0 : if(ifile->FileExist(Biasfilename_)) {
330 0 : ifile->open(Biasfilename_);
331 : // read all the lines, store last value of bias
332 : double MDtime;
333 0 : while(ifile->scanField("MD_time",MDtime)) {
334 0 : for(unsigned i=0; i<bias_.size(); ++i) {
335 : // convert i to string
336 0 : std::stringstream ss;
337 : ss << i;
338 : // label
339 0 : std::string label = "b" + ss.str();
340 : // read entry
341 0 : ifile->scanField(label, bias_[i]);
342 0 : }
343 : // new line
344 0 : ifile->scanField();
345 : }
346 0 : ifile->close();
347 : } else {
348 0 : error("Cannot find bias file "+Biasfilename_+"\n");
349 : }
350 0 : }
351 :
352 24 : unsigned Rescale::getSelector() {
353 24 : const auto it = plumed.passMap.find(selector_);
354 24 : if(it==plumed.passMap.end()) {
355 0 : error("SELECTOR "+selector_+" is not defined");
356 : }
357 24 : if(!(it->second>=0.0) || it->second>=static_cast<double>(gamma_.size())) {
358 0 : error("the value of SELECTOR "+selector_+" must be between 0 and NBIN-1");
359 : }
360 24 : return static_cast<unsigned>(it->second);
361 : }
362 :
363 60 : unsigned Rescale::proposeMove(unsigned x, unsigned xmin, unsigned xmax) {
364 60 : int xmin_i = static_cast<int>(xmin);
365 60 : int xmax_i = static_cast<int>(xmax);
366 : int dx;
367 60 : if( random_.RandU01() < 0.5 ) {
368 : dx = +1;
369 : } else {
370 : dx = -1;
371 : }
372 : // new index, integer
373 60 : int x_new = static_cast<int>(x) + dx;
374 : // check boundaries
375 60 : if(x_new >= xmax_i) {
376 22 : x_new = xmax_i-1;
377 : }
378 60 : if(x_new < xmin_i) {
379 : x_new = xmin_i;
380 : }
381 60 : return static_cast<unsigned>(x_new);
382 : }
383 :
384 60 : bool Rescale::doAccept(double oldE, double newE) {
385 : bool accept = false;
386 : // calculate delta energy
387 60 : double delta = ( newE - oldE ) / kbt_;
388 : // if delta is negative always accept move
389 60 : if( delta < 0.0 ) {
390 : accept = true;
391 : } else {
392 : // otherwise extract random number
393 53 : double s = random_.RandU01();
394 53 : if( s < std::exp(-delta) ) {
395 : accept = true;
396 : }
397 : }
398 60 : return accept;
399 : }
400 :
401 12 : void Rescale::doMonteCarlo(unsigned igamma, double oldE,
402 : const std::vector<double> & args, const std::vector<double> & bargs) {
403 : double oldB, newB;
404 :
405 : // cycle on MC steps
406 72 : for(unsigned i=0; i<MCsteps_; ++i) {
407 : // propose move in igamma
408 60 : unsigned new_igamma = proposeMove(igamma, 0, gamma_.size());
409 : // calculate new energy
410 : double newE = 0.0;
411 180 : for(unsigned j=0; j<args.size(); ++j) {
412 : // calculate energy term
413 120 : double fact = 1.0/pow(gamma_[new_igamma], expo_[j]) - 1.0;
414 120 : newE += args[j] * fact;
415 : }
416 : // calculate contributions from non-rescaled terms
417 60 : if(bargs.size()>0) {
418 0 : oldB = bias_[igamma]+bargs[igamma];
419 0 : newB = bias_[new_igamma]+bargs[new_igamma];
420 : } else {
421 60 : oldB = bias_[igamma];
422 60 : newB = bias_[new_igamma];
423 : }
424 : // accept or reject
425 60 : bool accept = doAccept(oldE+oldB, newE+newB);
426 60 : if(accept) {
427 29 : igamma = new_igamma;
428 : oldE = newE;
429 29 : MCaccgamma_++;
430 : }
431 : }
432 : // send values of gamma to all replicas
433 12 : if(comm.Get_rank()==0) {
434 12 : if(multi_sim_comm.Get_rank()!=0) {
435 6 : igamma = 0;
436 : }
437 12 : multi_sim_comm.Sum(&igamma, 1);
438 : } else {
439 0 : igamma = 0;
440 : }
441 : // local communication
442 12 : comm.Sum(&igamma, 1);
443 :
444 : // set the value of gamma into passMap
445 12 : plumed.passMap[selector_]=static_cast<double>(igamma);
446 12 : }
447 :
448 12 : void Rescale::print_bias(long long int step) {
449 : // if first time open the file
450 12 : if(first_bias_) {
451 2 : first_bias_ = false;
452 2 : Biasfile_.link(*this);
453 2 : Biasfile_.open(Biasfilename_);
454 : Biasfile_.setHeavyFlush();
455 4 : Biasfile_.fmtField("%30.5f");
456 : }
457 :
458 : // write fields
459 12 : double MDtime = static_cast<double>(step)*getTimeStep();
460 12 : Biasfile_.printField("MD_time", MDtime);
461 60 : for(unsigned i=0; i<bias_.size(); ++i) {
462 : // convert i to string
463 48 : std::stringstream ss;
464 : ss << i;
465 : // label
466 96 : std::string label = "b" + ss.str();
467 : // print entry
468 48 : Biasfile_.printField(label, bias_[i]);
469 48 : }
470 12 : Biasfile_.printField();
471 12 : }
472 :
473 12 : void Rescale::calculate() {
474 : // get the current value of the selector
475 12 : const unsigned igamma = getSelector();
476 :
477 : // collect data from other replicas
478 12 : std::vector<double> all_args(getNumberOfArguments(), 0.0);
479 : // first calculate arguments
480 36 : for(unsigned i=0; i<all_args.size(); ++i) {
481 24 : double arg = getArgument(i);
482 : // sum shared arguments across replicas
483 24 : if(shared_[i]==1) {
484 24 : if(comm.Get_rank()==0) {
485 24 : multi_sim_comm.Sum(arg);
486 : } else {
487 0 : arg = 0.0;
488 : }
489 24 : if(comm.Get_size()>1) {
490 0 : comm.Sum(arg);
491 : }
492 : }
493 : // put into all_args
494 24 : all_args[i] = arg;
495 : }
496 :
497 : // now separate terms that should be rescaled
498 12 : std::vector<double> args;
499 12 : if(getNumberOfArguments()>nores_) {
500 12 : args.resize(getNumberOfArguments()-nores_);
501 : }
502 36 : for(unsigned i=0; i<args.size(); ++i) {
503 24 : args[i] = all_args[i];
504 : }
505 : // and terms that should not
506 12 : std::vector<double> bargs;
507 12 : if(nores_>0) {
508 0 : bargs.resize(nores_);
509 : }
510 12 : for(unsigned i=0; i<bargs.size(); ++i) {
511 0 : bargs[i] = all_args[i+args.size()];
512 : }
513 :
514 : // calculate energy and forces, only on rescaled terms
515 : double ene = 0.0;
516 36 : for(unsigned i=0; i<args.size(); ++i) {
517 : // calculate energy term
518 24 : double fact = 1.0/pow(gamma_[igamma], expo_[i]) - 1.0;
519 24 : ene += args[i] * fact;
520 : // add force
521 24 : setOutputForce(i, -fact);
522 : }
523 :
524 : // set to zero on the others
525 12 : for(unsigned i=0; i<bargs.size(); ++i) {
526 0 : setOutputForce(i+args.size(), 0.0);
527 : }
528 :
529 : // set value of the bias
530 12 : setBias(ene);
531 : // set value of the wt-bias
532 12 : getPntrToComponent("wtbias")->set(bias_[igamma]);
533 : // set values of gamma
534 12 : getPntrToComponent("igamma")->set(igamma);
535 : // get time step
536 12 : long long int step = getStep();
537 12 : if(MCfirst_==-1) {
538 2 : MCfirst_=step;
539 : }
540 : // calculate gamma acceptance
541 12 : double MCtrials = std::floor(static_cast<double>(step-MCfirst_) / static_cast<double>(MCstride_))+1.0;
542 12 : double accgamma = static_cast<double>(MCaccgamma_) / static_cast<double>(MCsteps_) / MCtrials;
543 12 : getPntrToComponent("accgamma")->set(accgamma);
544 :
545 : // do MC at the right time step
546 12 : if(step%MCstride_==0&&!getExchangeStep()) {
547 12 : doMonteCarlo(igamma, ene, args, bargs);
548 : }
549 :
550 : // add well-tempered like bias
551 12 : if(step%Biaspace_==0) {
552 : // get updated igamma
553 12 : const unsigned newigamma = getSelector();
554 : // add "Gaussian"
555 12 : double kbDT = kbt_ * ( biasf_ - 1.0 );
556 12 : bias_[newigamma] += w0_ * std::exp(-bias_[newigamma] / kbDT);
557 : }
558 :
559 : // print bias
560 12 : if(step%Biasstride_==0) {
561 12 : print_bias(step);
562 : }
563 :
564 12 : }
565 :
566 :
567 : }
568 : }
569 :
|