Math Problem

Hi guys...

I'm working on an arduino project that will serve as a lighting controller for aquarium lights. The concept is that the user will enter a start time, a duration, and a maximum light level (in percent). From there, a parabolic function is created such that the x-axis represents the number of minutes past the start time. The y-axis represents the light level in percent.

The formula for the parabolic curve is: f(x) = -((kx^2)/h^2) + (2kx)/h
where (h,k) is the vertex.
The y-intercept is 0, and the x-intercepts are 0 and 2h.

I am certain that this formula is correct, as I have verified it both using WolframAlpha, and recreating the formula in Excel and graphing the function results (spreadsheet attached).

However, the results I get from the below code are incorrect. What am I missing? Any help is greatly appreciated!

This project uses a ZS-042 DS3231 RCT module to set the time and save it when the power is disconnected.

// DS3232RTC - Version: Latest 
#include <DS3232RTC.h>

struct   ChParams {
  time_t StartTime;
  byte   DurHour;
  byte   DurMin;
  byte   Max;
};

         byte     OldMinutes;
         ChParams Ch1 = {28800,14,0,60}; //28800 seconds = 8am Start Time
         byte     Ch1Lvl = 0;

void setup() {

  Serial.begin(9600);
  setSyncProvider(RTC.get);   // the function to get the time from the RTC
  if(timeStatus() != timeSet)
      Serial.println("Unable to sync with the RTC");
  else
      Serial.println("RTC has set the system time");

  OldMinutes = minute();      
  Ch1Lvl = UpdateChLvl(Ch1,now());
}

void loop() {

  if (OldMinutes != minute()) {
    time_t CurrTime = now();

//for debugging. Remove for final.
    Serial.println("");    
    Serial.println("Curr Time: " + PrintDateTime(CurrTime,2));
    Serial.println("");
    
    Ch1Lvl = UpdateChLvl(Ch1,CurrTime);
    
    OldMinutes = minute();
  }
}

byte UpdateChLvl(ChParams c, time_t t) {
  
  //the formula for the curve is:
  // y = -((kx^2)/h^2) + (2kx)/h
  //where (h,k) is the vertex
  
  int    ChMins;
  int    DurMins = (c.DurHour*60) + c.DurMin;
  float  h = DurMins/2; //this is the x value of the vertex
                        //the y value of the vertex is c.Max
  double Level;         

  //this is the x value to feed into the formula
  ChMins = (floor(elapsedSecsToday(t)/60)) - (floor(elapsedSecsToday(c.StartTime)/60));
  
  Level = (static_cast< double >(-1) * (static_cast< double >(c.Max*ChMins*ChMins) / static_cast< double >(h*h))) + (static_cast< double >(2*c.Max*ChMins) / static_cast< double >(h));
  
  if (Level < 0) {
    Level = 0;
  }

//for debugging. remove for final.  
  Serial.println("Ch Start: " + PrintDateTime(c.StartTime,2));
  Serial.println("Ch Dur: " + String(c.DurHour) + "h" + String(c.DurMin) + "m (" + String(DurMins) + ")");
  Serial.println("Ch Max: " + String(c.Max));
  Serial.println("Mins Elapsed Today: " + String(floor(elapsedSecsToday(t)/60)));
  Serial.println("Ch Start Minutes: " + String(floor(elapsedSecsToday(c.StartTime)/60)));
  Serial.println("Minute Diff (x value): " + String(ChMins));
  Serial.println("Y value of vertex (h): " + String(h));
  Serial.println("Level (output): " + String(Level));
  Serial.println("");
  
  return(round(Level));
}

TankLightingFormula.zip (13.8 KB)

Perhaps this is explained somewhere in the ZIP file, but I'm not willing to open it to find out. What is this line supposed to do?

 Level = (static_cast< double >(-1) * (static_cast< double >(c.Max*ChMins*ChMins) / static_cast< double >(h*h))) + (static_cast< double >(2*c.Max*ChMins) / static_cast< double >(h));

My guess is that static_cast is some kind of macro, but I don't know. If it's to convert -1 to a double, if for no other reason than documentation, I would change -1 to -1.0.

tko1982:
The formula for the parabolic curve is: f(x) = -((kx^2)/h^2) + (2kx)/h

Hello,

Should the trendline be with this same equation?

Here appeared this equation:

y = -0.0003x² + 0.2854x - 0.0754

I see you edited your original post but have not responded so I'm gong to add a tiny bit of new material and strongly reemphasize @econjack's post...

  float  h = DurMins/2; //this is the x value of the vertex

h will always be an integer value; the value will never include a 0.5 fraction.

static_cast< double >(-1)

Seriously? Why would you do that?

I tried to compile the code but the IDE returned this error below :frowning:

(Maybe the code needs some more checks)

'Ch1Lvl' was not declared in this scope

c.Max*ChMins*ChMins
2*c.Max*ChMins

Either or both of the above can cause integer overflow.

Try this:

 Level = -1.0 * c.Max*ChMins*ChMins / (h*h) + 2.0*c.Max*ChMins/h;

Pete

Thanks for all the replies. To answer a few questions:

Seriously? Why would you do that?

Because a programmer friend told me that sometimes when you do certain types of math, the data types can influence the result. I admit that I didn't do a lot of reading on it, but what I found seemed to indicate that he was correct, and therefore I made an effort to convert every element of my equation to a double. I can tell by your response that this was clearly not the right thing to do.

I tried to compile the code but the IDE returned this error below

That's my mistake. I tried to include only the relevant parts of my code, and I forgot to include the global declaration of Ch1Lvl. I will modify my original post to include the declaration.

Either or both of the above can cause integer overflow.

Pete, am I understanding correctly that the change you've made here (aside from removing the "static cast" portions is simply to add a ".0" to each integer in the formula?

Moderator edit: quote tags corrected

Yes. The constants then become floating point instead of integer and that forces the remainder of that portion of the calculation be done as floating point which won't overflow.

Pete

Thank you... I'll try that out when I get home this evening and see how it goes. I'll be sure to post back here either way.

Thanks for your help!

tko1982:
I can tell by your response that this was clearly not the right thing to do.

This is a far better choice...

tko1982:
Pete, am I understanding correctly that the change you've made here (aside from removing the "static cast" portions is simply to add a ".0" to each integer in the formula?

Essentially, it boils down to using the correct datatype. -1 is an int which is not the correct datatype. -1.0 is a double which is the correct datatype.

A suffix would have sufficed. -1f is a float.

A typed constant is often a good choice...

const double MinusOne = -1;

Thanks... that makes perfect sense. I'm sure it goes without saying that I'm not super familiar with C++, and am relying on google to help me with syntax. I appreciate your willingness to explain it to me!

But casting constants that way is a bit silly, except perhaps deep inside macros or templates.

A suffix would have sufficed. -1f is a float.

"-1.0" is also a float (well, actually it's a double. Same thing on an Uno.)

That solution seems to have done the trick. Thank you all kindly for your help!

I would do the math almost directly as it was written:

byte UpdateChLvl(ChParams c, time_t t)
{
  unsigned long    DurMins = (c.DurHour * 60) + c.DurMin;
  float h = DurMins / 2;
  float k = c.Max;

  // If the StartTime has not been reached, lights off.
  if (t < c.StartTime)
    return 0;

  // If the interval is over, lights off
  if (t > (c.StartTime + (DurMins * 60)))
    return 0;

  float x = (t - c.StartTime) / 60;  // Minutes into the DurMins interval

  // Calculate the function at time x (minutes since c.StartTime)
  // The formula for the curve is:
  // f(x) = -((k x^2) / h^2) + (2kx) / h
  // h is half the total interval in minutes
  // k is height of the vertex (maximum brightness, c.Max)
  byte Level = -((k * x * x) / (h * h)) + (2 * k * x) / h;

  return Level;
}
  float h = DurMins / 2.0;

:wink:

A typed constant is often a good choice...
const double MinusOne = -1;

A better choice, IMHO, would be:

const double MinusOne = -1.0;

Make the initializer match the type being initialized.

Yes, I know that the compiler will do an implicit cast, to make -1 into -1.0, but why not make it clear that you are paying attention to what you are initializing?

Excellent point and observation.

I actually did that on purpose to demonstrate that (within reasonable limits) no matter what is on the right it becomes the desired datatype on the left.