Timeline: safely calculate sum/difference of large fractional times

...in a similar vein as done for the product calculation.
In this case, we need to check the dimensions carefully and pick
the best calculation path, but as long as the overall result can
be represented, it should be possible to carry out the calculation
with fractional values, albeit introducing a small error.

As a follow-up, I have now also refactored the re-quantisation
functions, to be usable for general requantisation to another grid,
and I used these to replace the *naive* implementation of the
conversion FSecs -> µ-Grid, which caused a lot of integer-wrap-around

However, while the test now works basically without glitch or wrap,
the window position is still numerically of by 1e-6, which becomes
quite noticeably here due to the large overall span used for the test.
This commit is contained in:
Fischlurch 2022-12-01 23:23:50 +01:00
parent 7007101357
commit 289f92da7e
7 changed files with 326 additions and 21 deletions

View file

@ -73,6 +73,7 @@
#include <stdint.h>
#include <boost/rational.hpp>
#include "lib/util-quant.hpp"
namespace util {
@ -154,6 +155,39 @@ namespace util {
}
/**
* Re-Quantise a number into a new grid, truncating to the next lower grid point.
* @remark Grid-aligned values can be interpreted as rational numbers (integer fractions),
* where the quantiser corresponds to the denominator and the numerator counts
* the number of grid steps. To work both around precision problems and the
* danger of integer wrap-around, the integer division is performed on the
* old value and then the re-quantisation done on the remainder, using
* 128bit floating point for maximum precision. This operation can
* also be used to re-form a fraction to be cast in terms of the
* new quantiser; this introduces a tiny error, but typically
* allows for safe or simplified calculations.
* @param num the count in old grid-steps (#den) or the numerator
* @param den the old quantiser or the denominator of a fraction
* @param u the new quantiser or the new denominator to use
* @return the adjusted numerator, so that the fraction with u
* will be almost the same than dividing #num by #den
*/
inline int64_t
reQuant (int64_t num, int64_t den, int64_t u)
{
u = 0!=u? u:1;
auto [d,r] = util::iDiv (num, den);
using f128 = long double;
// round to smallest integer fraction, to shake off "number dust"
f128 const ROUND_ULP = 1 + 1/(f128(std::numeric_limits<int64_t>::max()) * 2);
// construct approximation quantised to 1/u
f128 frac = f128(r) / den;
int64_t res = d*u + int64_t(frac*u * ROUND_ULP);
ENSURE (abs (f128(res)/u - rational_cast<f128>(Rat{num,den})) <= 1.0/abs(u));
return res;
}
/**
* re-Quantise a rational number to a (typically smaller) denominator.
* @param u the new denominator to use
@ -170,15 +204,7 @@ namespace util {
inline Rat
reQuant (Rat src, int64_t u)
{
int64_t d = rational_cast<int64_t> (src);
int64_t r = src.numerator() % src.denominator();
using f128 = long double;
// construct approximation quantised to 1/u
f128 frac = rational_cast<f128> (Rat{r, src.denominator()});
Rat res = d + int64_t(frac*u) / Rat(u);
ENSURE (abs (rational_cast<f128>(src) - rational_cast<f128>(res)) <= 1.0/abs(u));
return res;
return Rat{reQuant (src.numerator(), src.denominator(), u), u};
}
} // namespace util

View file

@ -46,6 +46,7 @@
#include "lib/error.hpp"
#include "lib/time.h"
#include "lib/time/timevalue.hpp"
#include "lib/rational.hpp"
#include "lib/util-quant.hpp"
#include "lib/format-string.hpp"
@ -336,7 +337,10 @@ lumiera_tmpbuf_print_time (gavl_time_t time)
gavl_time_t
lumiera_rational_to_time (FSecs const& fractionalSeconds)
{
return rational_cast<gavl_time_t> (fractionalSeconds * lib::time::TimeValue::SCALE);
return gavl_time_t(util::reQuant (fractionalSeconds.numerator()
,fractionalSeconds.denominator()
,lib::time::TimeValue::SCALE
));
}
gavl_time_t

View file

@ -78,6 +78,13 @@ namespace util {
{ }
};
template<typename I>
inline IDiv<I>
iDiv (I num, I den) ///< support type inference and auto typing...
{
return IDiv<I>{num,den};
}
/** floor function for integer arithmetics.
* Unlike the built-in integer division, this function

View file

@ -116,6 +116,7 @@ namespace model {
using util::min;
using util::max;
using util::sgn;
namespace { ///////////////////////////////////////////////////////////////////////////////////////////////TICKET #1259 : reorganise raw time base datatypes : need conversion path into FSecs
/**
@ -143,6 +144,12 @@ namespace model {
% duration.denominator();
}
inline double
approx (Rat r)
{
return util::rational_cast<double> (r);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1261 : why the hell did I define time entities to be immutable. Looks like a "functional programming" fad in hindsight
/** @todo we need these only because the blurry distinction between
* lib::time::TimeValue and lib::time::Time, which in turn is caused
@ -521,12 +528,13 @@ namespace model {
static FSecs
scaleSafe (FSecs duration, Rat factor)
{
auto approx = [](Rat r){ return rational_cast<double> (r); };
if (not util::can_represent_Product(duration, factor))
{
if (approx(MAX_TIMESPAN) < approx(duration) * approx (factor))
return MAX_TIMESPAN; // exceeds limits of time representation => cap the result
auto guess{approx(duration) * approx (factor)};
if (approx(MAX_TIMESPAN) < abs(guess))
return MAX_TIMESPAN * sgn(guess); // exceeds limits of time representation => cap the result
if (0 == guess)
return 0;
// slightly adjust the factor so that the time-base denominator cancels out,
// allowing to calculate the product without dangerous multiplication of large numbers
@ -538,6 +546,52 @@ namespace model {
return duration * factor;
}
/**
* Calculate sum (or difference) of possibly large time durations, avoiding integer wrap-around.
* Again, this is a heuristics, based on re-quantisation to a smaller common denominator.
* @return exact result if representable, otherwise approximation
* @note result is capped to MAX_TIMESPAN when exceeding domain
*/
static FSecs
addSafe (FSecs t1, FSecs t2)
{
if (not util::can_represent_Sum (t1,t2))
{
auto guess{approx(t1) + approx(t2)};
if (approx(MAX_TIMESPAN) < abs(guess))
return MAX_TIMESPAN * sgn(guess); // exceeds limits => cap the result
// re-Quantise numbers to achieve a common denominator,
// thus avoiding to multiply numerators for normalisation
int64_t n1 = t1.numerator();
int64_t d1 = t1.denominator();
int s1 = sgn(n1)*sgn(d1);
n1 = abs(n1); d1 = abs(d1);
int64_t n2 = t2.numerator();
int64_t d2 = t2.denominator();
int s2 = sgn(n2)*sgn(d2);
n2 = abs(n2); d2 = abs(d2);
// quantise to smaller denominator to avoid increasing any numerator
int64_t u = d1<d2? d1:d2;
if (u < Time::SCALE)
// regarding precision, quantising to µ-grid is the better solution
u = Time::SCALE;
else //re-quantise to common denominator more fine-grained than µ-grid
if (s1*s2 > 0 // check numerators to detect danger of wrap-around
and (62<util::ilog2(n1) or 62<util::ilog2(n2)))
u >>= 1; // danger zone! wrap-around imminent
n1 = d1==u? n1 : reQuant (n1,d1, u);
n2 = d2==u? n2 : reQuant (n2,d2, u);
FSecs res{s1*n1 + s2*n2, u};
ENSURE (abs (guess - approx(res)) < 1.0/u);
return detox (res);
}
else
// directly calculate ordinary numbers...
return t1 + t2;
}
static Rat
establishMetric (uint pxWidth, Time startWin, Time afterWin)
@ -765,7 +819,7 @@ namespace model {
Rat posFactor = canvasOffset / FSecs{afterAll_-startAll_};
posFactor = parabolicAnchorRule (posFactor); // also limited 0...1
FSecs partBeforeAnchor = scaleSafe (duration, posFactor);
startWin_ = startAll_ + (canvasOffset - partBeforeAnchor);
startWin_ = startAll_ + addSafe (canvasOffset, -partBeforeAnchor);
establishWindowDuration (duration);
startAll_ = min (startAll_, startWin_);
afterAll_ = max (afterAll_, afterWin_);

View file

@ -359,11 +359,12 @@ namespace test {
CHECK (util::toString (sleazy+1) == "134217727/16777216sec");
// also works towards larger denominator, or with negative numbers...
CHECK (reQuant (poison, MAX-7) == 104811045873349724_r/14973006553335675);
CHECK (reQuant (1/poison, MAX) == 1317624576693539413_r/9223372036854775807);
CHECK (reQuant (-poison, 7777) == -54438_r/ 7777);
CHECK (reQuant (poison, -7777) == -54438_r/-7777);
CHECK (approx (reQuant (poison, MAX-7)) == 7);
CHECK (approx ( 1/poison ) == 0.142857149f);
CHECK (approx (reQuant (1/poison, MAX)) == 0.142857149f);
CHECK (approx (reQuant (poison, 7777)) == 6.99987125f);
}
};

View file

@ -554,8 +554,10 @@ namespace test {
CHECK (poison == 206435633551724850_r/307445734561825883);
CHECK (2_r/3 < poison and poison < 1); // looks innocuous...
CHECK (poison + Time::SCALE < 0); // simple calculations fail due to numeric overflow
CHECK (Time(FSecs(poison)) < Time::ZERO); // conversion to µ-ticks also leads to overflow
CHECK (-6 == _raw(Time(FSecs(poison))));
CHECK (poison * Time::SCALE < 0);
CHECK (-6 == rational_cast<gavl_time_t>(poison * Time::SCALE)); // naive conversion to µ-ticks would leads to overflow
CHECK (671453 == _raw(Time(FSecs(poison)))); // however the actual conversion routine is safeguarded
CHECK (671453.812f == rational_cast<float>(poison)*Time::SCALE);
using util::ilog2;
CHECK (40 == ilog2 (LIM_HAZARD)); // LIM_HAZARD is based on MAX_INT / Time::Scale
@ -624,7 +626,7 @@ namespace test {
Rat poisonousDuration = win.pxWidth() / poison; // Now, to demonstrate this "poison" was actually dangerous
CHECK (poisonousDuration == 7071251894921995309_r/8257425342068994); // ...when we attempt to calculate the new duration directly....
CHECK (Time(poisonousDuration) < Time::ZERO); // ...then a conversion to TimeValue will cause integer wrap
CHECK (poisonousDuration * Time::SCALE < 0); // ...then a conversion to TimeValue will cause integer wrap
CHECK(856.350708f == rational_cast<float> (poisonousDuration)); // yet numerically the duration actually established is almost the same
CHECK(856.350708f == rational_cast<float> (_FSecs(win.visible().duration())));
CHECK (win.px_per_sec() == 575000000_r/856350691); // the new metric however is comprised of sanitised fractional numbers
@ -642,7 +644,8 @@ namespace test {
SHOW_EXPR(win.px_per_sec());
SHOW_EXPR(win.pxWidth());
SHOW_EXPR(_raw(win.overallSpan().duration()) * rational_cast<double> (poison))
Time targetPos{TimeValue{gavl_time_t(_raw(win.overallSpan().duration()) * rational_cast<double> (poison))}};
TimeValue targetPos{gavl_time_t(_raw(win.overallSpan().duration())
* rational_cast<double> (poison))};
SHOW_EXPR(targetPos);
SHOW_EXPR(_raw(targetPos));
SHOW_EXPR(_raw(win.visible().start()))

View file

@ -40386,6 +40386,122 @@
<node COLOR="#338800" CREATED="1669682825470" ID="ID_906501375" MODIFIED="1669682838860" TEXT="verallgemeinert: scaleSafe()">
<icon BUILTIN="button_ok"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1669911724009" ID="ID_289279043" MODIFIED="1669911765633" TEXT="verallgemeinert: addSafe()">
<linktarget COLOR="#a32f55" DESTINATION="ID_289279043" ENDARROW="Default" ENDINCLINATION="167;16;" ID="Arrow_ID_333407901" SOURCE="ID_797370792" STARTARROW="None" STARTINCLINATION="-174;-8;"/>
<icon BUILTIN="flag-yellow"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1669932544504" ID="ID_750418137" MODIFIED="1669932569275" TEXT="FSecs &#x27fc; &#xb5;-Tick absichern">
<icon BUILTIN="flag-yellow"/>
<node CREATED="1669932578600" ID="ID_974803158" MODIFIED="1669932591544" TEXT="das erweist sich als h&#xe4;ufige Quelle von wrap-arounds">
<icon BUILTIN="messagebox_warning"/>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1669932596081" ID="ID_1661291024" MODIFIED="1669932618249" TEXT="dabei ist es einfach blo&#xdf; naiv gecodet">
<icon BUILTIN="smiley-oh"/>
</node>
<node CREATED="1669932620086" ID="ID_673992117" MODIFIED="1669932631571" TEXT="stattdessen die re-Quantisierung nutzen">
<icon BUILTIN="idea"/>
<node CREATED="1669932632788" ID="ID_831065868" MODIFIED="1669932761462" TEXT="nur dann hier auf 1/1000000">
<icon BUILTIN="yes"/>
</node>
<node COLOR="#338800" CREATED="1669932640820" ID="ID_1387350127" MODIFIED="1669932675887" TEXT="dazu eine Variante f&#xfc;r den Z&#xe4;hler allein schaffen">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1669932653473" ID="ID_120882361" MODIFIED="1669932684292" TEXT="kann sogar den Rat-Fall hierauf zur&#xfc;ckf&#xfc;hren">
<icon BUILTIN="idea"/>
</node>
<node COLOR="#435e98" CREATED="1669932686823" ID="ID_1630699394" MODIFIED="1669932696052" TEXT="Numerik-Probleme">
<icon BUILTIN="broken-line"/>
<node CREATED="1669932700323" ID="ID_1606021601" MODIFIED="1669932716796" TEXT="tja... Float bleibt Float selbst bei 128bit">
<icon BUILTIN="clanbomber"/>
</node>
<node CREATED="1669932728399" ID="ID_20790978" MODIFIED="1669932757508">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
man <b>mu&#223;</b>&#160;Floating-point runden wenn man glatte Werte will
</p>
</body>
</html></richcontent>
<icon BUILTIN="yes"/>
</node>
<node COLOR="#435e98" CREATED="1669932772210" ID="ID_787506618" MODIFIED="1669933074821" TEXT="Runden oder Tuncate?">
<icon BUILTIN="help"/>
<node CREATED="1669932795247" ID="ID_258240741" MODIFIED="1669932979085" TEXT="Zeit in Lumiera stets left-Truncate">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
das ist essentiell wichtig. &quot;Negative&quot; Zeiten d&#252;rfen sich <i>keinesfalls</i>&#160; anders verhalten. Eine andere Quantsierungs-Regel kann man dann ggfs. high-level auf ein left-Truncate aufsetzen (z.B. Mitte Frame-Intervall)
</p>
</body>
</html></richcontent>
<icon BUILTIN="yes"/>
</node>
<node CREATED="1669932981526" ID="ID_837170036" MODIFIED="1669933009189">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
&#10233; also mu&#223; man hier <i>technisch runden</i>
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1669933011434" ID="ID_1937730928" MODIFIED="1669933021860" TEXT="das bedeutet: auf 1/INT_MAX"/>
<node CREATED="1669933027664" ID="ID_1390211698" MODIFIED="1669933038658" TEXT="weil ein Bruch hier interpretiert wird als Quantisierung"/>
<node CREATED="1669933049926" ID="ID_1930108218" MODIFIED="1669933071839" TEXT="und wir daher auf die kleinstm&#xf6;glich darstellbare rationale Zahl runden"/>
</node>
<node COLOR="#435e98" CREATED="1669933076257" ID="ID_68878490" MODIFIED="1669933203590" TEXT="geht das &#xfc;berhaupt?">
<icon BUILTIN="help"/>
<node CREATED="1669933097062" ID="ID_1298571080" MODIFIED="1669933101833" TEXT="mit double nicht...."/>
<node CREATED="1669933102558" ID="ID_561202442" MODIFIED="1669933112105" TEXT="aber ich arbeite hier ja bewu&#xdf;t mit long double"/>
<node CREATED="1669933113580" ID="ID_1052633426" MODIFIED="1669933130786" TEXT="aber selbst da sprengt ein absolutes ULP den Wertebereich"/>
<node CREATED="1669933140405" ID="ID_672266484" MODIFIED="1669933163530" TEXT="&#x27f9; also relative ULP nehmen: *(1+ULP)"/>
<node CREATED="1669933167811" ID="ID_1558282411" MODIFIED="1669933183695" TEXT="das geht grade noch auf"/>
<node CREATED="1669933185842" ID="ID_1427917386" MODIFIED="1669933196158" TEXT="Schwein gehabt">
<icon BUILTIN="smiley-oh"/>
</node>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1669933204808" ID="ID_1033893690" MODIFIED="1669933252753" TEXT="WARNUNG: falls long double == double dann ist diese Rundung unwirksam">
<icon BUILTIN="messagebox_warning"/>
</node>
<node COLOR="#338800" CREATED="1669933257385" ID="ID_1774777202" MODIFIED="1669933428563" TEXT="Testfall: TimeParsing_test">
<icon BUILTIN="button_ok"/>
<node CREATED="1669933273591" ID="ID_1754745072" MODIFIED="1669933393067" TEXT="Darstellung von 1/250s">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
...das sind 4ms
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1669933281232" ID="ID_542233825" MODIFIED="1669933418231" TEXT="ohne Rundung: 3999&#xb5;s">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
parsing '1/250sec' resulted in 0:00:00.003 instead of 0:00:00.004
</p>
</body>
</html></richcontent>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1668707398858" ID="ID_1529383789" MODIFIED="1668707464339" TEXT="Test">
@ -40434,6 +40550,100 @@
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1669683157298" ID="ID_710000990" MODIFIED="1669683199793" TEXT="Fenster liegt verd&#xe4;chtig weit daneben">
<icon BUILTIN="stop-sign"/>
</node>
<node CREATED="1669911421774" ID="ID_1721803067" MODIFIED="1669911465062" TEXT="Umwandlung FSecs &#x27fc; &#xb5;-ticks entgleist">
<icon BUILTIN="broken-line"/>
<node CREATED="1669911515423" ID="ID_245485754" MODIFIED="1669911569313" TEXT="detox() vorher">
<icon BUILTIN="stop-sign"/>
</node>
<node CREATED="1669911534853" ID="ID_1958976369" MODIFIED="1669911684935" TEXT="die Summe selber k&#xf6;nnte auch wrappen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
...tut sie zwar nicht in dem Beispiel hier, aber mit gen&#252;gend krimineller Energie lie&#223;e sich ein valides Beispiel konstruieren, wobei
</p>
<ul>
<li>
die Ziel-Position dann au&#223;erhalb des legalen Bereichs liegen w&#252;rde
</li>
<li>
bei korrekter Behandlung daher das Ergebnis-Fenster in den legalen Bereich geschoben werden m&#252;&#223;te
</li>
<li>
aber ohne weitere Schutzma&#223;name hier die Berechnung der Zielposition entgleist
</li>
</ul>
</body>
</html></richcontent>
<icon BUILTIN="clanbomber"/>
</node>
</node>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1669911687704" ID="ID_797370792" MODIFIED="1669911771424" TEXT="k&#xf6;nnte man im nun etablierten Schema auch ein addSafe() definieren">
<arrowlink COLOR="#a32f55" DESTINATION="ID_289279043" ENDARROW="Default" ENDINCLINATION="167;16;" ID="Arrow_ID_333407901" STARTARROW="None" STARTINCLINATION="-174;-8;"/>
<icon BUILTIN="help"/>
<node COLOR="#338800" CREATED="1669911787964" ID="ID_147783330" MODIFIED="1669911798248" TEXT="Erkennung und Behandlung der Grenzbedingungen analog">
<icon BUILTIN="button_ok"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1669911817888" ID="ID_493362427" MODIFIED="1669911841073" TEXT="Problem: Summe k&#xf6;nnte trotzdem entgleisen">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1669912456275" ID="ID_661856327" MODIFIED="1669912464070" TEXT="nur bei Addition / gleicher Richtung"/>
<node CREATED="1669912668224" ID="ID_1384499593" MODIFIED="1669914705545" TEXT="nur wenn bei mindestens einem Summanden das h&#xf6;chste Bit gesetzt ist">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
0111 + 0111 = 1110
</p>
<p>
0011 + 0101 = 1000
</p>
<p>
aber...
</p>
<p>
0010 + 0101 = 0111
</p>
</body>
</html></richcontent>
<linktarget COLOR="#644f94" DESTINATION="ID_1384499593" ENDARROW="Default" ENDINCLINATION="-195;8;" ID="Arrow_ID_899257928" SOURCE="ID_41244947" STARTARROW="None" STARTINCLINATION="-101;-8;"/>
</node>
<node CREATED="1669913417600" ID="ID_175910999" MODIFIED="1669913524753" TEXT="&#x27f9; re-Quantisierung mu&#xdf; au&#xdf;erdem beide Summanden aus der Gefahrenzone bringen"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1669913560481" ID="ID_756192644" MODIFIED="1669913569368" TEXT="konkretes Rechenschema">
<icon BUILTIN="info"/>
<node CREATED="1669913571311" ID="ID_1313345585" MODIFIED="1669914699511" TEXT="verwende den (absolut) kleineren der beiden Nenner"/>
<node CREATED="1669914430412" ID="ID_41244947" MODIFIED="1669914714600" TEXT="&#xfc;berpr&#xfc;fe die Gefahrenzone f&#xfc;r die Z&#xe4;hler">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Gefahr besteht...
</p>
<ul>
<li>
wenn beide Vorzeichen gleichgerichtet sind
</li>
<li>
wenn mindestens einer der Z&#228;hler das 63te Bit gesetzt hat (2^62)
</li>
</ul>
</body>
</html></richcontent>
<arrowlink COLOR="#644f94" DESTINATION="ID_1384499593" ENDARROW="Default" ENDINCLINATION="-195;8;" ID="Arrow_ID_899257928" STARTARROW="None" STARTINCLINATION="-101;-8;"/>
<node CREATED="1669914775664" ID="ID_1885451601" MODIFIED="1669914823442" TEXT="wenn ungef&#xe4;hrlich, dann nur gr&#xf6;&#xdf;eren Nenner requantisieren"/>
<node CREATED="1669914824383" ID="ID_999893302" MODIFIED="1669914845448" TEXT="wenn gef&#xe4;hrlich, Nenner um ein Bit shiften und beide requantisieren"/>
</node>
<node CREATED="1669914866913" ID="ID_1877357496" MODIFIED="1669914878786" TEXT="Summation der angepa&#xdf;ten Z&#xe4;hler ausf&#xfc;hren"/>
<node CREATED="1669914880928" ID="ID_1472462771" MODIFIED="1669914895257" TEXT="Ergebnis mit gemeinsamem Nenner konstruieren"/>
<node CREATED="1669914896208" ID="ID_370915623" MODIFIED="1669914901963" TEXT="Ergebnis entgiften"/>
</node>
</node>
</node>
</node>
</node>