Programming Challenge: Werte-Mapping

Begonnen von RichardCZ, 21 April 2020, 10:20:44

Vorheriges Thema - Nächstes Thema

RichardCZ

Gesucht ist eine Lösung, die Wertebereich X auf Wertebereich Y (jeweils float) abbilden kann.

Wer z.B. die "Map" funktion in der Arduino IDE kennt (dort Integer), wird wissen was ich meine, ansonsten siehe Erläuterungen unter: https://forum.fhem.de/index.php/topic,109986.msg1045311.html#msg1045311

Stellt euch einfach vor, ihr habt es mit einem Sensor zu tun, der euch z.B. den Wertebereich -40 bis 100 auf 0 - 5 abbildet.
Das 0-5 seht ihr nicht, denn der AD Wandler bildet das normalerweise auf 0-1023 ab. Aber just nicht euer AD Wandler, denn der macht 8-1016
Ich habe mit Absicht keine Einheiten aufgeführt, denn die interessieren erstmal nicht.

Ihr bekommt also Werte 8-1016 reingeschoben und möchtet das auf -40 - 100 zurückrechnen.
Erstmal für den einfachsten Fall: linear
Bei der Funktion kann man davon ausgehen, dass beide Intervalle geordnet sind [x,y] -> x < y, aber der Wertebereich kann im rein negativen sein, negativ, positiv oder im rein positiven. also [-10, -5], [-3, 200], [100,300] alles valide.

Wie sieht euer Code aus?




Bonus 1:

* Ihr schreibt eine Funktion höherer Ordnung, welche mit den entsprechenden Werten initialisiert wird und eine fertige Transformationsfunktion zurückliefert und wie folgt verwendet werden kann:

my $transform = eure_funktion([-40,100], [8,1016]); # $transform is a coderef

später dann:

my $ambient_temp = $transform->($messwert);

Bonus 2:

eure_funktion hat einen 3. Parameter - wieder eine coderef - welche das Mapping definiert. Also ob sich z.B. der Sensor logarithmisch, exponentiell, geg. Messreihe... verhält. Wenn nicht gegeben, ist der default von eure_funktion "linear".




Preisgeld für jede Submission ist natürlich Ruhm und Ehre. Und wer den besten Code einreicht, mit dem rede ich künftig auch ein klein bischen weniger "von oben herab".  ;) Das ist doch ein Wort?
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

Sidey

Habe ich es richtig verstanden, dass Du das Mapping mittels Parameter einmal zentral realisieren möchtest, damit man diese Arbeit nicht in jedem Modul wiederholt implementieren muß?
Signalduino, Homematic, Raspberry Pi, Mysensors, MQTT, Alexa, Docker, AlexaFhem

Maintainer von: SIGNALduino, fhem-docker, alexa-fhem-docker, fhempy-docker

RichardCZ

Zitat von: Sidey am 21 April 2020, 13:32:25
Habe ich es richtig verstanden, dass Du das Mapping mittels Parameter einmal zentral realisieren möchtest, damit man diese Arbeit nicht in jedem Modul wiederholt implementieren muß?

Eigentlich möchte ich die begeisterte Leserschaft hier heranführen wie generisch Code auszusehen hat, der mal irgendwo in einer Basisklasse seinen Dienst verrichtet. Ich denke, wenn wir schon "Utils" Code möchten, dann bitte richtig.

Dieser Code kann als "zentral" angesehen werden, aber wenn er (wie oben unter "Bonus 1" ausgeführt) eine lokale, bereits parametrierte, Funktion ausspuckt, dann wird die natürlich nur in der entsprechenden Modulinstanz - mit den gewünschten Paramtern - laufen. ... und ggf. nach Beendigung wieder sterben.

Oder es ist nötig, dass ein Sensor rekalibriert werden muss zur Laufzeit - dann kommt solcher Code auch gut.

Ich erwarte jetzt nicht sofort fertigen Lösungen, man kann sich ja auch herantasten.
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

justme1968

#3
sub
createTransform($$;$)
{
  my $undef = sub() { return; };
 
  my $from = shift // return $undef;
  my   $to = shift // return $undef;
  my $func = shift;
 
  return $undef if( ref($from) ne 'ARRAY' );
  return $undef if( ref($to) ne 'ARRAY' );
  return sub { 'not a function' } if( $func && ref($func) ne 'CODE' );
 
  return sub { 'createTransform: from array must have 2 elements' } if( @{$from} != 2 );
  return sub { 'createTransform: to array must have 2 elements' } if( @{$to} != 2 );
 
  my $f_from = $from->[1] - $from->[0];
  my $f_to = $to->[1] - $to->[0];
 
  my $doTransform = sub {
    my $value = shift // return;
   
    $value -= $from->[0];
    $value /= $f_from;
    $value = $func->( $value ) if( $func );
    $value *= $f_to;
    $value += $to->[0];
   
    return $value;
  };
 
  return $doTransform;
}


my $transform = createTransform( [8,1016], [-40,100] );
print $transform->(8) ."\n";
print $transform->(0) ."\n";
print $transform->(512) ."\n";
print $transform->(1016) ."\n";

my $transform2 = createTransform( [0,100], [0,100] );
#my $transform2 = createTransform( [0,100], [0,100], sub($) {(shift) ** 2} );
#my $transform2 = createTransform( [0,100], [0,100], sub($) {sqrt(shift)} );
print $transform2->(0) ."\n";
print $transform2->(25) ."\n";
print $transform2->(50) ."\n";
print $transform2->(75) ."\n";
print $transform2->(100) ."\n";


man könnte das ganze auch noch erweitern um mehr stützstellen zu erlauben (d.h. mehr elemente im from und to array) und dann mehr als eine funktion (jeweils zwischen zwei stützstellen) ...

fehlerbehandlung, parameter prüfung und bereichsüberschreitungen könnte man auch noch verbessern.

und du hast sicher mehr als einen kommentar :)
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

RichardCZ

Zitat von: justme1968 am 22 April 2020, 18:40:48
und du hast sicher mehr als einen kommentar :)

Bestimmt, aber erst wenn ich mir das genauer angeschaut habe. Bis dahin habe ich nur einen kommentar: Super!

Erste Submission in der Challenge, noch dazu gleich mit Currying (siehe "Bonus 1"), da wollte ich hin.

Für alle, die das Thema Currying und "Funktionen höherer Ordnung" mit Perl mehr interessiert hier ein älteres Buch zum Thema und noch dazu ganz legal:
https://hop.perl.plover.com/book/ (link ist https://hop.perl.plover.com/book/pdf/HigherOrderPerl-trimmed.pdf)
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

Sidey

#5
Hi RichardCZ,


den Bonus hab ich erst mal ausgelassen, mache ich vielleicht noch :)
Aber hier meine Idee für die Nachbildung von "map" :)


sub scale {
    my ($val,$source_min,$source_max,$target_min,$target_max) = @_;
   
    return $target_min + ($val - $source_min) * (($target_max - $target_min) / ($source_max - $source_min));
}

print scale(4, 4, 20, 0, 100)."\n";  # 0
print scale(8, 4, 20, 0, 100)."\n"; #  25
print scale(12, 4, 20, 0, 100)."\n"; # 50
print scale(16, 4, 20, 0, 100)."\n";  # 75


print scale(25, 0, 50, 0, 10)."\n";  # 5

print scale(1023, 0, 1023, -40, 80)."\n";  # 80
print scale(0, 0, 1023, -40, 80)."\n";  # -40




Mit Bonus 1 und ein bisschen Errorhandling:
sub
createTransform
{
  my $source = shift // return ;
  my $target = shift // return ;
 
  my ($source_min,$source_max) = @{$source};
  my ($target_min,$target_max) = @{$target};


  my $doTransformSub = sub {
    my $val = shift // return;

    return $target_min + ($val - $source_min) * (($target_max - $target_min) / ($source_max - $source_min));
  };

  return $doTransformSub;
}

my $transform = createTransform( [8,1016], [-40,100] );
print $transform->(8) ."\n";     # -40
print $transform->(0) ."\n";     # -41.111111111
print $transform->(512) ."\n";   # 30
print $transform->(1016) ."\n";  # 100
Signalduino, Homematic, Raspberry Pi, Mysensors, MQTT, Alexa, Docker, AlexaFhem

Maintainer von: SIGNALduino, fhem-docker, alexa-fhem-docker, fhempy-docker

RichardCZ

Will noch jemand Code senden? Keine Panik - ich frage nur, weil wir ja keine Deadline gesagt haben. Wochenende noch?
Oder sollen wir die zwei Submissions besprechen?
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

RichardCZ

Ich danke den beiden Teilnehmern für die eingesandten Lösungen. Code und Anmerkungen habe ich mir angesehen.

Bevor wir das im Detail durchdiskutieren, noch schnell meine ad-hoc Lösung:

sub map_interval {
    my $from = ref $_[0] eq 'ARRAY' ? shift : return warn 'from-arrayref not given';
    my $to   = ref $_[0] eq 'ARRAY' ? shift : return warn 'to-arrayref not given';
    my $mode = shift // 'extrapol';
    my $func = shift;

    my $from_range = $from->[1] - $from->[0];
    my $to_range   = $to->[1]   - $to->[0];
    my $quot       = $to_range  / $from_range;

    if (ref $func ne 'CODE') {          # either undef, or some garbage
        $func = sub {                   # define default function (linear)
            my $value = shift;
            my $linear = $to->[0] + ($value - $from->[0]) * $quot;
            my $mtrans = $mode eq 'minmax' ? min($to->[1], max($to->[0], $linear))
                                           : $linear;
            return $mtrans;
        }
    }

    return sub {                        # Curried function
        my $value = shift // return;    # value to tranform or bail out
        return $func->($value, $from, $to);
    }
}


wobei ich klarstellen möchte, dass meine Lösung weder besser noch schlechter ist. Es ist einfach "eine Lösung".
Für den finalen Code so wie er mir vorschwebt sind somit alle drei Codes ein guter Startpunkt, aber sicher nicht
das letzte Wort.

@justme1968 Beitrag:

Prototypen lasse ich außen vor, denn einerseits ist meine Meinung dazu hier sicher wohlbekannt, andererseits
ist deren Vorhandensein/Nichtvorhandensein in diesem Fall absolut irrelevant. Was mir sehr gut gefällt -
eigentlich am besten von allen drei Lösungen ist das "Einarbeiten" der übergebenen Funktion in die bestehende
Closure. Die Lösung von Sidey hat "Bonus 2" nicht und meine Lösung erwartet eine komplette Neudefinition
falls eine Coderef übergeben wird.

Klar ist meine Lösung die "generischere", aber wenn man sich vor Augen führt wie der Code dann in den Modulen
verwendet werden sollte, dann ist sie auch die mühsamere, bzw. kann man dann den Code gleich lokal schreiben.
Da habe ich rein intuitiv das Gefühl, dass da eine optimale Lösung irgendwo dazwischen liegen muss.

Was ich ebenso überaus positiv empfinde ist der Vorschlag, der über den Code hinausging:

Zitat von: justme1968 am 22 April 2020, 18:40:48
man könnte das ganze auch noch erweitern um mehr stützstellen zu erlauben (d.h. mehr elemente im from und to array) und dann mehr als eine funktion (jeweils zwischen zwei stützstellen) ...

Da hatte ich drüber nachgedacht, sogar selbst ein wenig was probiert, bis mir klar wurde, dass
"dieser Vorschlag: JA, aber innerhalb dieses Codes: NEIN"

Gedankengang wie folgt: Als PBP Vasall stimme ich prinzipiell zu, dass die Komplexität einzelner
Funktionen (Subroutines) sich in Grenzen halten sollte.
(< 70 LOC - was, falls ich mich recht erinnere, auch das perlcritic Kriterium für "complex subroutine" ist)

Bevor man also diese Sub entsprechend erweitern und komplexer machen sollte, könnte man konstruktiv
an die Sache herangehen und diese Sub als "Primitive" einer übergeordneten Sub sehen, die besagte
Mehr-Stützstellen-Funktionalität bereitstellt:

my $mtrans = map_multi_interval([
    [0,    333] => [-40, -20],
    [334,  666] => [-20,  20],
    [667,  766] => [ 20,  60],
    [767, 1023] => [ 60, 100],
]);


Beispielsweise. Und dieser Multi-Map würde nichts anderes machen als besagte map_interval Funktionsdefinitionen
intern generieren und diese - samt einem Wertedispatch wieder in einer coderef zurückliefern.
Et voila! - dann fiel es mir wie Schuppen von den Augen: vermutlich brauchen wir die Coderef in dem "Primitive"
auch nicht, denn vieles kann man auch linear approximieren. Und wieder rein intuitiv, sehe ich hier einen
sehr eleganten, aber auch funktional mächtigen Weg. => danke @justme1968

Wo ich aber schon dabei war, dachte ich mir, dass die Einschränkung, das Interval müsse geordnet/steigend sein,
u.U. nicht notwendig ist, bzw die Sache zu sehr beschneidet. Was wenn unser Wertebereich umgekehrt
proportional ist? Sensor misst Leitfähigkeit/Widerstand. Also angenommen (in MS/m)

[0, 1023] -> [100, 0]

IMHO ein Argument mehr sich rein auf ein generisches Interval -> Interval mapping zu beschränken und erst
daraus komplexere Funktionen zusammenzusetzen.




Ein paar weitere technische Anmerkungen:

Wenn man eine Bibliotheksfunktion schreibt, muss man sich stets vor Augen halten wie sie dann später von
anderen verwendet werden wird bzw. verwendet werden sollte. Der Code muss natürlich effizient, robust etc.
sein, aber gleichzeitig muss es "easy" sein. Wenig Aufwand -> viel Ertrag.

Ich habe eine Weile darüber nachgedacht, warum sich justme1968 so abkarpft mit

  my $undef = sub() { return; };

  my $from = shift // return $undef;
  my   $to = shift // return $undef;


wo doch ein einfaches ... // return; so ... einfach wäre. Nun - je nach Anwendungsfall ist ein einfaches
return eben tödlich. Man stelle sich folgende Situation vor:

my $final = createTransform([$x, $y], [$v, $w])->($input);

Klar, das geht... wenn alles gut läuft, denn createTransform liefert eine Coderef zurück und dieser
kann gleich ein Parameter übergeben werden und das Endergebnis heimsen wir ein.

Wenn aber was schiefläuft und es kommt keine Coderef zurück sondern direkt ein undef, dann fliegt
die Sache fatal auf den Rüssel. Also eine Coderef zurückzuliefern, welche unbedingt undef zurückliefert
ist dann im Fehlerfall besser für

my $final = createTransform([$x, $y], [$v, $w])->($input) // return;




Comments welcome.

Und falls jemand Interesse hat an dem bislang Gesagten anzusetzen (reine Interval -> Interval Funktion
ohne Einschränkung des "Interval Orderings"), immer her damit! Ich werde versuchen einen Codehybrid
aus dem bisher vorhandenen zu generieren.

Ich habe gesagt, wir lassen noch das Wochenende. Vielleicht traut sich ja noch jemand mit seinem Code
vor. Bislang ist das Ranking IMHO 1: justme1968, 2: Sidey (mein Code zählt nicht).
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

RichardCZ

Nur als Randnotiz: https://www.arduino.cc/reference/en/language/functions/math/map/

ist die erwähnte "Map"-Funktion in dem Arduino Formalismus. Tatsächlich erlaubt die Funktion
auch explizit "umgedrehte" Wertebereiche:

ZitatNote that the "lower bounds" of either range may be larger or smaller than the "upper bounds" so the map() function may be used to reverse a range of numbers, for example

y = map(x, 1, 50, 50, 1);

The function also handles negative numbers well, so that this example

y = map(x, 1, 50, 50, -100);

is also valid and works well.

Ich wollte das Rad nicht neu erfinden, wollte mich also umsehen was se so an Interval-mapping code gibt.
Tja... viel mehr als diese Arduino Funktion kommt nicht.
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

justme1968

nur kurz noch ein paar anmerkungen:

- meine version liefert immer (d.h. auch im fehlerfall) eine coderef zurück. d.h. man muss das ergebnis der initialisieren nicht prüfen sondern kann es auf jeden fall ausführen. dafür sieht man den fehler erst zur laufzeit/benutzung. was besser ist hängt vermutlich unter anderem davon ab woher die daten der initialisierung kommen. fest im code oder dynamisch vom benutzer.

- die multimap hat den nachteil das es möglich ist aus versehen überlappende oder nicht kontinuierliche bereiche anzugeben. das muss man dann im hinterkopf haben und passend behandeln. auch ist es mehr schreibarbeit wenn man mit den stützstellen experimentiert und sie verschiebt. deshalb finde ich jeweils ein array mit mehr als zwei werten besser zu handhaben. der zusätzliche aufwand wäre 'nur' zu bestimmen in welchem intervall der aktuelle wert liegt und dann mit den passenden from/to werten weiter zu machen. das muss natürlich nicht unbedingt in die gleiche routine, da es aktuell nur 33 zeilen sind wäre bis 70 noch luft :)

- zumindest meine version kommt auch mit negativen steigungen zurecht: [0,100] -> [0,100], [0,100] -> [100,0], [100,0] -> [0,100], [100,0] -> [100,0], [0,100] -> [0,-100] zurecht. egal ob rein negativ, über 0 gehend, rein positiv, mit skalierung oder ohne und die möglichen kombinationen.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

RichardCZ

Tatsächlich ist in

my $mtrans = map_multi_interval([
    [0,    333] => [-40, -20],
    [334,  666] => [-20,  20],
    [667,  766] => [ 20,  60],
    [767, 1023] => [ 60, 100],
]);


ziemlich viel Redundanz.

my $mtrans = map_multi_interval([
    0    => -40,
    333  => -20,
    666  =>  20,
    766  =>  60,
    1023 => 100,
]);


wäre sicher machbar.
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

Sidey

Hi RichardCZ,

ich hatte bezogen auf deine Aussage keinen final fertig Code geliefert.
Die fehlertolerante codref return hatte ich herausgelassen, weil es mir hauptsächlich um das Prinzip der Lösung ging.

Ich hab da auch noch mal drüber nachgedacht und meinen Code ein wenig verbessert.
Ich habe hier absichtlich kein undef zurück geliefert, denn das würde print (siehe Beispiel) nicht mögen.

sub createTransform
{
  my $source = shift ;
  my $target = shift;

  my $failReturn = sub { return ""; };
  if(ref($source) ne 'ARRAY' || ref($target) ne 'ARRAY') {
    return $failReturn;   
  }
 
  if($#$source != 1 || $#$target != 1 ) {
    return $failReturn;   
  }

  my ($source_min,$source_max) = @{$source};
  my ($target_min,$target_max) = @{$target};


  my $doTransformSub = sub {
    my $val = shift // return;
   return $target_min + ($val - $source_min) * (($target_max - $target_min) / ($source_max - $source_min));
  };

  return $doTransformSub;
}

my $transform = createTransform( [8,1016], [-40,100] );
print $transform->(8) ."\n";     # -40
print $transform->(0) ."\n";     # -41.111111111
print $transform->(512) ."\n";   # 30
print $transform->(1016) ."\n";  # 100


Bislang konnte ich noch keine Kombinationen finden, in denen das Ergebnis nicht stimmt.


PS: Bonus 2 hatte ich im übrigen absichtlich nicht gemacht, das es meiner Ansicht nach die Komplexität unnötig erhöht und die Referenzfunktion Arduino/map das ebenfalls nicht bietet.
Ich tendiere da eher in eigene subs, weil map in den mir bekannten Sprachen immer linear funktioniert.
Signalduino, Homematic, Raspberry Pi, Mysensors, MQTT, Alexa, Docker, AlexaFhem

Maintainer von: SIGNALduino, fhem-docker, alexa-fhem-docker, fhempy-docker

RichardCZ

Kein Thema, bei 2 eingesandten Lösungen und 3 Plätzen auf dem Podest gibt's keine "Verlierer".

Außer die wo da so nicht mitmachen. ;)

Übrigens was

print $transform->(8) ."\n";     # -40
print $transform->(0) ."\n";     # -41.111111111


betrifft, habe ich komplett eine Sache vergessen. Wer seine Aufmerksamkeit auf diese Zeilen in
meinem Code lenkt:

...
    my $mode = shift // 'extrapol';
...
            my $mtrans = $mode eq 'minmax' ? min($to->[1], max($to->[0], $linear))
                                           : $linear;


sieht, dass ich mir da ein wenig feature creep erlaubt habe. Mein Code verhält sich by default so, dass
Werte außerhalb des Definitionsbereichs extrapoliert werden (0 -> -41.11), aber wenn man ihm
den "minmax" mode übergibt, dann eben (0 -> -40). Das fliegt einem natürlich um die Ohren, wenn
das Zielinterval nicht geordnet ist. Da ist also auch noch Verbesserungspotential.

Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

RichardCZ

https://gl.petatech.eu/root/HomeBot/-/commit/1d453e3eea11c235093f339c0cc839fc09d42e48

Ohne coderef, dafür mit start/end clipping Management und Testfällen.
Zielintervall kann auf oder absteigend sein, eine "Division durch 0" haben mir die Testfälle gezeigt, ist abgefangen.

Jetzt werde ich - hierauf aufbauend - das "multi-map" programmieren, basierend auf N Stützstellen (= N-1 Intervalle)

Wenn das jemand in FHEM Utils o.ä. übernehmen will: nur zu.
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

RichardCZ

Zitat von: RichardCZ am 25 April 2020, 21:30:09
my $mtrans = map_multi_interval([
    0    => -40,
    333  => -20,
    666  =>  20,
    766  =>  60,
    1023 => 100,
]);


wäre sicher machbar.

Leichter Index-Brainfuck, aber geht:


sub _build_intervals {
    my $node_mapping = shift;

    return if (@{$node_mapping} % 2);             # we didn't get an even number of nodes
    return if (@{$node_mapping} < 4);             # we didn't get a sufficient number of nodes

    my $iter      = n_iterator($node_mapping);    # get iterator
    my $node1     = $iter->(2);                   # we need at least 4 nodes
    my $node2     = $iter->(2);                   # to define a interval->interval mapping
    my $intervals = [[$node1->[0], $node2->[0]],
                     [$node1->[1], $node2->[1]]]; # get first interval

    while (my $node = $iter->(2)) {               # as long as iterator delivers something
        push @{$intervals},                       # extend our intervals arrayref
            [$intervals->[-2]->[1], $node->[0]],  # with the previous-from, current-from
            [$intervals->[-1]->[1], $node->[1]],  # with the previous-to, current-to
    }

    return $intervals;
}

sub n_iterator {
    my $list = shift;

    return sub {
        my $n = shift;
        return [ splice @{$list}, 0, $n ] if (@{$list});
        return;
    }
}


    my $intervals = _build_intervals($node_mapping);

    say Dumper($intervals);

$VAR1 = [
          [
            0,
            333
          ],
          [
            -40,
            -20
          ],
          [
            333,
            666
          ],
          [
            -20,
            20
          ],
          [
            666,
            766
          ],
          [
            20,
            60
          ],
          [
            766,
            1023
          ],
          [
            60,
            100
          ]
        ];



To be continued...
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.