export_gauss_newton_block_forces.cpp
Go to the documentation of this file.
1 /*
2  * This file is part of ACADO Toolkit.
3  *
4  * ACADO Toolkit -- A Toolkit for Automatic Control and Dynamic Optimization.
5  * Copyright (C) 2008-2014 by Boris Houska, Hans Joachim Ferreau,
6  * Milan Vukov, Rien Quirynen, KU Leuven.
7  * Developed within the Optimization in Engineering Center (OPTEC)
8  * under supervision of Moritz Diehl. All rights reserved.
9  *
10  * ACADO Toolkit is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public
12  * License as published by the Free Software Foundation; either
13  * version 3 of the License, or (at your option) any later version.
14  *
15  * ACADO Toolkit is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with ACADO Toolkit; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23  *
24  */
25 
33 
34 #include <acado/code_generation/templates/templates.hpp>
35 
36 using namespace std;
37 
39 
41  const std::string& _commonHeaderName
42  ) : ExportGaussNewtonBlockCN2( _userInteraction,_commonHeaderName )
43 {
44  qpObjPrefix = "acadoForces";
45  qpModuleName = "forces";
46 }
47 
49 {
51  if( status != SUCCESSFUL_RETURN ) return status;
52 
53  LOG( LVL_DEBUG ) << "Solver: setup extra initialization... " << endl;
54  // Add QP initialization call to the initialization
55  ExportFunction initializeForces( "initializeForces" );
56  initializeForces.setName( "initializeForces" );
57  initialize.addFunctionCall( initializeForces );
58  LOG( LVL_DEBUG ) << "done!" << endl;
59 
60  return SUCCESSFUL_RETURN;
61 }
62 
64  )
65 {
67  code.addStatement( *qpInterface );
68 
69  code.addFunction( cleanup );
70  code.addFunction( shiftQpData );
71 
74 
75  return ExportGaussNewtonCN2::getCode( code );
76 }
77 
79 {
81  if( status != SUCCESSFUL_RETURN ) return status;
82 
83  objHessians.clear();
84  objHessians.resize(getNumberOfBlocks() + 1);
85  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
86  {
87  objHessians[ i ].setup(string("H") + toString(i + 1), getNumBlockVariables(), getNumBlockVariables(), REAL, FORCES_PARAMS, false, qpObjPrefix);
88  }
89  objHessians[ getNumberOfBlocks() ].setup(string("H") + toString(getNumberOfBlocks() + 1), NX, NX, REAL, FORCES_PARAMS, false, qpObjPrefix);
90 
91 
92  objGradients.clear();
93  objGradients.resize(getNumberOfBlocks() + 1);
94  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
95  {
96  objGradients[ i ].setup(string("f") + toString(i + 1), getNumBlockVariables(), 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
97  }
98  objGradients[ getNumberOfBlocks() ].setup(string("f") + toString(getNumberOfBlocks() + 1), NX, 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
99 
100 
101  conC.clear();
102  conC.resize( getNumberOfBlocks() );
103  // XXX FORCES works with column major format
104  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
105  conC[ i ].setup(string("C") + toString(i + 1), getNumBlockVariables(), NX, REAL, FORCES_PARAMS, false, qpObjPrefix);
106 
107  cond.clear();
108  unsigned dNum = initialStateFixed() == true ? getNumberOfBlocks() + 1 : getNumberOfBlocks();
109  cond.resize(dNum);
110  for (unsigned i = 0; i < dNum; ++i)
111  cond[ i ].setup(string("d") + toString(i + 1), NX, 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
112 
113 
114  // TODO: SET HESSIAN AND GRADIENT INFORMATION
115  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
118 
119  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
122 
123 
124  // TODO: SET EQUALITY CONSTRAINT VALUES
125  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
126  condensePrep.addStatement(conC[ i ] == qpC.getRows(i*NX,(i+1)*NX).getTranspose());
128 
129  if( initialStateFixed() ) {
130  for (unsigned i = 1; i < dNum; ++i)
131  condensePrep.addStatement(cond[ i ] == zeros<double>(NX,1) - qpc.getRows((i-1)*NX,i*NX)); // TODO:CHECK THE SIGN
132  }
133  else {
134  for (unsigned i = 0; i < dNum; ++i)
135  condensePrep.addStatement(cond[ i ] == zeros<double>(NX,1) - qpc.getRows(i*NX,(i+1)*NX)); // TODO:CHECK THE SIGN
136  }
138 
139 
140  // TODO: REWRITE EXPAND ROUTINE
141  unsigned offset = performFullCondensing() == true ? 0 : NX;
142  LOG( LVL_DEBUG ) << "Setup condensing: rewrite expand routine" << endl;
143  ExportVariable stageOut("stageOut", getNumBlockVariables(), 1, REAL, ACADO_LOCAL);
144  expand = ExportFunction( "expand", stageOut, blockI );
145 
146  for (unsigned i = 0; i < getBlockSize(); ++i ) {
147  expand.addStatement( (u.getRow(blockI*getBlockSize()+i)).getTranspose() += stageOut.getRows(NX+i*NU, NX+(i+1)*NU) );
148  }
149 
150  expand.addStatement( sbar.getRows(0, NX) == stageOut.getRows(0, NX) );
151  expand.addStatement( (x.getRow(blockI*getBlockSize())).getTranspose() += sbar.getRows(0, NX) );
152 
153  if( getBlockSize() > 1 ) {
154  expand.addStatement( sbar.getRows(NX, getBlockSize()*NX) == d.getRows(blockI*getBlockSize()*NX,(blockI+1)*getBlockSize()*NX-NX) );
155  }
156 
157  for (unsigned row = 0; row < getBlockSize()-1; ++row ) {
159  expansionStep, evGx.getAddress((blockI*getBlockSize()+row) * NX), evGu.getAddress((blockI*getBlockSize()+row) * NX),
160  stageOut.getAddress(offset + row * NU), sbar.getAddress(row * NX),
161  sbar.getAddress((row + 1) * NX)
162  );
163  expand.addStatement( (x.getRow(blockI*getBlockSize()+row+1)).getTranspose() += sbar.getRows((row+1)*NX, (row+2)*NX) );
164  }
165 
166  // !! TODO: Calculation of multipliers: !!
167 
168 
169  return SUCCESSFUL_RETURN;
170 }
171 
173 {
175  if( status != SUCCESSFUL_RETURN ) return status;
176 
178  //
179  // Setup evaluation of box constraints on states and controls
180  //
182 
183  conLB.clear();
184  conLB.resize(getNumberOfBlocks() + 1);
185 
186  conUB.clear();
187  conUB.resize(getNumberOfBlocks() + 1);
188 
189  conA.clear();
190  conA.resize(getNumberOfBlocks());
191 
192  conAB.clear();
193  conAB.resize(getNumberOfBlocks());
194 
195  conLBIndices.clear();
196  conLBIndices.resize(getNumberOfBlocks() + 1);
197 
198  conUBIndices.clear();
199  conUBIndices.resize(getNumberOfBlocks() + 1);
200 
201  conABDimensions.clear();
202  conABDimensions.resize(getNumberOfBlocks() + 1);
203 
204  DVector lbTmp, ubTmp;
205 
206  //
207  // Stack state constraints
208  //
209  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
210  {
211  for (unsigned k = 0; k < getBlockSize(); ++k)
212  {
213  lbTmp = xBounds.getLowerBounds( i*getBlockSize()+k );
214  ubTmp = xBounds.getUpperBounds( i*getBlockSize()+k );
215 
216  if (isFinite( lbTmp ) == false && isFinite( ubTmp ) == false)
217  continue;
218 
219  for (unsigned j = 0; j < lbTmp.getDim(); ++j)
220  {
221  if (acadoIsFinite( lbTmp( j ) ) == true)
222  {
223  if( k == 0 ) {
224  conLBIndices[ i ].push_back( j );
225  }
226  }
227 
228  if (acadoIsFinite( ubTmp( j ) ) == true)
229  {
230  if( k == 0 ) {
231  conUBIndices[ i ].push_back( j );
232  }
233  }
234  }
235  }
237  }
239 
240  lbTmp = xBounds.getLowerBounds( N );
241  ubTmp = xBounds.getUpperBounds( N );
242  for (unsigned j = 0; j < lbTmp.getDim(); ++j)
243  {
244  if (acadoIsFinite( lbTmp( j ) ) == true)
245  {
246  conLBIndices[ getNumberOfBlocks() ].push_back( j );
247  }
248 
249  if (acadoIsFinite( ubTmp( j ) ) == true)
250  {
251  conUBIndices[ getNumberOfBlocks() ].push_back( j );
252  }
253  }
254 
255  //
256  // Stack control constraints
257  //
258  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
259  {
260  for (unsigned k = 0; k < getBlockSize(); ++k)
261  {
262  lbTmp = uBounds.getLowerBounds( i*getBlockSize()+k );
263  ubTmp = uBounds.getUpperBounds( i*getBlockSize()+k );
264 
265  if (isFinite( lbTmp ) == false && isFinite( ubTmp ) == false)
266  continue;
267 
268  for (unsigned j = 0; j < lbTmp.getDim(); ++j)
269  {
270  if (acadoIsFinite( lbTmp( j ) ) == true)
271  {
272  conLBIndices[ i ].push_back(NX + k*NU + j);
273  }
274 
275  if (acadoIsFinite( ubTmp( j ) ) == true)
276  {
277  conUBIndices[ i ].push_back(NX + k*NU + j);
278  }
279  }
280  }
281  }
282 
283  //
284  // Setup variables
285  //
286  for (unsigned i = 0; i < getNumberOfBlocks() + 1; ++i)
287  {
288  conLB[ i ].setup(string("lb") + toString(i + 1), conLBIndices[ i ].size(), 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
289  conUB[ i ].setup(string("ub") + toString(i + 1), conUBIndices[ i ].size(), 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
290  }
291 
292  //
293  // FORCES evaluation of simple box constraints
294  //
295  for (unsigned i = 0; i < getNumberOfBlocks() + 1; ++i) {
296  for (unsigned j = 0; j < conLBIndices[ i ].size(); ++j)
297  {
298  evaluateConstraints << conLB[ i ].getFullName() << "[ " << toString(j) << " ]" << " = " << lb.get( i*getNumBlockVariables()+conLBIndices[ i ][ j ],0 ) << ";\n";
299  }
300  }
302 
303  for (unsigned i = 0; i < getNumberOfBlocks() + 1; ++i)
304  for (unsigned j = 0; j < conUBIndices[ i ].size(); ++j)
305  {
306  evaluateConstraints << conUB[ i ].getFullName() << "[ " << toString(j) << " ]" << " = " << ub.get( i*getNumBlockVariables()+conUBIndices[ i ][ j ],0 ) << ";\n";
307  }
309 
310  //
311  // Setup variables
312  //
313  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
314  {
315  conA[ i ].setup(string("A") + toString(i + 1), getNumBlockVariables(), conABDimensions[ i ], REAL, FORCES_PARAMS, false, qpObjPrefix); // XXX FORCES works with column major format
316  conAB[ i ].setup(string("Ab") + toString(i + 1), conABDimensions[ i ], 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
317  }
318 
319 
320  evaluateAffineConstraints.setup("evaluateAffineConstraints");
321  //
322  // FORCES evaluation for the affine constraints after condensing
323  //
324  for (unsigned i = 0; i < getNumberOfBlocks(); ++i) {
327 
330  }
331 
332  return SUCCESSFUL_RETURN;
333 }
334 
336 {
338  if( status != SUCCESSFUL_RETURN ) return status;
339 
340  xVars.setup("x", getNumQPvars(), 1, REAL, ACADO_LOCAL); // NOT USED
341  yVars.setup("",0,0); // NOT USED
342 
343  return SUCCESSFUL_RETURN;
344 }
345 
347 {
349  //
350  // Preparation phase
351  //
353 
354  preparation.setup( "preparationStep" );
355  preparation.doc( "Preparation step of the RTI scheme." );
356  ExportVariable retSim("ret", 1, 1, INT, ACADO_LOCAL, true);
357  retSim.setDoc("Status of the integration module. =0: OK, otherwise the error code.");
358  preparation.setReturnValue(retSim, false);
359  ExportIndex index("index");
360  preparation.addIndex( index );
361 
362  preparation << retSim.getFullName() << " = " << modelSimulation.getName() << "();\n";
363 
367 
369  preparation << (Dy -= y) << (DyN -= yN);
371 
372  ExportForLoop condensePrepLoop( index, 0, getNumberOfBlocks() );
373  condensePrepLoop.addFunctionCall( condensePrep, index );
374  preparation.addStatement( condensePrepLoop );
375 
377 
379  DMatrix mReg = eye<double>( getNX() );
380  mReg *= levenbergMarquardt;
383 
385  int variableObjS;
386  get(CG_USE_VARIABLE_WEIGHTING_MATRIX, variableObjS);
387  ExportVariable SlxCall =
388  objSlx.isGiven() == true || variableObjS == false ? objSlx : objSlx.getRows(N * NX, (N + 1) * NX);
391 
393  //
394  // Feedback phase
395  //
397 
398  ExportVariable tmp("tmp", 1, 1, INT, ACADO_LOCAL, true);
399  tmp.setDoc( "Status code of the qpOASES QP solver." );
400 
401  feedback.setup("feedbackStep");
402  feedback.doc( "Feedback/estimation step of the RTI scheme." );
403  feedback.setReturnValue( tmp );
404  feedback.addIndex( index );
405 
406  if (initialStateFixed() == true)
407  {
408  feedback.addStatement( cond[ 0 ] == x0 - x.getRow( 0 ).getTranspose() );
409  }
411 
412  //
413  // Configure output variables
414  //
415  std::vector< ExportVariable > vecQPVars;
416 
417  vecQPVars.clear();
418  vecQPVars.resize(getNumberOfBlocks() + 1);
419  for (unsigned i = 0; i < getNumberOfBlocks(); ++i)
420  vecQPVars[ i ].setup(string("out") + toString(i + 1), getNumBlockVariables(), 1, REAL, FORCES_OUTPUT, false, qpObjPrefix);
421  vecQPVars[ getNumberOfBlocks() ].setup(string("out") + toString(getNumberOfBlocks() + 1), NX, 1, REAL, FORCES_OUTPUT, false, qpObjPrefix);
422 
423  //
424  // In case warm starting is enabled, give an initial guess, based on the old solution
425  //
426  int hotstartQP;
427  get(HOTSTART_QP, hotstartQP);
428 
429  if ( hotstartQP )
430  {
432  }
433 
434  //
435  // Call a QP solver
436  // NOTE: we need two prefixes:
437  // 1. module prefix
438  // 2. structure instance prefix
439  //
440  ExportFunction solveQP;
441  solveQP.setup("solve");
442  solveQP.setName( "solve" );
443 
444  feedback
445  << tmp.getFullName() << " = "
446  << qpModuleName << "_" << solveQP.getName() << "( "
447  << "&" << qpObjPrefix << "_" << "params" << ", "
448  << "&" << qpObjPrefix << "_" << "output" << ", "
449  << "&" << qpObjPrefix << "_" << "info" << " , NULL);\n";
451 
452  for (unsigned i = 0; i < getNumberOfBlocks(); ++i) {
453  feedback.addFunctionCall( expand, vecQPVars[i], ExportIndex(i) );
454  }
455 
456  feedback.addStatement( x.getRow( N ) += vecQPVars[ getNumberOfBlocks() ].getTranspose() );
458 
460  //
461  // Setup evaluation of the KKT tolerance
462  //
464 
465  ExportVariable kkt("kkt", 1, 1, REAL, ACADO_LOCAL, true);
466 
467  getKKT.setup( "getKKT" );
468  getKKT.doc( "Get the KKT tolerance of the current iterate. Under development." );
469  // kkt.setDoc( "The KKT tolerance value." );
470  kkt.setDoc( "1e-15." );
471  getKKT.setReturnValue( kkt );
472 
473  getKKT.addStatement( kkt == 1e-15 );
474 
475  return SUCCESSFUL_RETURN;
476 }
477 
479 {
480  //
481  // Configure and export QP interface
482  //
483 
484  qpInterface = std::shared_ptr< ExportForcesInterface >(new ExportForcesInterface(FORCES_TEMPLATE, "", commonHeaderName));
485 
486  ExportVariable tmp1("tmp", 1, 1, REAL, FORCES_PARAMS, false, qpObjPrefix);
487  ExportVariable tmp2("tmp", 1, 1, REAL, FORCES_OUTPUT, false, qpObjPrefix);
488  ExportVariable tmp3("tmp", 1, 1, REAL, FORCES_INFO, false, qpObjPrefix);
489 
490  string params = qpModuleName + "_params";
491 
492  string output = qpModuleName + "_output";
493 
494  string info = qpModuleName + "_info";
495 
496  string header = qpModuleName + ".h";
497 
498  qpInterface->configure(
499  header,
500 
501  params,
502  tmp1.getDataStructString(),
503 
504  output,
505  tmp2.getDataStructString(),
506 
507  info,
508  tmp3.getDataStructString()
509  );
510 
511  //
512  // Configure and export MATLAB QP generator
513  //
514 
515  string folderName;
516  get(CG_EXPORT_FOLDER_NAME, folderName);
517  string outFile = folderName + "/acado_forces_generator.m";
518 
519  qpGenerator = std::shared_ptr< ExportForcesGenerator >(new ExportForcesGenerator(FORCES_GENERATOR, outFile, "", "real_t", "int", 16, "%"));
520 
521  int maxNumQPiterations;
522  get( MAX_NUM_QP_ITERATIONS,maxNumQPiterations );
523 
524  int printLevel;
525  get(PRINTLEVEL, printLevel);
526 
527  // if not specified, use default value
528  if ( maxNumQPiterations <= 0 )
529  maxNumQPiterations = 3 * getNumQPvars();
530 
531  int useOMP;
532  get(CG_USE_OPENMP, useOMP);
533 
534  int hotstartQP;
535  get(HOTSTART_QP, hotstartQP);
536 
537  qpGenerator->configure(
538  NX,
539  getBlockSize()*NU,
541  conLBIndices,
542  conUBIndices,
544  (Q1.isGiven() == true && R1.isGiven() == true) ? 1 : 0,
545  diagonalH,
546  diagonalHN,
548  qpModuleName,
549  (PrintLevel)printLevel == HIGH ? 2 : 0,
550  maxNumQPiterations,
551  useOMP,
552  true,
553  hotstartQP
554  );
555 
556  qpGenerator->exportCode();
557 
558  //
559  // Export Python generator
560  //
561 // ACADOWARNINGTEXT(RET_NOT_IMPLEMENTED_YET,
562 // "A python code generator interface for block condensing with FORCES is under development.");
563 
564  return SUCCESSFUL_RETURN;
565 }
566 
567 
#define LOG(level)
Just define a handy macro for getting the logger.
Lowest level, the debug level.
virtual unsigned getNumStateBoundsPerBlock() const
ExportVariable getRow(const ExportIndex &idx) const
ExportFunction & setName(const std::string &_name)
ExportVariable getTranspose() const
bool initialStateFixed() const
VariablesGrid xBounds
ExportVariable & setup(const std::string &_name, uint _nRows=1, uint _nCols=1, ExportType _type=REAL, ExportStruct _dataStruct=ACADO_LOCAL, bool _callItByValue=false, const std::string &_prefix=std::string())
bool isGiven(const ExportIndex &rowIdx, const ExportIndex &colIdx) const
std::vector< std::vector< unsigned > > conLBIndices
uint getNX() const
Allows to pass back messages to the calling function.
Generator of the FORCES interface, the to, be called from MATLAB.
DVector getUpperBounds(uint pointIdx) const
Allows to export code of a for-loop.
string toString(T const &value)
VariablesGrid uBounds
ExportVariable objSlx
#define CLOSE_NAMESPACE_ACADO
std::vector< std::vector< unsigned > > conUBIndices
ExportVariable DyN
ExportVariable evGx
Defines a scalar-valued index variable to be used for exporting code.
An OCP solver based on the block N^2 condensing algorithm.
virtual returnValue setupConstraintsEvaluation(void)
ExportFunction & setup(const std::string &_name="defaultFunctionName", const ExportArgument &_argument1=emptyConstExportArgument, const ExportArgument &_argument2=emptyConstExportArgument, const ExportArgument &_argument3=emptyConstExportArgument, const ExportArgument &_argument4=emptyConstExportArgument, const ExportArgument &_argument5=emptyConstExportArgument, const ExportArgument &_argument6=emptyConstExportArgument, const ExportArgument &_argument7=emptyConstExportArgument, const ExportArgument &_argument8=emptyConstExportArgument, const ExportArgument &_argument9=emptyConstExportArgument)
virtual returnValue setDoc(const std::string &_doc)
std::string commonHeaderName
ExportFunction modelSimulation
virtual ExportFunction & doc(const std::string &_doc)
virtual bool isDefined() const
virtual returnValue setupConstraintsEvaluation(void)
printLevel
Definition: example1.py:55
unsigned getDim() const
Definition: vector.hpp:172
A class for configuration and export for interface to the FORCES QP solver.
const std::string get(const ExportIndex &rowIdx, const ExportIndex &colIdx) const
ExportVariable QN2
ExportVariable QN1
Encapsulates all user interaction for setting options, logging data and plotting results.
Allows to export code of an arbitrary function.
ExportGaussNewtonBlockForces(UserInteraction *_userInteraction=0, const std::string &_commonHeaderName="")
returnValue addStatement(const ExportStatement &_statement)
PrintLevel
std::string getFullName() const
returnValue addLinebreak(uint num=1)
ExportFunction & setReturnValue(const ExportVariable &_functionReturnValue, bool _returnAsPointer=false)
RowXpr row(Index i)
Definition: BlockMethods.h:725
BooleanType acadoIsFinite(double x, double TOL)
ExportVariable evGu
virtual returnValue getCode(ExportStatementBlock &code)
std::string getName() const
ExportVariable getRows(const ExportIndex &idx1, const ExportIndex &idx2) const
std::shared_ptr< ExportForcesInterface > qpInterface
DVector getLowerBounds(uint pointIdx) const
#define BEGIN_NAMESPACE_ACADO
BooleanType isFinite(const T &_value)
USING_NAMESPACE_ACADO void output(const char *name, const Expression &e)
returnValue addFunction(const ExportFunction &_function)
virtual returnValue getCode(ExportStatementBlock &code)
Allows to export code for a block of statements.
ExportFunction initialize
std::shared_ptr< ExportForcesGenerator > qpGenerator
ExportArgument getAddress(const ExportIndex &_rowIdx, const ExportIndex &_colIdx=emptyConstExportIndex) const
ExportFunction & addIndex(const ExportIndex &_index)
ExportFunction regularizeHessian
std::string getDataStructString() const
#define ACADOERROR(retval)
Defines a matrix-valued variable to be used for exporting code.
returnValue addFunctionCall(const std::string &_fName, const ExportArgument &_argument1=emptyConstExportArgument, const ExportArgument &_argument2=emptyConstExportArgument, const ExportArgument &_argument3=emptyConstExportArgument, const ExportArgument &_argument4=emptyConstExportArgument, const ExportArgument &_argument5=emptyConstExportArgument, const ExportArgument &_argument6=emptyConstExportArgument, const ExportArgument &_argument7=emptyConstExportArgument, const ExportArgument &_argument8=emptyConstExportArgument, const ExportArgument &_argument9=emptyConstExportArgument)


acado
Author(s): Milan Vukov, Rien Quirynen
autogenerated on Mon Jun 10 2019 12:34:33