Loading...
Searching...
No Matches
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-------------------------------------------------------------------------------
10License
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"
35// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
36
37namespace Foam
38{
39namespace functionObjects
40{
43 (
47 );
48}
49}
50
51// 'Muted' colour scheme from https://personal.sron.nl/~pault/ (12.07.24)
52Foam::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
68template<class Type>
69bool 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 {
87 for (direction d = 0; d < pTraits<Type>::nComponents; ++d)
88 {
89 cols.push_back(valuei++);
90 values_.push_back(DynamicList<scalar>());
91 }
92 }
93
94 for (direction d = 0; d < pTraits<Type>::nComponents; ++d)
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
110Foam::label Foam::functionObjects::graphFunctionObject::setAxisProps
111(
112 const bool logScale,
113 scalar& xmin,
114 scalar& xmax,
115 scalar& xtick
116) const
117{
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
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
177 << "power:" << power << " factor:" << factor
178 << " xmin:" << xmin << " xmax:" << xmax
179 << " xtick:" << xtick << endl;
181 return round((xmax - xmin)/xtick);
182}
183
184
185// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
186
187Foam::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;
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// ************************************************************************* //
scalar range
scalar y
#define Log
Definition PDRblock.C:28
Macros for easy insertion into run-time selection tables.
#define addToRunTimeSelectionTable(baseType, thisType, argNames)
Add to construction table with typeName as the key.
for(const label curEdgei :curPointEdges)
label size() const noexcept
The number of elements in list.
Definition DLListBase.H:194
void setSize(label n)
Alias for resize().
Definition List.H:536
Output to string buffer, using a OSstream. Always UNCOMPRESSED.
Foam::string str() const
Get the string. As Foam::string instead of std::string (may change in future).
Class to control time during OpenFOAM simulations that is also the top-level objectRegistry.
Definition Time.H:75
static bool master(const label communicator=worldComm)
True if process corresponds to the master rank in the communicator.
Definition UPstream.H:1714
A list of keyword definitions, which are a keyword followed by a number of values (eg,...
Definition dictionary.H:133
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.
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...
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...
Abstract base-class for Time/database function objects.
const word & name() const noexcept
Return the name of this functionObject.
Accumulates function object result values and renders into a graph in SVG format.
virtual bool execute()
Execute the function-object operations.
virtual bool write()
Write the function-object results.
Base class for function objects, adding functionality to read/write state information (data required ...
stateFunctionObject(const stateFunctionObject &)=delete
No copy construct.
Type getObjectResult(const word &objectName, const word &entryName, const Type &defaultValue=Type(Zero)) const
Retrieve result from named object.
const Time & time_
Reference to the time database.
const Time & time() const
Return time database.
Base class for writing single files from the function objects.
Definition writeFile.H:113
writeFile(const objectRegistry &obr, const fileName &prefix, const word &name="undefined", const bool writeToFile=true, const string &ext=".dat")
Construct from objectRegistry, prefix, fileName.
Definition writeFile.C:200
virtual autoPtr< OFstream > newFileAtTime(const word &name, scalar timeValue) const
Return autoPtr to a new file for a given time.
Definition writeFile.C:110
A class for handling character strings derived from std::string.
Definition string.H:76
A class for handling words, derived from Foam::string.
Definition word.H:66
#define defineTypeNameAndDebug(Type, DebugSwitch)
Define the typeName and debug information.
Definition className.H:142
engineTime & runTime
#define FatalIOErrorInFunction(ios)
Report an error message using Foam::FatalIOError.
Definition error.H:629
OBJstream os(runTime.globalPath()/outputName)
auto & name
#define DebugInfo
Report an information message using Foam::Info.
const char * end
Definition SVGTools.H:223
Namespace for bounding specifications. At the moment, mostly for tables.
const dimensionedScalar c
Speed of light in a vacuum.
Function objects are OpenFOAM utilities to ease workflow configurations and enhance workflows.
Namespace for OpenFOAM.
List< word > wordList
List of word.
Definition fileName.H:60
label max(const labelHashSet &set, label maxValue=labelMin)
Find the max value in labelHashSet, optionally limited by second argument.
Definition hashSets.C:40
List< label > labelList
A List of labels.
Definition List.H:62
Vector< label > labelVector
Vector of labels.
Definition labelVector.H:47
dimensionedScalar y0(const dimensionedScalar &ds)
void component(FieldField< Field, typename FieldField< Field, Type >::cmptType > &sf, const FieldField< Field, Type > &f, const direction d)
dimensionedScalar log10(const dimensionedScalar &ds)
fileName::Type type(const fileName &name, const bool followLink=true)
Return the file type: DIRECTORY or FILE, normally following symbolic links.
Definition POSIX.C:801
dimensionedScalar pow(const dimensionedScalar &ds, const dimensionedScalar &expt)
const word GlobalIOList< Tuple2< scalar, vector > >::typeName("scalarVectorTable")
Ostream & endl(Ostream &os)
Add newline and flush stream.
Definition Ostream.H:519
dimensionedScalar y1(const dimensionedScalar &ds)
dimensioned< typename typeOfMag< Type >::type > mag(const dimensioned< Type > &dt)
label min(const labelHashSet &set, label minValue=labelMax)
Find the min value in labelHashSet, optionally limited by second argument.
Definition hashSets.C:26
FlatOutput::OutputAdaptor< Container, Delimiters > flatOutput(const Container &obj, Delimiters delim)
Global flatOutput() function with specified output delimiters.
Definition FlatOutput.H:217
uint8_t direction
Definition direction.H:49
IOerror FatalIOError
Error stream (stdout output on all processes), with additional 'FOAM FATAL IO ERROR' header text and ...
word name(const expressions::valueTypeCode typeCode)
A word representation of a valueTypeCode. Empty for expressions::valueTypeCode::INVALID.
Definition exprTraits.C:127
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition errorManip.H:125
constexpr char nl
The newline '\n' character (0x0a).
Definition Ostream.H:50
dictionary dict
volScalarField & h
volScalarField & e
#define forAll(list, i)
Loop across all elements in list.
Definition stdFoam.H:299
Surround with '\0' and '\0' separate with ' '.
Definition FlatOutput.H:81
Surround with '(' and ')' separate with ','.
Definition FlatOutput.H:86
void addAttr(const char *key, const Type &value)
Definition SVGTools.H:76
autoPtr< OFstream > filePtr