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.