graphFunctionObject.C
Go to the documentation of this file.
1 /*---------------------------------------------------------------------------*\
2  ========= |
3  \ / F ield | OpenFOAM: The Open Source CFD Toolbox
4  \ / O peration |
5  \ / A nd | www.openfoam.com
6  \/ M anipulation |
7 -------------------------------------------------------------------------------
8  Copyright (C) 2024 OpenCFD Ltd.
9 -------------------------------------------------------------------------------
10 License
11  This file is part of OpenFOAM.
12 
13  OpenFOAM is free software: you can redistribute it and/or modify it
14  under the terms of the GNU General Public License as published by
15  the Free Software Foundation, either version 3 of the License, or
16  (at your option) any later version.
17 
18  OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
19  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21  for more details.
22 
23  You should have received a copy of the GNU General Public License
24  along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
25 
26 \*---------------------------------------------------------------------------*/
27 
28 #include "graphFunctionObject.H"
30 #include "OFstream.H"
31 #include "labelVector.H"
32 #include "FlatOutput.H"
33 #include "SVGTools.H"
34 
35 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
36 
37 namespace Foam
38 {
39 namespace functionObjects
40 {
41  defineTypeNameAndDebug(graphFunctionObject, 0);
43  (
44  functionObject,
45  graphFunctionObject,
46  dictionary
47  );
48 }
49 }
50 
51 // 'Muted' colour scheme from https://personal.sron.nl/~pault/ (12.07.24)
52 Foam::wordList Foam::functionObjects::graphFunctionObject::defaultColours
53 ({
54  "#CC6677",
55  "#332288",
56  "#DDCC77",
57  "#117733",
58  "#88CCEE",
59  "#882255",
60  "#44AA99",
61  "#999933",
62  "#AA4499"
63 });
64 
65 
66 // * * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * //
67 
68 template<class Type>
69 bool Foam::functionObjects::graphFunctionObject::getValue
70 (
71  const label objecti,
72  label& valuei
73 )
74 {
75  const word& object = objects_[objecti];
76  const word& entry = entries_[objecti];
77 
78  Type result;
79  if (!this->getObjectResult(object, entry, result))
80  {
81  return false;
82  }
83 
84  auto& cols = objectToCol_[objecti];
85  if (cols.empty())
86  {
88  {
89  cols.push_back(valuei++);
90  values_.push_back(DynamicList<scalar>());
91  }
92  }
93 
95  {
96  scalar v = component(result, d);
97 
98  if (logScaleY_)
99  {
100  v = (v < SMALL) ? 1 : log10(v);
101  }
102 
103  values_[cols[d]].push_back(v);
104  }
105 
106  return true;
107 }
108 
109 
110 Foam::label Foam::functionObjects::graphFunctionObject::setAxisProps
111 (
112  const bool logScale,
113  scalar& xmin,
114  scalar& xmax,
115  scalar& xtick
116 ) const
117 {
118  DebugInfo
119  << "1 -- xmin:" << xmin << " xmax:" << xmax
120  << " xtick:" << xtick << endl;
121 
122  /*
123  Divisions Based on (12.07.24):
124  https://peltiertech.com/calculate-nice-axis-scales-in-your-excel-worksheet
125  */
126 
127  const scalar range = xmax - xmin;
128  const scalar eps = 0.01*range;
129 
130  // Extend xmin and xmax by eps
131  if (mag(xmin) < SMALL)
132  {
133  xmin = 0;
134  }
135  else
136  {
137  xmin = (xmin > 0) ? max(0, xmin - eps) : xmin - eps;
138  }
139 
140  if (mag(xmax) < SMALL)
141  {
142  xmax = mag(xmin) < SMALL ? 1 : 0;
143  }
144  else
145  {
146  xmax = (xmax < 0) ? min(0, xmax + eps) : xmax + eps;
147  }
148 
149  DebugInfo
150  << "2 -- xmin:" << xmin << " xmax:" << xmax
151  << " xtick:" << xtick << endl;
152 
153  auto lookup = [](const scalar x) -> scalar
154  {
155  if (x < 2.5) { return 0.2; }
156  if (x < 5.0) { return 0.5; }
157  if (x < 10.0) { return 2.0; }
158  return 10.0;
159  };
160 
161  const scalar power = log10(range);
162  const scalar factor = pow(10, power - floor(power));
163 
164  xtick = lookup(factor)*pow(10, floor(power));
165  xmin = xtick*floor(xmin/xtick);
166  xmax = xtick*(floor(xmax/xtick) + 1);
167 
168  // Convert ticks to integer powers of 10 for log scales
169  if (logScale)
170  {
171  xmin = floor(xmin);
172  xmax = ceil(xmax);
173  xtick = 1;
174  }
175 
176  DebugInfo
177  << "power:" << power << " factor:" << factor
178  << " xmin:" << xmin << " xmax:" << xmax
179  << " xtick:" << xtick << endl;
180 
181  return round((xmax - xmin)/xtick);
182 }
183 
184 
185 // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
186 
187 Foam::functionObjects::graphFunctionObject::graphFunctionObject
188 (
189  const word& name,
190  const Time& runTime,
191  const dictionary& dict
192 )
193 :
195  writeFile(runTime, name, typeName, dict, true, ".svg"),
196  objects_(),
197  entries_(),
198  titles_(),
199  colours_(),
200  dashes_(),
201  times_(),
202  values_(),
203  objectToCol_(),
204  xMin_(dict.getOrDefault<scalar>("xMin", GREAT)),
205  xMax_(dict.getOrDefault<scalar>("xMax", GREAT)),
206  yMin_(dict.getOrDefault<scalar>("yMin", GREAT)),
207  yMax_(dict.getOrDefault<scalar>("yMax", GREAT)),
208  xlabel_(dict.getOrDefault<string>("xlabel", "Iteration/Time")),
209  ylabel_(dict.getOrDefault<string>("ylabel", "Property")),
210  width_(dict.getOrDefault<label>("width", 800)),
211  height_(dict.getOrDefault<label>("height", 600)),
212  strokeWidth_(dict.getOrDefault<label>("strokeWidth", 2)),
213  logScaleX_(dict.getOrDefault<bool>("logScaleX", false)),
214  logScaleY_(dict.getOrDefault<bool>("logScaleY", false)),
215  drawGrid_(dict.getOrDefault<bool>("drawGrid", true))
216 {
217  const dictionary& functions = dict.subDict("functions");
218  objects_.setSize(functions.size());
219  entries_.setSize(functions.size());
220  titles_.setSize(functions.size());
221  colours_.setSize(functions.size());
222  dashes_.setSize(functions.size());
223  objectToCol_.setSize(functions.size());
224 
225  label defaultColouri = 0;
226  label entryi = 0;
227 
228  for (const auto& e : functions)
229  {
230  if (!e.isDict())
231  {
232  FatalIOErrorInFunction(functions)
233  << "Functions must be provided in dictionary format"
234  << exit(FatalIOError);
235  }
236 
237  const dictionary& d = e.dict();
238  objects_[entryi] = d.get<word>("object");
239  entries_[entryi] = d.get<word>("entry");
240  titles_[entryi] = d.getOrDefault<string>("title", e.keyword());
241 
242  labelVector colour;
243  if (d.readIfPresent("colour", colour))
244  {
245  // Warn/error if outside 0-255 range?
246  colour[0] = min(255, max(0, colour[0]));
247  colour[1] = min(255, max(0, colour[1]));
248  colour[2] = min(255, max(0, colour[2]));
249 
250  OStringStream oss;
251  oss << "rgb" << flatOutput(colour, FlatOutput::ParenComma{});
252  colours_[entryi] = oss.str();
253  }
254  else
255  {
256  colours_[entryi] = defaultColours[defaultColouri++];
257  if (defaultColouri == defaultColours.size())
258  {
259  // Lots of lines to plot - exhausted list of default colours.
260  // Restarting ...
261  defaultColouri = 0;
262  }
263  }
264 
265  {
266  labelList dashes;
267  if (d.readIfPresent("dashes", dashes))
268  {
269  OStringStream oss;
270  oss << flatOutput(dashes, FlatOutput::BareSpace{});
271  dashes_[entryi] = oss.str();
272  }
273  else
274  {
275  // Solid line
276  dashes_[entryi] = "0";
277  }
278  }
279 
280  ++entryi;
281  }
282 }
283 
284 
285 // * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
286 
288 {
289  if (!Pstream::master()) return true;
290 
291  scalar& graphTime = times_.emplace_back(time_.timeOutputValue());
292 
293  if (logScaleX_)
294  {
295  graphTime = log10(max(graphTime, SMALL));
296  }
297 
298  label valuei = 0;
299  forAll(objects_, objecti)
300  {
301  bool ok =
302  getValue<label>(objecti, valuei)
303  || getValue<scalar>(objecti, valuei)
304  || getValue<vector>(objecti, valuei)
305  || getValue<sphericalTensor>(objecti, valuei)
306  || getValue<symmTensor>(objecti, valuei)
307  || getValue<tensor>(objecti, valuei);
308 
309  if (!ok)
310  {
311  // Entry not found
312  Log << type() << " " << name() << " execute: "
313  << "Unable to get value for object:" << objects_[objecti]
314  << " entry:" << entries_[objecti] << endl;
315  }
316  }
317 
318  return true;
319 }
320 
321 
323 {
324  if (!Pstream::master()) return true;
325  // DebugVar(values_);
326 
327  auto filePtr = newFileAtTime(name(), time().value());
328  auto& os = filePtr();
329 
330  scalar ymin = GREAT;
331  scalar ymax = -GREAT;
332 
333  bool valid = false;
334  for (const auto& data : values_)
335  {
336  for (const auto& value : data)
337  {
338  ymin = min(ymin, value);
339  ymax = max(ymax, value);
340  valid = true;
341  }
342  }
343 
344  // Early exit if there is no data
345  if (!valid)
346  {
347  Log << type() << " " << name() << " write:" << nl
348  << " No data to plot - skipping" << nl;
349 
350  // Empty graph
351  os << SVG::header(width_, height_) << nl << SVG::end << endl;
352 
353  return false;
354  }
355 
356  auto applyLimits = [](const scalar val, const scalar lim, const bool lg)
357  {
358  if (lim < 0.99*GREAT)
359  {
360  return lg ? log10(lim) : lim;
361  }
362 
363  return val;
364  };
365 
366 
367  // Set y axis limits if user-supplied
368  ymin = applyLimits(ymin, yMin_, logScaleY_);
369  ymax = applyLimits(ymax, yMax_, logScaleY_);
370 
371 
372  scalar ytick = 0;
373  const label ny = setAxisProps(logScaleY_, ymin, ymax, ytick);
374 
375  const scalar border = 0.1;
376  const scalar w = width_*(1.0 - 2*border);
377  const scalar h = height_*(1.0 - 2*border);
378 
379  // Set x axis limits if user-supplied
380  scalar xmin = applyLimits(0, xMin_, logScaleX_);
381  scalar xmax = applyLimits(max(times_), xMax_, logScaleX_);
382 
383  // Set x axis properties; return the number of tic divisions
384  scalar xtick = 0;
385  const label nx = setAxisProps(logScaleX_, xmin, xmax, xtick);
386 
387  // Top pixel co-ordinate
388  auto top = [=](const scalar y)
389  {
390  const scalar ratio = (y - ymin)/(ymax - ymin + ROOTVSMALL);
391  return round(height_ - ratio*h - border*height_);
392  };
393 
394  // Left pixel co-ordinate
395  auto left = [=](const scalar x)
396  {
397  return round(x/(xmax - xmin + ROOTVSMALL)*w + border*width_);
398  };
399 
400  const scalar fontpx = min(20, h/(2*values_.size()));
401  const scalar fontdy = 1.5*fontpx;
402 
403  // Legend - top right: text (right aligned), coloured line (fixed positions)
404  const label legendLineRight = border*width_ + w - fontpx;
405  const label legendLineLeft = legendLineRight - 0.5*border*width_;
406  const label legendLabelRight = legendLineLeft - 0.5*fontpx;
407 
408  // Graph box and tick colour
409  const word colour = "rgb(105,105,105)";
410 
411  os << SVG::header(width_, height_) << nl;
412 
413  // Graph bounding box
414  SVG::element bounds("rect", {{"fill", "none"}, {"stroke", colour}});
415  bounds.addAttr("x", round(border*width_));
416  bounds.addAttr("y", round(border*height_));
417  bounds.addAttr("width", round(w));
418  bounds.addAttr("height", round(h));
419  os << bounds << bounds.end << nl;
420 
421 
422  // X axis label
423  os << SVG::text
424  (
425  xlabel_,
426  0.5*width_,
427  height_ - 0.5*(border*height_) + fontpx,
428  {{"font-size", Foam::name(1.2*fontpx)}},
429  "middle"
430  )
431  << nl;
432 
433  // Y axis label - text rotated
434  SVG::text ytext
435  (
436  ylabel_,
437  0,
438  0,
439  {{"font-size", Foam::name(1.2*fontpx)}},
440  "middle"
441  );
442  ytext.addAttr("alignment-baseline", "middle");
443  ytext.addAttrStr
444  (
445  "transform",
446  "translate(" + Foam::name(left(xmin) - 3*fontpx) + ","
447  + Foam::name(0.5*height_) + ") rotate(270)"
448  );
449  os << ytext << nl;
450 
451  const label dTick = 0.2*fontpx;
452 
453  // Background grid
454  if (drawGrid_)
455  {
456  const word colourGrid = "rgb(200,200,200)";
457 
458  for (label i = 1; i < nx; ++i)
459  {
460  const label x = left(xmin + i*xtick);
461  const label y1 = top(ymin);
462  const label y2 = top(ymax);
463 
464  // Dashed grid lines
465  os << SVG::line
466  (
467  x,
468  y1,
469  x,
470  y2,
471  {
472  {"stroke", colourGrid},
473  {"stroke-width", "1"},
474  {"stroke-dasharray", "4"}
475  }
476  ) << nl;
477  }
478 
479  for (label i = 1; i < ny; ++i)
480  {
481  const label y = top(ymin + i*ytick);
482  const label x1 = left(xmin);
483  const label x2 = left(xmax);
484 
485  // Dashed grid lines
486  os << SVG::line
487  (
488  x1,
489  y,
490  x2,
491  y,
492  {
493  {"stroke", colourGrid},
494  {"stroke-width", "1"},
495  {"stroke-dasharray", "4"}
496  }
497  ) << nl;
498  }
499  }
500 
501  // Axis labels
502  for (label i = 0; i <= nx; ++i)
503  {
504  const scalar v = xmin + i*xtick;
505  const label x = left(v);
506  const scalar y0 = ymin;
507  const label y1 = top(y0);
508  const label y2 = y1 + dTick;
509  const string tickLabel = logScaleX_
510  ? "<tspan>10<tspan style=\"font-size:"
511  + Foam::name(label(0.75*fontpx))
512  + "px\" dy=\"" + Foam::name(-0.4*fontpx) + "\">"
513  + Foam::name(v)
514  + "</tspan></tspan>"
515  : Foam::name(v);
516 
517  // Ticks
518  os << SVG::line
519  (
520  x,
521  y1,
522  x,
523  y2,
524  {
525  {"stroke", colour},
526  {"stroke-width", Foam::name(strokeWidth_)}
527  }
528  ) << nl;
529 
530  // Labels
531  os << SVG::text
532  (
533  tickLabel,
534  x,
535  y2 + 1.25*fontpx,
536  {{"font-size", Foam::name(fontpx)}},
537  "middle"
538  ) << nl;
539  }
540  for (label i = 0; i <= ny; ++i)
541  {
542  const scalar v = ymin + i*ytick;
543  const label y = top(v);
544  const label y2 = y + 0.4*fontpx;
545  const scalar x0 = xmin;
546  const label x1 = left(x0);
547  const label x2 = x1 - dTick;
548  const string tickLabel = logScaleY_
549  ? "<tspan>10<tspan style=\"font-size:"
550  + Foam::name(label(0.6*fontpx))
551  + "px\" dy=\"" + Foam::name(-0.4*fontpx) + "\">"
552  + Foam::name(v)
553  + "</tspan></tspan>"
554  : Foam::name(v);
555 
556  // Ticks
557  os << SVG::line
558  (
559  x1,
560  y,
561  x2,
562  y,
563  {{"stroke", colour},{"stroke-width", "1"}}
564  ) << nl;
565 
566  // Labels
567  os << SVG::text
568  (
569  tickLabel,
570  x2 - 0.5*fontpx,
571  y2,
572  {{"font-size", Foam::name(fontpx)}},
573  "end"
574  ) << nl;
575  }
576 
577 
578  forAll(objects_, objecti)
579  {
580  const word& colour = colours_[objecti];
581 
582  const auto& cols = objectToCol_[objecti];
583  for (const label c : cols)
584  {
585  const word cmpt = cols.size() > 1 ? Foam::name(c) : "";
586 
587  label legendTop = border*height_ + fontdy*(c+1);
588  os << SVG::text
589  (
590  titles_[objecti] + cmpt,
591  legendLabelRight,
592  legendTop,
593  {{"font-size", Foam::name(fontpx)}},
594  "end"
595  ) << nl;
596 
597  os << SVG::line
598  (
599  legendLineLeft,
600  legendTop - 0.5*fontpx,
601  legendLineRight,
602  legendTop - 0.5*fontpx,
603  {{"stroke", colour},{"stroke-width", "2"}},
604  {{"stroke-dasharray", dashes_[objecti]}}
605  ) << nl;
606 
607 
608  os << "<path d=\"";
609  const auto& data = values_[c];
610  bool firstPoint = true;
611  forAll(data, i)
612  {
613  const scalar t = times_[i];
614  const scalar v = data[i];
615 
616  if ((v > ymin) && (v < ymax))
617  {
618  if (firstPoint)
619  {
620  os << " M ";
621  }
622  else
623  {
624  os << " L ";
625  }
626 
627  os << left(t) << ' ' << top(v);
628 
629  firstPoint = false;
630  }
631  else
632  {
633  firstPoint = true;
634  }
635  }
636 
637  os << "\""
638  << " style=\"stroke:" << colour << ";"
639  << " fill:none; stroke-width:2;\""
640  << " stroke-dasharray=\"" << dashes_[objecti].c_str() << "\" />"
641  << nl;
642  }
643  }
644 
645  os << SVG::end << endl;
646 
647  Log << type() << " " << name() << " write:" << nl
648  << " Written file " << os.name() << nl << endl;
649 
650  return true;
651 }
652 
653 
654 // ************************************************************************* //
dictionary dict
void size(const label n)
Older name for setAddressableSize.
Definition: UList.H:116
defineTypeNameAndDebug(ObukhovLength, 0)
uint8_t direction
Definition: direction.H:46
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition: errorManip.H:125
dimensioned< typename typeOfMag< Type >::type > mag(const dimensioned< Type > &dt)
A list of keyword definitions, which are a keyword followed by a number of values (eg...
Definition: dictionary.H:129
label max(const labelHashSet &set, label maxValue=labelMin)
Find the max value in labelHashSet, optionally limited by second argument.
Definition: hashSets.C:40
constexpr char nl
The newline &#39;\n&#39; character (0x0a)
Definition: Ostream.H:50
engineTime & runTime
Ostream & endl(Ostream &os)
Add newline and flush stream.
Definition: Ostream.H:531
dimensionedScalar y0(const dimensionedScalar &ds)
::Foam::direction nComponents(const expressions::valueTypeCode) noexcept
The number of components associated with given valueTypeCode.
Definition: exprTraits.C:40
T get(const word &keyword, enum keyType::option matchOpt=keyType::REGEX) const
Find and return a T. FatalIOError if not found, or if the number of tokens is incorrect.
void push_back(const T &val)
Append an element at the end of the list.
Definition: ListI.H:220
Class to control time during OpenFOAM simulations that is also the top-level objectRegistry.
Definition: Time.H:69
Macros for easy insertion into run-time selection tables.
scalar range
#define forAll(list, i)
Loop across all elements in list.
Definition: stdFoam.H:421
Foam::string str() const
Get the string. As Foam::string instead of std::string (may change in future)
Definition: StringStream.H:256
autoPtr< OFstream > filePtr
Definition: createFields.H:37
fileName::Type type(const fileName &name, const bool followLink=true)
Return the file type: DIRECTORY or FILE, normally following symbolic links.
Definition: POSIX.C:799
scalar y
const dimensionedScalar e
Elementary charge.
Definition: createFields.H:11
void setSize(const label n)
Alias for resize()
Definition: List.H:320
word name(const expressions::valueTypeCode typeCode)
A word representation of a valueTypeCode. Empty for expressions::valueTypeCode::INVALID.
Definition: exprTraits.C:127
A class for handling words, derived from Foam::string.
Definition: word.H:63
Type getObjectResult(const word &objectName, const word &entryName, const Type &defaultValue=Type(Zero)) const
Retrieve result from named object.
label min(const labelHashSet &set, label minValue=labelMax)
Find the min value in labelHashSet, optionally limited by second argument.
Definition: hashSets.C:26
dimensionedScalar y1(const dimensionedScalar &ds)
#define DebugInfo
Report an information message using Foam::Info.
bool readIfPresent(const word &keyword, T &val, enum keyType::option matchOpt=keyType::REGEX) const
Find an entry if present, and assign to T val. FatalIOError if it is found and the number of tokens i...
OBJstream os(runTime.globalPath()/outputName)
addToRunTimeSelectionTable(functionObject, ObukhovLength, dictionary)
const dimensionedScalar h
Planck constant.
dimensionedScalar pow(const dimensionedScalar &ds, const dimensionedScalar &expt)
#define FatalIOErrorInFunction(ios)
Report an error message using Foam::FatalIOError.
Definition: error.H:637
Surround with &#39;(&#39; and &#39;)&#39; separate with &#39;,&#39;.
Definition: FlatOutput.H:86
const dimensionedScalar c
Speed of light in a vacuum.
static bool master(const label communicator=worldComm)
True if process corresponds to the master rank in the communicator.
Definition: UPstream.H:1094
const char * end
Definition: SVGTools.H:223
#define Log
Definition: PDRblock.C:28
Base class for function objects, adding functionality to read/write state information (data required ...
T getOrDefault(const word &keyword, const T &deflt, enum keyType::option matchOpt=keyType::REGEX) const
Find and return a T, or return the given default value. FatalIOError if it is found and the number of...
List< label > labelList
A List of labels.
Definition: List.H:62
Base class for writing single files from the function objects.
Definition: writeFile.H:112
A class for handling character strings derived from std::string.
Definition: string.H:72
void component(FieldField< Field, typename FieldField< Field, Type >::cmptType > &sf, const FieldField< Field, Type > &f, const direction d)
Output to string buffer, using a OSstream. Always UNCOMPRESSED.
Definition: StringStream.H:208
dimensionedScalar log10(const dimensionedScalar &ds)
Namespace for OpenFOAM.
FlatOutput::OutputAdaptor< Container, Delimiters > flatOutput(const Container &obj, Delimiters delim)
Global flatOutput() function with specified output delimiters.
Definition: FlatOutput.H:225
IOerror FatalIOError
Error stream (stdout output on all processes), with additional &#39;FOAM FATAL IO ERROR&#39; header text and ...