diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index 2fd869df2..776befd69 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -136,13 +136,14 @@ namespace model { /** * @return `true` if the given duration can be represented cleanly as µ-ticks. + * @remark decision can be broken down to the remainder term: `n/d = i + r/d`; + * when expanding with `*u/u`, result is clean if `u/d` is non-fractional. * @todo should likewise be member of a FSecs wrapper type... */ inline bool isMicroGridAligned (FSecs duration) { - return 0 == (duration.numerator() * Time::SCALE) - % duration.denominator(); + return 0 == Time::SCALE % duration.denominator(); } inline double @@ -582,11 +583,13 @@ namespace model { // One case stands out, insofar this factor is guaranteed to be present: // because one of the numbers is a quantised Time, it has Time::SCALE as denominator, // maybe after cancelling out some further common integral factors - int64_t reduction = Time::SCALE / duration.denominator(); + auto [reduction,rem] = util::iDiv (Time::SCALE, duration.denominator()); + if (rem != 0) reduction = 1; // when duration is not µ-Tick quantised + int64_t durationQuant = duration.denominator()*reduction; int64_t durationTicks = duration.numerator()*reduction; //-f1--------------------+-u-------------------+-q---------------------+-f2--------------------+-invert-- - Cases cases{{{durationTicks , Time::SCALE , factor.numerator() , factor.denominator() , false} + Cases cases{{{durationTicks , durationQuant , factor.numerator() , factor.denominator() , false} ,{factor.numerator() , factor.denominator(), duration.numerator() , duration.denominator(), false} ,{duration.denominator(), duration.numerator(), factor.denominator() , factor.numerator() , true} ,{factor.denominator() , factor.numerator() , duration.denominator(), duration.numerator() , true} @@ -689,7 +692,7 @@ namespace model { } /** calculate `rational_cast (zoomFactor * duration)` - * @remark indirect calculation path to avoid overflow on large durations + * @remark indirect calculation to avoid overflow on large durations */ static int64_t calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration) @@ -715,6 +718,58 @@ namespace model { } // Note: denominator 1000 is additional safety margin // wouldn't be necessary, but makes detox(largeTime) more precise + /** + * Reform the effective metric in all dangerous corner cases. + * Ensure the metric value is not »poisonous« and can be multiplied + * even with Time::SCALE without numeric wrap-around. + * @note this function introduces a slight error to simplify the numbers; + * then the result is optimised to conform to pxWith and duration + */ + Rat + optimiseMetric (uint pxWidth, FSecs dur, Rat rawMetric) + { + using util::ilog2; + REQUIRE (0 < pxWidth and 0 < dur and 0 < rawMetric); + REQUIRE (isMicroGridAligned (dur)); + // circumvent numeric problems due to excessive large factors + int64_t magDen = ilog2(rawMetric.denominator()); + int reduction = toxicDegree (rawMetric); + int quant = max (magDen-reduction, 16); + // re-quantise metric into power of two <= 2^40 (headroom 22 bit) + // Known to work always, since 9e-10 < metric < 2e+6 + Rat adjMetric = util::reQuant (rawMetric, int64_t(1) << quant); + + // Correct that metric to reproduce expected pxWidth... + // Retain reduced denominator, but optimise the numerator + // pixel = trunc{ metric*duration } + double epsilon = std::numeric_limits::epsilon() + , dn = dur.numerator() + , dd = dur.denominator() + , md = adjMetric.denominator() + , mn = (pxWidth+epsilon)*md*dd/dn; + // construct optimised zoom metric result + int64_t num = mn, den = adjMetric.denominator(); + if (epsilon < mn - num) + {// optimisation found inter-grid result -- increase precision + int headroom = max (1, HAZARD_DEGREE - max (ilog2(num), ilog2(den))); + int64_t scale = int64_t(1) << headroom; + num = scale*mn; // quantise again with increased resolution + den = scale*den; // at least factor 2 to get some improvement + if (pxWidth > dn/dd*num/den) // If still some remaining error.... + ++num; // round up to be sure to hit the next higher pixel count + } + adjMetric = Rat{num, den}; + ENSURE (pxWidth == calcPixelsForDurationAtScale (adjMetric, dur)); + double impliedDur = double(pxWidth)*den/num; + double relError = abs(dn/dd /impliedDur -1); + double quantErr = 1.0/(num-1); + ENSURE (quantErr > relError, "metric misses duration by " + "%3.2f%% > %3.2f%% (=relative quantisation error)" + ,100*relError, 100.0*quantErr); + return adjMetric; + } + + static Rat establishMetric (uint pxWidth, Time startWin, Time afterWin) { @@ -724,35 +779,13 @@ namespace model { pxWidth = max (1, rational_cast (DEFAULT_METRIC * dur)); Rat metric = Rat(pxWidth) / dur; // rational arithmetic ensures we can always reproduce the pxWidth - ENSURE (pxWidth == rational_cast (metric*dur)); + ENSURE (pxWidth == calcPixelsForDurationAtScale (metric, dur)); ENSURE (0 < metric); return metric; } - Rat - conformMetricToWindow (uint pxWidth) - { - REQUIRE (pxWidth > 0); - REQUIRE (afterWin_> startWin_); - FSecs dur{afterWin_-startWin_}; - Rat adjMetric = detox (Rat(pxWidth) / dur); - // check if new metric reproduces expected pxWidth... - for (uint resPx - ;pxWidth != (resPx = calcPixelsForDurationAtScale (adjMetric, dur)) // calculate trunc {adjMetric*dur} - ; // but calculate cleverly to avoid numeric wrap - ) // Problem: detox() introduced too much error - { // need to adjust metric to match expected pxWidth - // Using Newton-Raphson; can't just calculate fix due to numeric-wraparound - auto delta = double(resPx) - pxWidth; // note: approximation calculated in double - int64_t mn{adjMetric.numerator()}, dn{dur.numerator()}, - md{adjMetric.denominator()}, dd{dur.denominator()}; - //assuming: f(xₙ) ≔ Δₙ = resPx-pxWidth = trunc{ xₙ * dn/(md*dd) } - pxWidth - mn -= int64_t(delta * md*dd / dn); // xₙ₁ ≔ xₙ - f(xₙ)/f'(xₙ) - adjMetric = Rat{mn, md}; - } - return adjMetric; - } - + /** this is the centrepiece of the whole zoom metric logic... + * @note control flow for every scale adjustment passes here */ void conformWindowToMetric (Rat changedMetric) { @@ -761,9 +794,8 @@ namespace model { FSecs dur{afterWin_-startWin_}; uint pxWidth = calcPixelsForDurationAtScale (px_per_sec_, dur); dur = Rat(pxWidth) / detox (changedMetric); - dur = min (dur, MAX_TIMESPAN); - dur = max (dur, MICRO_TICK); // prevent window going void - dur = detox (dur); // prevent integer wrap in time conversion + dur = min (dur, MAX_TIMESPAN);// limit maximum window size + dur = max (dur, MICRO_TICK); // prevent window going void TimeVar timeDur{Duration{dur}}; // prefer bias towards increased window instead of increased metric if (not isMicroGridAligned (dur)) @@ -773,10 +805,24 @@ namespace model { establishWindowDuration (Duration{timeDur}); // re-check metric to maintain precise pxWidth px_per_sec_ = conformMetricToWindow (pxWidth); - ENSURE (_FSecs(afterWin_-startWin_) < MAX_TIMESPAN); + ENSURE (_FSecs(afterWin_-startWin_) <= MAX_TIMESPAN); ENSURE_matchesExpectedPixWidth (changedMetric, afterWin_-startWin_, pxWidth); } + Rat + conformMetricToWindow (uint pxWidth) + { + REQUIRE (pxWidth > 0); + REQUIRE (afterWin_> startWin_); + FSecs dur{afterWin_-startWin_}; + Rat adjMetric = Rat(pxWidth) / dur; + if (not toxicDegree(adjMetric) + and pxWidth == calcPixelsForDurationAtScale (adjMetric, dur)) + return adjMetric; + else + return optimiseMetric(pxWidth, dur, adjMetric); + } + /** * The zoom metric factor must not become "poisonous". * This leads to a minimum possible zoom factor for a given pixWidth, @@ -985,7 +1031,7 @@ namespace model { void placeWindowRelativeToAnchor (FSecs duration) { - FSecs partBeforeAnchor = relativeAnchor() * duration; + FSecs partBeforeAnchor = scaleSafe(duration, relativeAnchor()); startWin_ = Time{anchorPoint()} - Time{partBeforeAnchor}; } @@ -1016,7 +1062,7 @@ namespace model { FSecs anchorPoint() const { - return startWin_ + FSecs{afterWin_-startWin_} * relativeAnchor(); + return startWin_ + scaleSafe (afterWin_-startWin_, relativeAnchor()); } /** diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index fe92b69ce..c1f6f38bc 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -715,19 +715,47 @@ namespace test { /*--Test-4-----------*/ win.setMetric (1001_r/LIM_HAZARD); // but zooming in more than that limit will be honored - CHECK (_raw(win.visible().duration()) == 3295239643684000); // ...window now slightly reduced in size + CHECK (_raw(win.visible().duration()) == 3295239643684316); // ...window now slightly reduced in size CHECK (_raw(win.visible().duration()) < 3 * LIM_HAZARD*1000); - CHECK (win.px_per_sec() == 750_r/823809910921); // ...yet effective zoom factor was still marginally adjusted for safety - CHECK (win.px_per_sec() > 1000_r/LIM_HAZARD); - CHECK (win.px_per_sec() > 1001_r/LIM_HAZARD); // (this is what was requested) - CHECK (win.px_per_sec() < 1002_r/LIM_HAZARD); // (thus result was slightly adjusted upwards) + CHECK (win.px_per_sec() > 1000_r/LIM_HAZARD); + CHECK (win.px_per_sec() == 1001_r/LIM_HAZARD); // (this is what was requested) + CHECK (win.px_per_sec() == 1001_r/1099511627776); CHECK (win.pxWidth() == 3); - + /*--Test-5-----------*/ + win.setMetric (1000_r/LIM_HAZARD * 1024_r/1023); // likewise zooming back out slightly below limit is possible + CHECK (_raw(win.visible().duration()) == 3295313657856000); // ...window now again slightly increased, but not at maximum size + CHECK (_raw(win.visible().duration()) < 3 * LIM_HAZARD*1000); + CHECK (win.px_per_sec() > 1000_r/LIM_HAZARD); + CHECK (win.px_per_sec() < 1001_r/LIM_HAZARD); + CHECK (win.px_per_sec() == 1000_r/LIM_HAZARD * 1024_r/1023); // zoom factor precisely reproduced in this case + CHECK (win.px_per_sec() == 125_r/137304735744); + CHECK (win.pxWidth() == 3); + + /*--Test-6-----------*/ + win.setMetric (1001_r/(LIM_HAZARD-3)); // however, setting »poisonous« factors close below the limit... + CHECK (win.px_per_sec() > 1001_r/LIM_HAZARD); // results in a sanitised (simplified) zoom factor + CHECK (win.px_per_sec() < 1002_r/LIM_HAZARD); + CHECK (1001_r/(LIM_HAZARD-3) == 77_r/84577817521); // This case is especially interesting, since the initial factor isn't »toxic«, + // but the resulting duration is not µ-grid aligned, and after fixing that, + CHECK (3_r/3295239643675325 * Time::SCALE == 120000_r/131809585747013);// the resulting zoom factor is comprised of very large numbers, + CHECK (win.px_per_sec() == 2003_r/2199023255552); // ...which are then simplified and adjusted... + CHECK (win.pxWidth() == 3); // ... to match also the pixel size + + CHECK (_raw(Duration{3_r/(77_r/84577817521)}) == 3295239643675324); // This is the duration we'd expect (truncated down) + CHECK (_raw(win.visible().duration()) == 3295239643675325); // ...this is the duration we actually get + CHECK (_raw(Duration{3_r/win.px_per_sec()}) == 3293594491590614); // Unfortunately, calculating back from the smoothed zoom-metric + // .. would yield a duration way off, with an relative error < 1‰ + CHECK (2003.0f/2002 - 1 == 0.000499486923f); // The reason for this relative error is the small numerator of 2002 + // (2002 is increased to 2003 to get above 3px) + + /*--Test-7-----------*/ win.calibrateExtension (1'000'000'000); // implicit drastic zoom-out by increasing the number of pixels CHECK (win.pxWidth() < 1'000'000'000); // however: this number is capped at a fixed maximum CHECK (win.pxWidth() == MAX_PX_WIDTH); // (which „should be enough“ for the time being...) - CHECK (win.px_per_sec() == 16062_r/98763149723); // the zoom metric has been adapted, but to a sanitised value + CHECK (win.px_per_sec() == 89407_r/549755813888); // the zoom metric has been adapted, but to a sanitised value + CHECK (win.px_per_sec() > Rat{MAX_PX_WIDTH} /MAX_TIMESPAN); + CHECK (win.px_per_sec() < Rat{MAX_PX_WIDTH+1}/MAX_TIMESPAN); CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // overall canvas duration not changed CHECK (_raw(win.visible().duration()) == 614891469123651720); // window duration now expanded to the maximum possible value @@ -740,21 +768,18 @@ namespace test { CHECK (MAX_PX_WIDTH * 1000000_r/614891469123651720 == 2500000000_r/15372286728091293); CHECK (win.px_per_sec() * _FSecs(win.visible().duration()) < 0); // we can't even calculate the resulting pxWidth() naively CHECK (rational_cast(win.px_per_sec()) // ...while effectively these values are still correct - * rational_cast(_FSecs(win.visible().duration())) == 100000.727f); + * rational_cast(_FSecs(win.visible().duration())) == 100000.031f); CHECK (rational_cast(MAX_PX_WIDTH*1000000_r/614891469123651720) == 1.62630329e-07f); // theoretical value - CHECK (rational_cast(win.px_per_sec()) == 1.62631508e-07f); // value actually chosen - CHECK (win.px_per_sec() == 16062_r/98763149723); + CHECK (rational_cast(win.px_per_sec()) == 1.62630386e-07f); // value actually chosen + CHECK (win.px_per_sec() == 89407_r/549755813888); - /*--Test-6-----------*/ - win.setMetric (bruteZoom); // And now put one on top by requesting excessive zoom-out: + /*--Test-8-----------*/ + win.setMetric (bruteZoom); // And now put one on top by requesting excessive zoom-out! CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // overall canvas duration not changed - SHOW_EXPR(_raw(win.overallSpan().duration())) - SHOW_EXPR(_raw(win.visible().duration())) - CHECK (_raw(win.visible().duration()) == 614891469123640625); // window duration was slightly decreased -- WHY?? (TODO) - CHECK (16062_r/98763149723 > 200000_r/1229782938247); // zoom factor numerically slightly reduced - SHOW_EXPR(win.px_per_sec()) - CHECK (win.px_per_sec() == 200000_r/1229782938247); // and now hitting again the minimum limit - SHOW_EXPR(MAX_PX_WIDTH /(614891469123651720_r/1000000)) + CHECK (_raw(win.visible().duration()) == 614891469123651720); // window duration was capped precisely at DURATION_MAX + CHECK (win.px_per_sec() == 89407_r/549755813888); // zoom factor and now hitting again the minimum limit + CHECK (MAX_PX_WIDTH /(614891469123651720_r/Time::SCALE) == 2500000000_r/15372286728091293); // (this would be the exact factor) + CHECK (2500000000_r/15372286728091293 < 89407_r/549755813888); // zoom factor (again) numerically sanitised CHECK (win.pxWidth() == MAX_PX_WIDTH); // pixel count unchanged at maximum } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 19c16b614..14f856604 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -38729,7 +38729,7 @@ - + @@ -38909,7 +38909,7 @@ - + @@ -39036,6 +39036,7 @@ + @@ -39067,7 +39068,7 @@ - + @@ -39358,6 +39359,10 @@ + + + + @@ -39677,7 +39682,12 @@ - + + + + + + @@ -40182,8 +40192,8 @@ - - + + @@ -40363,9 +40373,10 @@ - - - + + + + @@ -40395,7 +40406,7 @@ - + @@ -40522,7 +40533,7 @@ - + @@ -40549,7 +40560,7 @@ - + @@ -40638,6 +40649,7 @@ + @@ -40682,16 +40694,542 @@ - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ bleibt noch der Belang: die Duration soll hier auf µ-Tick aufgerundet  werden +

+ +
+ + + + + + + + + + + + + +

+ (num * Time::SCALE) % den == 0 +

+

+ Das ist zwar offensichtlich richtig, erlaubt aber als Solches noch keine weiteren Schlüsse, sofern man über num/den nichts weiter weiß +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

+ das geht in Floating-Point aber eben grade nicht in Integer-Bruchrechnung; ein ULP ist der »maximal giftige Bruch« +

+ +
+
+ + + + + + + + + + +
    +
  • + man ruft eine Detektor-Funktion auf +
  • +
  • + man inkrementiert die quantisierte Variante +
  • +
+ +
+
+
+ + + + + + +

+ das hier ist die beste Alternative +

+ +
+ +
+
+
+ + + + + + +

+ sonst kann man eine fehlerfreie Kalkulation nicht garantieren +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ die MAX_TIMESPAN-Limitierung beginnt etwas über 1000px zu greifen; diese Unterschwelle wäre aber px · 1e-14 +

+ +
+
+
+ + + + + + + +

+ minimale Pixel-Zahl 1px. Mit 1/10 Band dann 1.1, mit 1px-Band 2 +

+ +
+
+ + + + + + + +
+ + + + + + + + + + + + + +

+ will sagen, wenn man das 0.1px-Band zum Absturz bringen kann, dann klappt das mit der doppelten Zeit auch beim 1px-Band +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Die Newton-Approximation kam ja nur ins Spiel, weil ich mit Integer rechnen wollte, in dieser Domain aber die Rechnung nicht umkehren konnte. Im weiteren Kontext betrachtet war das von Anfang an ein Schmuh (und ich habe jetzt schon zwei Tage lang ein dumpfes Gefühl, daß es ein Schmuh ist....) +

+ +
+
+ + + + + + + + + +

+ Im Regelfall möchte ich die präzise Integer-Bruch-Arithmetik, aber ich möchte die Grenzfälle nahtlos mit integrieren, und nehme für diese Grenzfälle absolut betrachtet erehbliche Fehler in Kauf. +

+ +
+ + +
+ + + +
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +

+ kommt dadurch zustande, daß ich nach dem Hochskalieren i.d.R doch noch einen Rest habe, den ich für Integer-Arithmetik abrunde; damit dann trotzdem die gewünschte (i.d.R. um einen Pixel höhere) Pixelzahl rauskommt, inkrementiere ich die letzte Stelle, und das ist zugleich meine maximale Fehlerschranke. In Extremfällen haben wir im Zähler noch eine 4-stellige Zahl (1000/LIM_HAZARD) und damit etwa 1‰ Fehler maximal. Das ist nicht schön, aber akzeptabel — gemessen daran, daß ich dadruch eine »ungiftige« Metrik sicherstellen kann. Sobald man etwas weg ist von der Unterschranke der Metrik 1000/LIM_HAZARD, wirkt das Nach-Skalieren und Mitnehmen des gebrochenen Anteils viel besser, und der Fehler sinkt drastisch +

+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
  • + und zwar mit einer Zahl zwischen 1001/LIM_HAZARD > gesucht > 1000/LIM_HAZARD +
  • +
  • + die resultierende Metrik sollte idealerweise giftig sein +
  • +
  • + aber auch noch Headroom übrig lassen (andernfalls gehen wir in die falsche Richtung) +
  • +
+ +
+
+
+ + + + + + +

+ 1000_r/LIM_HAZARD * 1024_r/1023 +

+ +
+
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ geht, weil die Metrik limitiert ist, insofern ist wenigstens dafür im Extremfall noch ein Bit übrig +

+ +
+
+
+ + + + + + + + + + + +
+
+ + + + + + +

+ ...und der ganze Ansatz mit Newton-Näherung stellte sich als unsinnig heraus; jetzt rechnen wir die Optimierung mit einem Schuß in float, und reizen dann sogar noch den Headroom aus, wodurch die Werte viel genauer werden +

+ +
+ +
+
+
+ +
@@ -40746,6 +41284,17 @@
+ + + + + + + + + + + @@ -41189,8 +41738,111 @@ - + + + + + + + + + + + + + + + + + +

+ hier wissen wir, daß wir den Quantiser ggfs hochskalieren können auf 1e6 (er hat sich u.u mit der µ-Tick-Zahl gekürzt). Da es sich um eine valide Duration handelt, ist dieses Hochskalieren stets garantiert. Mithin ist dieser Faktor das garantierte Minimum (die schlechtest mögliche Genauigkeit ≙ 1e-6 +

+ +
+ +
+
+ + + + + + + + + + + + + + +

+ aus Gründen der Symmetrie kann man das gleiche Argument jeweils auch auf den Kehrwert anwenden, deshalb haben wir ja 4 Fälle (bei zwei Eingangs-Faktoren). Man muß nur ggfs. dann den Kehrwert vom Ergebnis ausgeben. Beispiel: wir nehmen den Zähler vom ersten Faktor als Quantisierer. Dann ist f1 der Nenner vom ersten Faktor, und muß daher auch im Ergebnis im Nenner landen, nicht im Zähler wie beim regulären Schema +

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -41329,7 +41981,7 @@ - + @@ -41590,77 +42242,8 @@ - - - - - - - - - - - -

- hier wissen wir, daß wir den Quantiser ggfs hochskalieren können auf 1e6 (er hat sich u.u mit der µ-Tick-Zahl gekürzt). Da es sich um eine valide Duration handelt, ist dieses Hochskalieren stets garantiert. Mithin ist dieser Faktor das garantierte Minimum (die schlechtest mögliche Genauigkeit ≙ 1e-6 -

- -
- -
-
- - - - - - - - - - - - - - -

- aus Gründen der Symmetrie kann man das gleiche Argument jeweils auch auf den Kehrwert anwenden, deshalb haben wir ja 4 Fälle (bei zwei Eingangs-Faktoren). Man muß nur ggfs. dann den Kehrwert vom Ergebnis ausgeben. Beispiel: wir nehmen den Zähler vom ersten Faktor als Quantisierer. Dann ist f1 der Nenner vom ersten Faktor, und muß daher auch im Ergebnis im Nenner landen, nicht im Zähler wie beim regulären Schema -

- -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +