A Case Study in the Flexibility of Virtual Parameters

The Case

Recently, we were working with a company that had installed a meter to measure the water volume being injected into a well site.

Case Facts

  • This data was brought into OnPing.
  • After a few weeks there seemed to be a variance between what the meter was reporting and what made sense.
  • A problem with the meter was discovered, leading to several things needing to be done.

What OnPing Needed to Do

The part I wanted to write about here was how we used OnPing’s virtual parameters to take existing data and calculate a new real-time metric that could be used to extract information about the pump even though it was not reading right. 

One of the end users suspected their pump was running about half the time and wanted to use that information to allow them to utilize it more fully. They also wanted this information to try and check the expected volume moved by the system. 

Metric Development

What they asked us to do was make a parameter that represented the daily percentage uptime of the tank. The rest of this post will be how that parameter was implemented.

Confirm Run Status

First, if you want to implement a percentage run-time you need to know if the system was running. Luckily, we had a run status being brought in from OnPing. Run status trends often look something like this:

Defining Equipment Parameters

So in our case we get 1’s when the pump is running and 0’s when the pump is off. What we want to know is for any given unit of time, how often were we getting 1’s compared to the total.

So when building scripts I like to work at both ends towards the middle. The last calculation we need will look something like: in (totalNumberOfOnes / totalNumberOfPoints) * 100.0

So we need to develop some way of getting the total number of 1’s. In OnPing’s TachDB historian, values are averaged at resolution so the easiest way to do this permanently is to make the check for greater than 1:

if (v > 0.0) then (Some 1.0) else None

Some and None

You might ask, “What are Some and None“?  These are a special of type in inferno for signifying the existience of a value. We can test each value in a range and report if it was found. Afterwards, we can filter for found values with something like: Array.keepSomes values. From there, we can count the size of that array to get us our total number of points that match our condition. That gives us both numbers to create the  Calculation we were looking for:

in let totalNumberOfOnes = Aray.length (Array.keepSomes values)
in let totalNumberOfPoints = Array.length values

Grabbing Data in a Set Interval

Now, we need to grab points for 24 hours. In this case, inferno offers a function: Time.intervalEvery.

You can hover over A function in inferno to get its type signature: 

Time.intervalEvery : timeDiff → time → time → array of time

This is saying intervalEvery takes 3 values (timeDiff, time, time) and returns an array of time.

Here, timeDiff implies a time interval. The other two times are start of range and end of range respectively. This gives us times we can use in our array processing system to get our values. Putting it together looks like this. 

in let values = [(valueAt runstatus t) | t <- Time.intervalEvery
                                               (Time.seconds 128)
                                                startOfDay
                                                (?now)]

The code above says return Some value at time t, from named input runstatus

Here is the type signature for ‘valueA‘:

TachDB.valueAt : 
  {implicit resolution : resolution} ⇒ series of double → time → option of double

Unpacking a Line of Inferno Script

A lot of stuff to unpack there! First, usually if you see series of double , the function is expecting a named input argument. A named input is a parameter from the OnPing data system. The second argument is time. We generated an array of times earlier and here we are consuming them to produce values at specific times. The implicit resolution : resolution argument is giving the granularity of the approximations used when fetching the data. For now just know that if this isn’t provided explicitly by the programmer, it defaults to whatever OnPing system is calling the value to provide it. Lastly, the output of the function is ‘option of double.’

This is what those Some and None things are called in the type layer. It is saying we can’t be sure that a value exists at the given time for  this data stream. If one doesn’t exist we will return None otherwise we will return (Some a). We will have to deal with this possibility because our system needs to do comparisons against values that exist. Time for a function!

This time, we want to test if the value exists. If it does, then we test if it is the one we want. If not, lets just say it is a 0 and return None.  Here is what that function looks like:

in let validateInput = fun someV ->
          match someV with {
              | None -> None
              | Some v -> if (v > 0.0) then (Some 1.0) else None }

So we are matching against a value that might not be there, taking the v we found and checking if the target is in a running state (>0). 

Resolution

This method would not work if the incoming data was sparse. In that case we would need to use whatever the last value seen was or some other sort of interpolation to get a good value. The script can also adjust the resolution to try and minimize this. In many situations, this works wonderfully.  To do this, rebind the implicit argument like:

in let ?resolution = toResolution 128
in

Now, anything we do in the ‘in‘ part will use a resolution of 128 seconds to average returned values. 

Handling Time

Lastly, we need some input times! Another implicit value defines the time right now, unimaginately called ‘?now ‘. This value trends the script across times in all screens throughout OnPing. 

Subtract times from ?now  to get the start value:

start = ?now - (Time.hours 24)

We have everything we need to write the script. Let’s put it all together!

let start = ?now - (Time.hours 24)
in let ?resolution = toResolution 128
in let validateInput = fun someV ->
            match someV with {
              | None -> None
              | Some v -> if (v > 0.0) then (Some 1.0) else None }

in let values = 
          [validateInput (valueAt runstatus t) | 
            t <- Time.intervalEvery (Time.seconds 128) start (?now)]
in let result = (roundTo 5
                 (((Array.length (Array.keepSomes values)) / 
                  (Array.length values)))) * 100.0
in result
 

 Some of the ‘let’ values defined earlier aren’t explicitly defined in this version. Nonetheless, you can see their values. Also, the result is rounded to 5 decimal places. 

That was a lot of work! Luckily, this function is now publicly available (as DailyPercentageRuntime) so we can apply it anywhere! The official version finds the actual start of the day – using timezone offsets and things which probably deserve a post on their own! 

View the Trends!

When we assign the script to a parameter, we get really cool stuff like accumulating percentage use totals. The chart below trends how much time a plunger is in recovery for a day.

How Inferno Improved the Project

The thing to notice here is how the new metrics applied retroactively and were fully integrated into the visualization systems already deployed. After we make the virtual parameter, everything just happens without additonal User support.

In our example, we see the system is in recovery most of the day on the 22nd and hardly at all on the 23rd. This is a huge benefit of being really strict about making as much as we can from time series enabled data.

Inferno’s precise resolution control is a huge help here. We can find resolutions that greatly improve performance – while remaining true to the base results. The inferno system is just coming online with all this, and we are excited for what is to come.