politopix  4.1.0
 All Classes Files Functions Variables Typedefs Enumerations Enumerator Friends Macros
main.cpp
Go to the documentation of this file.
1 // politopix allows to make computations on polytopes such as finding vertices, intersecting, Minkowski sums, ...
2 // Copyright (C) 2011-2015 : Delos Vincent
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Lesser General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU Lesser General Public License for more details.
13 //
14 // You should have received a copy of the GNU Lesser General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
16 //
19 // I2M (UMR CNRS 5295 / University of Bordeaux)
20 
21 #include <boost/shared_ptr.hpp>
22 #include <boost/timer.hpp>
23 #include <iostream>
24 #include <string.h>
25 #include <cmath>
27 #include "Config.h"
28 
29 
31 class RunOptions {
32 
33  public:
34  RunOptions() {}
35 
37  static void setCheckGeneratorsTest(bool b) {_checkGeneratorsTest = b;}
38 
40  static bool getCheckGeneratorsTest() {return _checkGeneratorsTest;}
41 
43  static void setCheckGeneratorsValue(unsigned int d) {_checkGeneratorsValue = d;}
44 
46  static unsigned int getCheckGeneratorsValue() {return _checkGeneratorsValue;}
47 
48  protected:
49  static bool _checkGeneratorsTest;
50  static unsigned int _checkGeneratorsValue;
51 
52 };
53 
54 
55 bool RunOptions::_checkGeneratorsTest = false;
56 unsigned int RunOptions::_checkGeneratorsValue = 0;
57 
58 
59 int main(int argc, char* argv[]) {
60 
61  unsigned int dimension = 0;
62  double tolerance= 0.;
64  std::string fileName1,fileName2;
65  bool boundingBox = true;
66  bool boundingVolume = false;
67  double boundingSize = 1000;
68  double volumeOfPolytope = -1.;
69  unsigned int facetsToCheck = 0, generatorsToCheck = 0;
70  int vertexToComputeDistances=-1, facetToComputeDistances=-1;
71  bool computeDistances=false;
72  bool p1=false, p2=false, c1=false, c2=false;
73  bool COMPUTE_VOL=false, SUBSET=false;
74  bool INTER=false, CHCK_EQ=false, MS=false, check_all=false, output=false;
75  std::string outputFileName;
76 
77  std::ostringstream stream_;
78  stream_ << "Version ";
79  stream_ << politopix_VERSION_MAJOR;
80  stream_ << ".";
81  stream_ << politopix_VERSION_MINOR;
82  stream_ << ".";
83  stream_ << politopix_VERSION_PATCH;
84  std::string version = stream_.str();
85 
86  // Parse the main arguments
87  for(int i = 1; i < argc; ++i) {
88  if (strcmp(argv[i], "-p1") == 0 || strcmp(argv[i], "--polytope1") == 0) {
89  if (i+1 == argc) {
90  cerr << "Invalid file " << argv[i] << std::endl;
91  return EXIT_FAILURE;
92  }
93  // Get file
94  char* fileName_1 = argv[++i];
95  std::string file1(fileName_1);
96  fileName1 = file1;
97  p1 = true;
98  }
99  else if (strcmp(argv[i], "-p2") == 0 || strcmp(argv[i], "--polytope2") == 0) {
100  if (i+1 == argc) {
101  cerr << "Invalid file " << argv[i] << std::endl;
102  return EXIT_FAILURE;
103  }
104  char* fileName_2 = argv[++i];
105  std::string file2(fileName_2);
106  fileName2 = file2;
107  p2 = true;
108  }
109  else if (strcmp(argv[i], "-o") == 0 || strcmp(argv[i], "--output") == 0) {
110  if (i+1 == argc) {
111  cerr << "Invalid file " << argv[i] << std::endl;
112  return EXIT_FAILURE;
113  }
114  char* fileName_3 = argv[++i];
115  std::string file3(fileName_3);
116  outputFileName = file3;
117  output = true;
118  }
119  else if (strcmp(argv[i], "-c1") == 0 || strcmp(argv[i], "--polyhedralcone1") == 0) {
120  if (i+1 == argc) {
121  cerr << "Invalid file " << argv[i] << std::endl;
122  return EXIT_FAILURE;
123  }
124  // Get file
125  char* fileName_1 = argv[++i];
126  std::string file1(fileName_1);
127  fileName1 = file1;
128  c1 = true;
129  }
130  else if (strcmp(argv[i], "-c2") == 0 || strcmp(argv[i], "--polyhedralcone2") == 0) {
131  if (i+1 == argc) {
132  cerr << "Invalid file " << argv[i] << std::endl;
133  return EXIT_FAILURE;
134  }
135  char* fileName_2 = argv[++i];
136  std::string file2(fileName_2);
137  fileName2 = file2;
138  c2 = true;
139  }
140  else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
141  cout << version << std::endl;
142  return EXIT_SUCCESS;
143  }
144  else if (strcmp(argv[i], "-ch") == 0 || strcmp(argv[i], "--check-all") == 0) {
145  // We perform all checks, it can be slow.
146  check_all = true;
147  }
148  else if (strcmp(argv[i], "-MS") == 0 || strcmp(argv[i], "--MinkowskiSum") == 0) {
149  // We compute Minkowski sums.
150  MS = true;
151  INTER = false;
152  CHCK_EQ = false;
153  }
154  else if (strcmp(argv[i], "-IN") == 0 || strcmp(argv[i], "--Intersection") == 0) {
155  // We intersect polytopes or polyhedral cones, this is the default behaviour.
156  MS = false;
157  INTER = true;
158  CHCK_EQ = false;
159  }
160  else if (strcmp(argv[i], "-SS") == 0 || strcmp(argv[i], "--Subset") == 0) {
161  // We intersect polytopes or polyhedral cones, this is the default behaviour.
162  SUBSET = true;
163  MS = false;
164  INTER = false;
165  CHCK_EQ = false;
166  }
167  else if (strcmp(argv[i], "-EQ") == 0 || strcmp(argv[i], "--Equality") == 0) {
168  // We intersect polytopes or polyhedral cones, this is the default behaviour.
169  MS = false;
170  INTER = false;
171  CHCK_EQ = true;
172  }
173  else if (strcmp(argv[i], "-bb") == 0 || strcmp(argv[i], "--boundingbox") == 0) {
174  if (i+1 == argc) {
175  cerr << "Invalid bounding box dimension " << argv[i] << std::endl;
176  return EXIT_FAILURE;
177  }
178  INTER = true;
179  boundingBox = true;
180  boundingVolume = true;
181  boundingSize = atof(argv[++i]);
182  }
183  else if (strcmp(argv[i], "-bs") == 0 || strcmp(argv[i], "--boundingsimplex") == 0) {
184  if (i+1 == argc) {
185  cerr << "Invalid bounding simplex dimension " << argv[i] << std::endl;
186  return EXIT_FAILURE;
187  }
188  // Here mark the fact we choose a simplex and not a cube.
189  INTER = true;
190  boundingBox = false;
191  boundingVolume = true;
192  boundingSize = atof(argv[++i]);
193  }
194  else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--dimension") == 0) {
195  if (i+1 == argc) {
196  cerr << "Invalid cartesian space dimension " << argv[i] << std::endl;
197  return EXIT_FAILURE;
198  }
199  dimension = atoi(argv[++i]);
200  Rn::setDimension(dimension);
201  }
202  else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--tolerance") == 0) {
203  if (i+1 == argc) {
204  cerr << "Invalid cartesian space tolerance " << argv[i] << std::endl;
205  return EXIT_FAILURE;
206  }
207  tolerance = atof(argv[++i]);
208  Rn::setTolerance(tolerance);
209  }
210  else if (strcmp(argv[i], "-VO") == 0 || strcmp(argv[i], "--Volume") == 0) {
211  COMPUTE_VOL = true;
212  INTER = false;
213  }
214  else if (strcmp(argv[i], "-cg") == 0 || strcmp(argv[i], "--check-generators") == 0) {
215  if (i+1 == argc) {
216  cerr << "Invalid number of generators to check " << argv[i] << std::endl;
217  return EXIT_FAILURE;
218  }
219  generatorsToCheck = atoi(argv[++i]);
220  RunOptions::setCheckGeneratorsTest(true);
221  RunOptions::setCheckGeneratorsValue(generatorsToCheck);
222  }
223  else if (strcmp(argv[i], "-cf") == 0 || strcmp(argv[i], "--check-facets") == 0) {
224  if (i+1 == argc) {
225  cerr << "Invalid number of facets to check " << argv[i] << std::endl;
226  return EXIT_FAILURE;
227  }
228  facetsToCheck = atoi(argv[++i]);
229  RunOptions::setCheckGeneratorsTest(true);
230  RunOptions::setCheckGeneratorsValue(generatorsToCheck);
231  }
232  else if (strcmp(argv[i], "--generator") == 0) {
233  if (i+1 == argc) {
234  cerr << "Invalid number of generator " << argv[i] << std::endl;
235  return EXIT_FAILURE;
236  }
237  vertexToComputeDistances = atoi(argv[++i]);
238  computeDistances = true;
239  MS = false;
240  INTER = false;
241  CHCK_EQ = false;
242  }
243  else if (strcmp(argv[i], "--facet") == 0) {
244  if (i+1 == argc) {
245  cerr << "Invalid number of facet " << argv[i] << std::endl;
246  return EXIT_FAILURE;
247  }
248  facetToComputeDistances = atoi(argv[++i]);
249  computeDistances = true;
250  MS = false;
251  INTER = false;
252  CHCK_EQ = false;
253  }
254  else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
255  cout << version << std::endl;
256  cout << "\t-d [--dimension] ARG\t:\tSet the cartesian space dimension" << std::endl;
257  cout << "\t-t [--tolerance] ARG\t:\tSet the cartesian space tolerance [default: 1.e-06]" << std::endl;
258  cout << "\t-o [--output] ARG\t:\tThe optional output file (ptop or pcon extension)" << std::endl;
259  cout << "\t-p1 [--polytope1] ARG\t:\tFirst polytope input file (ptop extension)" << std::endl;
260  cout << "\t-p2 [--polytope2] ARG\t:\tSecond polytope input file (ptop extension)" << std::endl;
261  cout << "\t-c1 [--polyhedralcone1] ARG\t:\tFirst polyhedral cone input file (pcon extension)" << std::endl;
262  cout << "\t-c2 [--polyhedralcone2] ARG\t:\tSecond polyhedral cone input file (pcon extension)" << std::endl;
263  cout << "\t-cf [--check-facets] ARG\t:\tUsed to test when we know the final number of facets" << std::endl;
264  cout << "\t-cg [--check-generators] ARG\t:\tUsed to test when we know the final number of generators" << std::endl;
265  cout << "\t-bs [--boundingsimplex] ARG\t:\tBounding simplex size, containing the bounding box -bb (n+1 vertices)" << std::endl;
266  cout << "\t-bb [--boundingbox] ARG\t:\tBounding box size centered on the origin including the polytope (2^n vertices)" << std::endl;
267  cout << "\t-ch [--check-all]\t\t:\tUsed to perform all tests (no arguments, can be slow)." << std::endl;
268  cout << "\t-MS [--MinkowskiSum] \t:\tSet the option to turn on Minkowski sums" << std::endl;
269  cout << "\t-IN [--Intersection] \t:\tSet the option to turn on intersections (default option)" << std::endl;
270  cout << "\t-SS [--Subset] \t:\tSet the option to test whether P1 is included in P2" << std::endl;
271  cout << "\t-VO [--Volume] \t:\tSet the option to turn on the volume computation for the polytope" << std::endl;
272  cout << "\t-EQ [--Equality] \t:\tSet the option to turn on the equality check between ptop or pcon" << std::endl;
273  cout << "\t-v [--version]\t\t\t:\tGive the current version" << std::endl;
274  return 0;
275  }
276  else {
277  cerr << "Unknown option " << argv[i] << std::endl;
278  return EXIT_FAILURE;
279  }
280  }
281 
282  if (Rn::getDimension() == 0) {
283  cerr << "Dimension has not been set." << std::endl;
284  return EXIT_FAILURE;
285  }
286 
287  // Here we deal with the fact that the intersection is the default operation.
288  // So if we have two operands and we don't perform neither a sum nor an equality/inclusion check then it's an intersection
289  if (c2 == true || p2 == true)
290  if (MS == false && CHCK_EQ == false && SUBSET == false)
291  INTER = true;
292 
293  int truncationStep = 0;
294  boost::shared_ptr<PolyhedralCone_Rn> A;
295  boost::shared_ptr<PolyhedralCone_Rn> B;
296  boost::shared_ptr<PolyhedralCone_Rn> C;
297  try {
298  if (p1==true) {
299  A.reset(new Polytope_Rn());
300  IO_Polytope::load(fileName1, A);
301  // Is a bounding volume provided ?
302  if (boundingVolume == true) {
303  if (A->numberOfGenerators() == 0) {
304  // By default the bounding volume is a cube (2n facets), if not a tetrahedron (n+1 facets).
305  if (boundingBox == true) {
306  truncationStep = 2*Rn::getDimension();
307  A->createBoundingBox(boundingSize);
308  }
309  else {
310  truncationStep = Rn::getDimension()+1;
311  A->createBoundingSimplex(boundingSize);
312  }
313  }
314  else {
315  boost::shared_ptr<Polytope_Rn> PA = boost::static_pointer_cast<Polytope_Rn>(A);
316  DoubleDescriptionFromGenerators::Compute( PA, boundingSize);
317  if (check_all == true)
318  PA->checkTopologyAndGeometry();
319  std::string FileOut("output");
320  FileOut += fileName1;
321  if (output == true)
322  FileOut = outputFileName;
323  IO_Polytope::save(FileOut, PA);
324 
325  return EXIT_SUCCESS;
326  }
327  }
328  }
329  else if (c1==true) {
330  A.reset(new PolyhedralCone_Rn());
331  IO_Polytope::load(fileName1, A);
332  }
333  if (p2==true) {
334  B.reset(new Polytope_Rn());
335  IO_Polytope::load(fileName2, B);
336  }
337  else if (c2==true) {
338  B.reset(new PolyhedralCone_Rn());
339  IO_Polytope::load(fileName2, B);
340  }
341  }
342  catch(std::ios_base::failure& e) {
343  cerr << "In/out exception: " << e.what() << std::endl;
344  return EXIT_FAILURE;
345  }
346 
347  if ((p1==true && p2==true) || (c1==true && c2==true)) {
348  if (p1==true && p2==true) {
349  C.reset(new Polytope_Rn());
350  }
351  else {
352  C.reset(new PolyhedralCone_Rn());
353  }
354  {for (unsigned int i=0; i<A->numberOfGenerators(); i++) {
355  C->addGenerator(A->getGenerator(i));
356  }}
358  for (iteHSA.begin(); iteHSA.end()!=true; iteHSA.next()) {
359  C->addHalfSpace(iteHSA.current());
360  }
362  for (iteHSB.begin(); iteHSB.end()!=true; iteHSB.next()) {
363  C->addHalfSpace(iteHSB.current());
364  }
365  truncationStep = A->numberOfHalfSpaces();
366  }
367  else if ((p1==true && p2!=true) || (c1==true && c2!=true)) {
368  // There is no polytope or polyhedral cone other than A.
369  C = A;
370  }
371  else {
372  cerr << "This program needs at least one polytope or one polyhedral cone and no mix between them." << std::endl;
373  return EXIT_FAILURE;
374  }
375 
376  boost::timer this_timer;
377  try {
378  // Do we have two operands and a sum or an intersection ?
379  if ((MS == true) || (INTER == true) || (COMPUTE_VOL == true) || (SUBSET == true)) {
380  if (MS == true) {
381  // Minkowski sum calculation
382  if (p1!=true || p2!=true) {
383  cout << "ERROR 2 polytopes are needed to compute the minkowski sum." << std::endl;
384  return -1;
385  }
386  //NormalFan_Rn NFA(boost::static_pointer_cast<Polytope_Rn>(A));
387  //NFA.dump(cout);
388  //NormalFan_Rn NFB(boost::static_pointer_cast<Polytope_Rn>(B));
389  //NFB.dump(cout);
390  //NormalFan_Rn NFC;
391  //NFC.computeCommonRefinement(NFA,NFB);
392  //NFC.dump(cout);
393  C.reset(new Polytope_Rn());
394 
395  //cout << "VERTEX NUMBER = " << Polytope_Rn().computeMinkowskiVerticesNumber(A,B) << std::endl;
396 
397  boost::shared_ptr<Polytope_Rn> PA = boost::static_pointer_cast<Polytope_Rn>(A);
398  boost::shared_ptr<Polytope_Rn> PB = boost::static_pointer_cast<Polytope_Rn>(B);
399  boost::shared_ptr<Polytope_Rn> PC = boost::static_pointer_cast<Polytope_Rn>(C);
400  MinkowskiSum Ope(PA,PB,PC);
401  //return 0;
402  }
403  else if (COMPUTE_VOL == true) {
404  boost::shared_ptr<Polytope_Rn> Pol = boost::static_pointer_cast<Polytope_Rn>(C);
405  volumeOfPolytope = VolumeOfPolytopes_Rn::compute(Pol);
406  cout << "VolumeOfPolytope=" << volumeOfPolytope << std::endl;
407  return EXIT_SUCCESS;
408  }
409  else if (SUBSET == true) {
410  bool isInside = A->isIncluded(B);
411  std::string answer = (isInside == true) ? "" : "not";
412  cout << "P1 is " << answer << " inside of P2" << std::endl;
413  return EXIT_SUCCESS;
414  }
415  else if (INTER == false && MS == false && check_all == true && p2 == false && c2 == false) {
416  // Here we just check the input body, we don't perform neither intersection nor sum.
417  A->checkTopologyAndGeometry();
418  return EXIT_SUCCESS;
419  }
420  else {
421  //C->truncate(truncationStep);
422 
423  // Declare an iterator.
424  //lexmaxIteratorOfListOfHalfSpaces lexmin_ite(C);
425  //constIteratorOfListOfHalfSpaces lexmin_ite(C);
426  constIteratorOfListOfGeometricObjects< boost::shared_ptr<HalfSpace_Rn> > lexmin_ite(C->getListOfHalfSpaces());
427  //DoubleDescriptionWeakRedundancy< boost::shared_ptr<PolyhedralCone_Rn>, lexmaxIteratorOfListOfHalfSpaces > DD(C, lexmin_ite, truncationStep);
428  //NoRedundancyProcessing< boost::shared_ptr<PolyhedralCone_Rn> > NRP;
429  //WeakRedundancyProcessing< boost::shared_ptr<PolyhedralCone_Rn> > NRP;
432  boost::shared_ptr<PolyhedralCone_Rn>,
434  //constIteratorOfListOfHalfSpaces,
435  //NoRedundancyProcessing< boost::shared_ptr<PolyhedralCone_Rn> > >
436  //WeakRedundancyProcessing< boost::shared_ptr<PolyhedralCone_Rn> > >
438  DD(C, lexmin_ite, NRP, truncationStep);
439  }
440  cout.precision(10 - (int)log10(Rn::getTolerance()) );
441  cout << "TIME=" << this_timer.elapsed() << std::endl;
442 
443  if (C->numberOfGenerators()==0 && C->numberOfHalfSpaces()==0)
444  cout << "The result is empty." << std::endl;
445  else {
446  if (generatorsToCheck != 0) {
447  if (generatorsToCheck == C->numberOfGenerators())
448  cout << "Found " << generatorsToCheck << " generators..... OK" << std::endl;
449  else
450  cout << "Found " << C->numberOfGenerators() << " generators instead of " << generatorsToCheck << " KO !!!!!" << std::endl;
451  }
452  if (facetsToCheck != 0) {
453  if (facetsToCheck == C->numberOfHalfSpaces())
454  cout << "Found " << facetsToCheck << " facets ..... OK" << std::endl;
455  else
456  cout << "Found " << C->numberOfHalfSpaces() << " facets instead of " << facetsToCheck << " KO !!!!!" << std::endl;
457  }
458  std::string FileOut("output");
459  FileOut += fileName1;
460  if (output == true)
461  FileOut = outputFileName;
462  IO_Polytope::save(FileOut, C);
463  }
464  }
465  }
466  catch(std::invalid_argument& excep) {
467  cerr << "TIME=" << this_timer.elapsed() << std::endl;
468  cerr << "Invalid argument exception " << excep.what() << std::endl;
469  return EXIT_FAILURE;
470  }
471  catch(std::out_of_range& excep) {
472  cerr << "TIME=" << this_timer.elapsed() << std::endl;
473  cerr << "Out of range exception " << excep.what() << std::endl;
474  return EXIT_FAILURE;
475  }
476  catch(std::ios_base::failure& excep) {
477  cerr << "TIME=" << this_timer.elapsed() << std::endl;
478  cerr << "In/out exception " << excep.what() << std::endl;
479  return EXIT_FAILURE;
480  }
481  catch(std::domain_error& excep) {
482  cerr << "TIME=" << this_timer.elapsed() << std::endl;
483  cerr << "Domain error exception " << excep.what() << std::endl;
484  return EXIT_FAILURE;
485  }
486  catch(std::logic_error& excep) {
487  cerr << "TIME=" << this_timer.elapsed() << std::endl;
488  cerr << "Logic error exception " << excep.what() << std::endl;
489  return EXIT_FAILURE;
490  }
491  catch(...) {
492  cerr << "TIME=" << this_timer.elapsed() << std::endl;
493  cerr << "Unexpected exception caught !" << std::endl;
494  return EXIT_FAILURE;
495  }
496  //C->dump();
497  if (check_all == true)
498  C->checkTopologyAndGeometry();
499  else if (CHCK_EQ == true) {
500  if ((p1==true && p2==true) || (c1==true && c2==true))
501  A->checkEquality(B);
502  else {
503  cerr << "ERROR 2 polytopes or polyhedral cones are needed to check equality." << std::endl;
504  return EXIT_FAILURE;
505  }
506  }
507  else if (computeDistances == true) {
508  if (vertexToComputeDistances >= 0) {
509  C->checkGenerator(vertexToComputeDistances, std::cout);
510  }
511  else if (facetToComputeDistances >= 0) {
512  C->checkFacet(facetToComputeDistances, std::cout);
513  }
514  }
515 
516  return EXIT_SUCCESS;
517 }
static polito_EXPORT void setDimension(unsigned int dim)
Set the dimension for the cartesian space we work in.
Definition: Rn.cpp:27
#define politopix_VERSION_MINOR
Definition: Config.h:3
#define politopix_VERSION_MAJOR
Definition: Config.h:2
Model a polytope using its two equivalent definitions : the convex hull and the half-space intersecti...
Definition: Polytope_Rn.h:34
Model a polyhedral cone using its two equivalent definitions : the convex hull and the half-space int...
Compute the Minkowski sum of two polytopes.
static polito_EXPORT void load(const std::string &filename, boost::shared_ptr< PolyhedralCone_Rn > POLY)
Load the main data format to store polytopes.
Definition: IO_Polytope.cpp:26
static polito_EXPORT void save(const std::string &filename, boost::shared_ptr< PolyhedralCone_Rn > POLY)
Save the polytope to the main data format.
static polito_EXPORT void setTolerance(double t)
Give the minimum distance between two points.
Definition: Rn.cpp:33
static polito_EXPORT double getTolerance()
Give the minimum distance between two points.
Definition: Rn.cpp:31
static polito_EXPORT unsigned int getDimension()
Return the dimension of the cartesian space we work in.
Definition: Rn.cpp:29
static double compute(const boost::shared_ptr< Polytope_Rn > P)
Return the volume of the given polytope P.
This class can be more time-consuming than WeakRedundancyProcessing or NoRedundancyProcessing because...
This class is designed to run the list of all geometric objects representing a polytope.
#define politopix_VERSION_PATCH
Definition: Config.h:4
static int Compute(boost::shared_ptr< Polytope_Rn > &pol, double bb_size=1000.)
Use the polarity to get the facets from the generators.
int main(int argc, char *argv[])
Definition: main.cpp:59
The algorithm implemented here is an incremental algorithm as mentioned in How Good are Convex Hull ...