# Scorecard Metric with Signal Name / Display Signal Name for the Maximum of 3 Signals

## Recommended Posts

• Seeq Team

It is often useful to create a scorecard metric that displays a signal name for use in Organzier Topics. This is relatively simple using Formula and the toSignal() function. This Formula creates a string signal that has a value of "Signal Name" for all of time.

After I have my string signal, I can use a simple scorecard metric with no statistics to create a scorecard that just displays the value of this string. I have changed the header to only display the end time. Now, I can use this scorecard in an auto-updating Organizer Topic that will always show "Signal Name".

This functionality is very useful if you want to create a string signal that has more than one value. For example, say that I have three signals. I want to create a scorecard metric that tells me which of these three signals is the largest at any point in time.

I will start by creating conditions for when each signal is larger than the other two. First, I use Deviation Search to find when Signal 1 > Signal 2 and for when Signal 1 > Signal 3. Then I use Composite Condition (logic: intersection), to find when Signal 1 is max. I repeat this process for Signal 2 - Deviation Search to find Signal 2 > Signal 1 and Signal 2 > Signal 3 + composite condition. Finally, I use Formula to find when Signal 3 is max by using union() and inverse() to find when Signal 1 or 2 are not max : \$1max.union(\$2max).inverse(). Now I have 3 conditions which should cover the whole time series, which are true when each Signal is the maximum of the 3 at any point!

I'm now ready to create the string signal. Just like I did for a single string, I will essentially be creating new signals with toString(), but this time, I will use splice() to splice in the different strings ("1 is max", "2 is max", or "3 is max") when each condition is present. This works because my "X is max" conditions will never overlap. The result is a string signal that equals whatever signal is the maximum at every point in time!

Finally, I'll use Simple Scorecard again to create a metric that displays this Max Signal for use in Organizer Topics.

• 2
##### Share on other sites

• 6 months later...

Hi Nick,

Thanks for this tutorial, I was wondering if there was a way to achieve a similar result using the Max/Min functions?

I have several parameters each with 8 signals and I think doing the above for all of these may be more complicated for me than it needs to be.

For instance, I have 8 different pH results and my current scorecard will display the daily min, average & max value for these 8 signals but I would like to be able to also display which one of these signals was the min and max.

Any help would be great!

Cheers,

Isaac

##### Share on other sites

Hi Isaac,

I found a way by using external Python scripts, which requires you or your administrator to create two files (GetMinIndex.py, GetMaxIndex.py) in the folder "<Seeq datafolder>/mydata/external-calculation/python-scripts/user" on the Seeq server.

GetMinIndex.py:

```from extcalc import KeywiseExternalCalculationScript

class GetMinIndex(KeywiseExternalCalculationScript):

def initialize(self):
pass

def validate(self, validation_object):
"""
# type: (ValidationObject) -> None
"""
signal_types = list(set(validation_object.get_signal_types()))
if (len(signal_types) > 1) or (signal_types[0] != 'NUMERIC'):
raise ValueError('Expecting all signals of type NUMERIC, got signal types: {sigt}'.format(
sigt=signal_types))
else:
pass

def compute(self, key, samples_for_key):
return key, samples_for_key.index(min(samples_for_key))

def compute_output_mode(self):
return 'NUMERIC'

def cleanup(self):
pass
```

GetMaxIndex.py:

```from extcalc import KeywiseExternalCalculationScript

class GetMaxIndex(KeywiseExternalCalculationScript):

def initialize(self):
pass

def validate(self, validation_object):
"""
# type: (ValidationObject) -> None
"""

signal_types = list(set(validation_object.get_signal_types()))
if (len(signal_types) > 1) or (signal_types[0] != 'NUMERIC'):
raise ValueError('Expecting all signals of type NUMERIC, got signal types: {sigt}'.format(
sigt=signal_types))
else:
pass

def compute(self, key, samples_for_key):
return key, samples_for_key.index(max(samples_for_key))

def compute_output_mode(self):
return 'NUMERIC'

def cleanup(self):
pass
```

In the next step I used the following formula inside Seeq to determine the min / max values per day for each of the signals and store the name of the signal to the property. Please note that the order of the signals added to the group must match the order of the signals used by the external calculation. If more signals need to be compared you have to extend the formula. The name of the script to call can be found by clicking the connected datasources:

```//Create daily condition
\$days = days()

//Calculate Max for each signal
\$aMax = \$a.aggregate(maxValue(), \$days, durationKey())
\$bMax = \$b.aggregate(maxValue(), \$days, durationKey())
\$gMax = \$g.aggregate(maxValue(), \$days, durationKey())
\$hMax = \$h.aggregate(maxValue(), \$days, durationKey())

//Calculate Min for each signal
\$aMin = \$a.aggregate(minValue(), \$days, durationKey())
\$bMin = \$b.aggregate(minValue(), \$days, durationKey())
\$gMin = \$g.aggregate(minValue(), \$days, durationKey())
\$hMin = \$h.aggregate(minValue(), \$days, durationKey())

//Create group with name of signals
\$signals = group('Area A','Area B','Area G','Area H')

//set properties for each capsule
\$days.transform(\$caps -> {
//Get index of which signal is max
\$idxMax = externalCalculation('GetMaxIndex.py:NUMERIC:C21fV0DJEK6c', \$aMax, \$bMax, \$gMax, \$hMax).getValue(\$caps.getStart())
//Get index of which signal is min
\$idxMin = externalCalculation('GetMinIndex.py:NUMERIC:t/CkhyqKBgTv', \$aMin, \$bMin, \$gMin, \$hMin).getValue(\$caps.getStart())

//Get value from group based on index and set as property
\$caps.setProperty('MinSignalName', \$signals.pick(\$idxMin.toNumber().ceiling()+1))
.setProperty('MaxSignalName', \$signals.pick(\$idxMax.toNumber().ceiling()+1))
})
```

Next, I used toSignal() to create two signals from the properties:

```//Extract property of capsule
\$dwp.toSignal('MaxSignalName').toStep(1d)```
```//Extract property of capsule
\$dwp.toSignal('MinSignalName').toStep(1d)```

In trendview you can now see the two signals:

Last step is building the scorecard:

Another approach would be using Seeq Data Labs to calculate the values and push them back to Seeq.

Hope this helps.

Regards,

Thorsten

##### Share on other sites

Thanks for your time and info Thorsten,

Unfortunately I do not think I have access to Python on my company profile, I have put a request in however.

Just to clarify, is all this required to achieve what Nick originally did in the initial post but with 8 signals? Can I not use the formula version of Deviation Search and increase the number of boundaries from 1 to 7 for example?

Many thanks,

Isaac

##### Share on other sites

Hi Isaac,

don't know why I did not came across this solution first, which is fairly the easiest to achive I think:

You can use boolean operators for this.

Determine Max Signal:

```\$days = days()

//Calculate maximum per day
\$aMax = \$a.aggregate(maxValue(), \$days, durationKey())
\$bMax = \$b.aggregate(maxValue(), \$days, durationKey())
\$gMax = \$g.aggregate(maxValue(), \$days, durationKey())
\$hMax = \$h.aggregate(maxValue(), \$days, durationKey())

//Which is max?
\$aIsMax = \$aMax > \$bMax && \$aMax > \$gMax && \$aMax > \$hMax
\$bIsMax = \$bMax > \$aMax && \$bMax > \$gMax && \$bMax > \$hMax
\$gIsMax = \$gMax > \$aMax && \$gMax > \$bMax && \$gMax > \$hMax
\$hIsMax = \$hMax > \$bMax && \$hMax > \$gMax && \$hMax > \$gMax

//Generate signal
"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMax)
.splice("Area B".toSignal(1d), \$bIsMax)
.splice("Area G".toSignal(1d), \$gIsMax)
.splice("Area H".toSignal(1d), \$hIsMax)```

Determine Min Signal:

```\$days = days()

//Caulate minimum per day
\$aMin = \$a.aggregate(minValue(), \$days, durationKey())
\$bMin = \$b.aggregate(minValue(), \$days, durationKey())
\$gMin = \$g.aggregate(minValue(), \$days, durationKey())
\$hMin = \$h.aggregate(minValue(), \$days, durationKey())

//Which is min?
\$aIsMin = \$aMin < \$bMin && \$aMin < \$gMin && \$aMin < \$hMin
\$bIsMin = \$bMin < \$aMin && \$bMin < \$gMin && \$bMin < \$hMin
\$gIsMin = \$gMin < \$aMin && \$gMin < \$bMin && \$gMin < \$hMin
\$hIsMin = \$hMin < \$bMin && \$hMin < \$gMin && \$hMin < \$gMin

//Generate Signal
"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMin)
.splice("Area B".toSignal(1d), \$bIsMin)
.splice("Area G".toSignal(1d), \$gIsMin)
.splice("Area H".toSignal(1d), \$hIsMin)```

Regards,

Thorsten

##### Share on other sites

Hi Thorsten,

Thanks again, that looks like a winner!

Only problem is we are still on R21.0.42.03 and I believe the use of those math operators are a feature of R21.044 and higher.

Are you aware of how to do the above formula in previous versions of Seeq?

##### Share on other sites

Hi Isaac,

you may try this instead:

Max:

```\$days = days()
\$aMax = \$a.aggregate(maxValue(), \$days, durationKey())
\$bMax = \$b.aggregate(maxValue(), \$days, durationKey())
\$gMax = \$g.aggregate(maxValue(), \$days, durationKey())
\$hMax = \$h.aggregate(maxValue(), \$days, durationKey())

\$aIsMax = \$aMax.isGreaterThan(\$bMax).intersect(\$aMax.isGreaterThan(\$gMax)).intersect(\$aMax.isGreaterThan(\$hMax))
\$bIsMax = \$bMax.isGreaterThan(\$aMax).intersect(\$bMax.isGreaterThan(\$gMax)).intersect(\$bMax.isGreaterThan(\$hMax))
\$gIsMax = \$gMax.isGreaterThan(\$aMax).intersect(\$gMax.isGreaterThan(\$bMax)).intersect(\$gMax.isGreaterThan(\$hMax))
\$hIsMax = \$hMax.isGreaterThan(\$aMax).intersect(\$hMax.isGreaterThan(\$bMax)).intersect(\$hMax.isGreaterThan(\$gMax))

"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMax)
.splice("Area B".toSignal(1d), \$bIsMax)
.splice("Area G".toSignal(1d), \$gIsMax)
.splice("Area H".toSignal(1d), \$hIsMax)```

Min:

```\$days = days()
\$aMin = \$a.aggregate(minValue(), \$days, durationKey())
\$bMin = \$b.aggregate(minValue(), \$days, durationKey())
\$gMin = \$g.aggregate(minValue(), \$days, durationKey())
\$hMin = \$h.aggregate(minValue(), \$days, durationKey())

\$aIsMin = \$aMin.isLessThan(\$bMin).intersect(\$aMin.isLessThan(\$gMin)).intersect(\$aMin.isLessThan(\$hMin))
\$bIsMin = \$bMin.isLessThan(\$aMin).intersect(\$bMin.isLessThan(\$gMin)).intersect(\$bMin.isLessThan(\$hMin))
\$gIsMin = \$gMin.isLessThan(\$aMin).intersect(\$gMin.isLessThan(\$bMin)).intersect(\$gMin.isLessThan(\$hMin))
\$hIsMin = \$hMin.isLessThan(\$aMin).intersect(\$hMin.isLessThan(\$bMin)).intersect(\$hMin.isLessThan(\$gMin))

"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMin)
.splice("Area B".toSignal(1d), \$bIsMin)
.splice("Area G".toSignal(1d), \$gIsMin)
.splice("Area H".toSignal(1d), \$hIsMin)```

Regards,

Thorsten

##### Share on other sites

Hi Thorsten,

Unfortunately it appears that the isGreaterThan function only works for scalars. Perhaps I need to try and get our version of Seeq updated!

Thanks for your help,

Isaac

##### Share on other sites

• 3 weeks later...
• Seeq Team

Hi Isaac,

On version R21.0.43, you may still have to include the valuesearch(isgreaterthan(x)) syntax in your condition formulas. For your last comment, regarding isgreaterthan functions only working for scalars, the best way to handle this would be to take the difference between signals and compare that to zero, for example A is greater than B if (A-B)>0.

This slight modification of Thorsten's method may work for you:

Max:

```\$days = days()
\$aMax = \$a.aggregate(maxValue(), \$days, durationKey())
\$bMax = \$b.aggregate(maxValue(), \$days, durationKey())
\$gMax = \$g.aggregate(maxValue(), \$days, durationKey())
\$hMax = \$h.aggregate(maxValue(), \$days, durationKey())

\$aIsMax = (\$aMax-\$bMax).ValueSearch(isGreaterThan(0)).intersect((\$aMax-\$gMax).valueSearch(isGreaterThan(0))).intersect((\$aMax-\$hMax).valueSearch(isGreaterThan(0)))
\$bIsMax = (\$bMax-\$aMax).ValueSearch(isGreaterThan(0)).intersect((\$bMax-\$gMax).valueSearch(isGreaterThan(0))).intersect((\$bMax-\$hMax).valueSearch(isGreaterThan(0)))
\$gIsMax = (\$gMax-\$bMax).ValueSearch(isGreaterThan(0)).intersect((\$gMax-\$aMax).valueSearch(isGreaterThan(0))).intersect((\$gMax-\$hMax).valueSearch(isGreaterThan(0)))
\$hIsMax = (\$hMax-\$aMax).ValueSearch(isGreaterThan(0)).intersect((\$hMax-\$bMax).valueSearch(isGreaterThan(0))).intersect((\$hMax-\$gMax).valueSearch(isGreaterThan(0)))

"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMax)
.splice("Area B".toSignal(1d), \$bIsMax)
.splice("Area G".toSignal(1d), \$gIsMax)
.splice("Area H".toSignal(1d), \$hIsMax)```

Min:

```\$days = days()
\$aMin = \$a.aggregate(minValue(), \$days, durationKey())
\$bMin = \$b.aggregate(minValue(), \$days, durationKey())
\$gMin = \$g.aggregate(minValue(), \$days, durationKey())
\$hMin = \$h.aggregate(minValue(), \$days, durationKey())

\$aIsMin = (\$aMin-\$bMin).ValueSearch(isLessThan(0)).intersect((\$aMin-\$gMin).valueSearch(isLessThan(0))).intersect((\$aMin-\$hMin).valueSearch(isLessThan(0)))
\$bIsMin = (\$bMin-\$aMin).ValueSearch(isLessThan(0)).intersect((\$bMin-\$gMin).valueSearch(isLessThan(0))).intersect((\$bMin-\$hMin).valueSearch(isLessThan(0)))
\$gIsMin = (\$gMin-\$bMin).ValueSearch(isLessThan(0)).intersect((\$gMin-\$aMin).valueSearch(isLessThan(0))).intersect((\$gMin-\$hMin).valueSearch(isLessThan(0)))
\$hIsMin = (\$hMin-\$aMin).ValueSearch(isLessThan(0)).intersect((\$hMin-\$bMin).valueSearch(isLessThan(0))).intersect((\$hMin-\$gMin).valueSearch(isLessThan(0)))

"".toSignal(1d)
.splice("Area A".toSignal(1d), \$aIsMin)
.splice("Area B".toSignal(1d), \$bIsMin)
.splice("Area G".toSignal(1d), \$gIsMin)
.splice("Area H".toSignal(1d), \$hIsMin)```

Thanks,

Allison

• 2

## Create an account or sign in to comment

You need to be a member in order to leave a comment

## Create an account

Sign up for a new account in our community. It's easy!

Register a new account