Messy math with map() and pow()

Hi everyone,

I'm trying to improve upon my old code to controll humidity and temperature inside my terrarium.

The fans used to be drivven by a linear map() function but this results in short, powerfull bursts that pushed beyond the target value. I'm trying to use a pow() function ( x^(x/100) ) to follow a nice graph like this:

I have something that seems to be working (still testing) but the math looks kinda rubbish. I map de sensor readings to the target temperature (desiredHumidity) and the maximum tolerable value (90% in this case). After mapping I use pow() to adjust "severity" and after I map it again to get a PWM output signal. Any tips to clean this up?

code:

if (humidity > maxHumidity) { humidity = maxHumidity; }
    
mappedVal = map(humidity, desiredHumidity, maxHumidity, 0, 100); 
computedVal = pow(mappedVal, mappedVal / 100);
pwmVal1 = map(computedVal, 0, 100, 0, 255);

digitalWrite(pwmPin_1, pwmVal1);

seems odd that the lower bound is the target, desiredHumidity. The result is a negative value if < the target

this seems odd as well, raising the mappedVal to the power of itself scaled.

looks like your curve maps to x^3 / (1.7 * 10*4). I'm not sure x is your humidity measurement

That bugs me as well. I could throw in if (mappedVal < 0) { mappedVal =0; }

I won't pretend that I understand it well, myself. After searching all afternoon I found the formula and it gave me the exact graph I wanted :grimacing:

You have the 'mappedVal' in the exponent. That is too complex for me.

There are online curve fitting sites.
For example: https://mycurvefit.com/. Enter the data points, then try all the curve fitting formulas. It shows how well the curve goes through the points.

Your curve gave me both a linear and a power curve feeling, so that is what I made:

// Messy math with map() and pow()
// Forum:
//   https://forum.arduino.cc/t/messy-math-with-map-and-pow/1275821
// This Wokwi project: https://wokwi.com/projects/401785042678168577
//    
//
// A simple power curve could be enough.
// But the lower part of the graph has a more linear part.
// Therefor I made a smooth transition from linear to
// a power curve.
//
// The "y(forum)" are guessed values from the graph
// on the forum
//   x    y(forum)  y(this sketch)
//   -----------------------------
//   0    1         1    
//   20   2         2.8
//   40   4.5       4.5
//   60   12        10.4
//   80   33        33.5 
//   100  100       100  
//
// The values are calculated between 0...1 for simplicity.

void setup() 
{
  Serial.begin(115200);

  float x = 0;
  for(int i=0; i<=10; i++)
  {
    float ypow = pow(x,4.2);         // the power curve
    float ylin = 0.01 + x/8.0;       // the linear curve
    float y = (1.0-x)*ylin + x*ypow; // smooth transistion

    Serial.print(x*100.0);
    Serial.print(", ");
    Serial.println(y*100.0);
    x += 0.1;
  }
}

void loop() 
{
  delay(10);
}

Try it in Wokwi:

The left side of the curve is linear and the right side of the curve is a power curve. With a smooth transition from 0 to 100%.

That website can make a good curve with the 'x' value in the exponent of the power function :thinking: This is a good matching curve: y = 1.27847 - (-0.0207948/-0.05583161)*(1 - e^(+0.05583161*x)).
It can be entered as y=1.27847-(-0.0207948/-0.05583161)*(1-exp(0.05583161*x)) here: https://www.desmos.com/calculator
afbeelding

If you make a function that returns the calculated value, then you can always alter and tune the curve in the function.

I was using desmos.com for the initial graph I posted, using X ^ ( X/100).

I didn't know about mycurvefit.com though, so thanks. Frustratingly, even after registering, I can't finish what I started after refining datapoints. Maxed out calculations for trial version :neutral_face:

Either way: is there any benefit using a long incomprehensible row of numbers for a calculation instead of using pow() ?

Do you want to control a fan or a temperature ?
Maybe you need a PID control instead of a curve.

If you need a curve, then 5 data points with linear interpolation might be good enough. It is straightforward and easy to alter a value.

1 Like

I had to google what a PID is but that sounds about right, except the rest of my code won't work with it.

I'm controlling both temperature and humidity with, currently, 1 set of fans.

It's a long story but currently I'm focussing on controlling 1 of these values. There is some code for situations where both values are deviating, but I'd rather have something working for either first.

The linear distribution using map() was working but always overshooting and causing a counter reaction. So I started looking into a way to have a very fine low setting, so the closer I get to the target value, the less of an active influence is exerted.

Counter to the fan for a high value is, in case of the humidity, an ultrasonic mist maker. Hence I don't want to overshoot.

Maybe a PID is the way to go, though it does not sound much easier. I'll read up on it.

I see. Running it on a Giga R1 so plenty of space (and power).

@arneko

As Koepel said, multi point linear interpolation might work good enough.

Have a look at multimap() which was made to do non linear mapping by multipoint linear interpolation.

1 Like

I read about your MultiMap function when I was reading this old thread today. It's where I got the pow() idea from.

To be honest I could not quite wrap my head around it, but let me hazard a guess.
I make an array with values corresponding to the X-axis and a similar array for the Y-axis, and MultiMap will use this to calculate all other points between the given values, even though they may not be linear?

I just had a thought for an adaptation:

if (humidity > maxHumidity) { humidity = maxHumidity; }
    
mappedVal = map(humidity, desiredHumidity, maxHumidity, 0, 100); 
computedVal = pow(mappedVal, mappedVal / 100);
pwmVal1 = map(computedVal, 0, 100, 0, 255);

digitalWrite(pwmPin_1, pwmVal1);

changes into:

if (humidity > maxHumidity) { humidity = maxHumidity; }
    
mappedVal = map(humidity, desiredHumidity, maxHumidity, 0, 255); 
computedVal = pow(mappedVal, mappedVal / 255);

digitalWrite(pwmPin_1, computedVal);

Is this sound or should I have gone to bed an hour ago?

Two hours ago.

Here is the MultiMap library in action:

// Testing the MultiMap library
// https://github.com/RobTillaart/MultiMap
// This Wokwi project: https://wokwi.com/projects/401812192306752513
//
// A tab between x and y, with a linefeed
// at the end of a line, works for the
// website: https://quickplotter.com/

#include <MultiMap.h>

// input values
float input[6]  = { 0.0, 20.0, 40.0, 60.0, 80.0, 100.0};

// corresponding output values
float output[6] = { 1.0, 2.0, 4.5, 12.0, 33.0, 100.0};

void setup() 
{
  Serial.begin(115200);

  for(float x=0.0; x<=100.0; x+=2.0)
  {
    float y = multiMap<float>(x, input, output, 6);
    Serial.print(x);
    Serial.print("\t");
    Serial.print(y);
    Serial.println();
  }
}

void loop() 
{
  delay(10);
}

Try it in Wokwi: MultiMap library in action - Wokwi ESP32, STM32, Arduino Simulator

This time I tried the website: https://quickplotter.com/

That is very nice.
What happens if I change the sketch to integers ?

I love it when a library does exactly what it is supposed to do.

1 Like

Please note that the multimap allows non-equidistant IN or X values. They only need to be in increasing order.

This allows to have less points where the curve is almost linear and more points where the curve is more dynamic ==> keep the maximum error under control.

So these will work too

// input values
float input[5]  = { 0.0,  40.0, 50.0, 60.0, 100.0};

// corresponding output values
float output[5] = { 1.0, 4.5, 7.0, 12.0, 100.0};

(adjust size in the call)

If you want PWM, this should be analogWrite().

That might be why previous attempts failed to produce a "low" setting for the fans :open_mouth:

I'm implementing analogWrite() and MultiMap with this set:


float input[10] = {1, 10, 20, 30, 40, 50, 60, 70, 80, 90}
float output[10] = {1, 1.5, 3.0, 4.2, 7.5, 12.5, 20, 37.5, 55, 100}

If all goes well I'll let you know this evening!

we use analogWrite() because it can output PWM, the average voltage is:
val÷255×Vcc
whereas val is the PWM value (0-255) and Vcc is the power source voltage.

@LoganTheEngineer7 If the Microsoft Copilot wrote that, can you please remove it ?
It is weird, it makes no sense, it is not to the point and it is wrong. Only AI can produce such an answer.

no, I wrote that, It made sense to me.
let me edit it...

Then I apologize.

The digitalWrite() function does not expect a bool value, but HIGH or LOW.
Timed I/O can be used in this project, but it does not have to.
The calculation is what a teacher does tell, but in practice it is just 0...255 from low output to full on.
Here is the analogWrite() for an Arduino Uno: https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/wiring_analog.c#L96
The PWM value is not a unsigned 8-bits value. It is just an 'int'.