Analyze Measures and SPC
Let the Coolant Temperature measure run for at least 15 minutes before starting, so there are enough records to analyze.
- Navigate to Ops → Analysis → Measures.
- Select the
Coolant Temperaturemeasure for Machine 1, choose today, set granularity to Hour, and click Go. - Review the four summary cards: Avg Measure Value should sit near 22, and the Total Samples card should show about 60 samples per event.
- In Measure Value with Rolling Average, find the slow 30-minute swing you built into the simulated signal.
- Check the Measure Value Distribution histogram. It should be centered on 22.
The two tables at the bottom help you trust the data before you report on it:
- Outliers (z-score > 2) lists windows whose value was unusually far from the mean, largest first.
- Gap Analysis flags windows with a larger-than-normal gap since the previous record, which usually means collection stopped: the tag was disabled, the gateway restarted, or the source went bad.
- Copy the following script and execute it in the Script Console.
measureId = system.kanoa.utilities.getFieldValue('measureId', system.kanoa.measures.getMeasures({'measureName': 'Coolant Temperature'}))
end = system.date.now()
start = system.date.addHours(end, -1)
summary = system.kanoa.utilities.convertDatasetToJSON(system.kanoa.measures.getMeasureSummary({'measureId': measureId, 'tStampStart': start, 'tStampEnd': end}))
for row in summary:
for k,v in row.items():
print k,v
SPC Checks
With a Kanoa Quality license, a measure can be checked against statistical process control rules every time a window is written, instead of waiting for someone to look at a chart.
-
In Configuration → Quality → Attributes, create an attribute named
Coolant Temperature. -
Click View Attribute Settings and, on the Tolerances tab, add a tolerance with the asset set to Machine 1 and the item left blank, so it applies to every item on Machine 1:
LSL LCL Target UCL USL 18 20 22 24 26 -
In Configuration → Ops → Measures, link the attribute to the
Coolant Temperaturemeasure and save. -
On the measure tag, trigger
spec/refreshto load the new limits, then confirm thespecfolder shows them. -
On the measure tag, set
spcCheckEnabledtotrue. -
Push the process out of control: change the
Coolant Tempexpression's base value from22to25for a few minutes. -
Watch
outOfControlturntrueandruleViolationslist the rules that fired, then set the base value back to22.
Out-of-control conditions also appear in the notifications widget, so you can put an alert on an operator screen or dashboard for the line.
The rule set (western_electric, nelson, or both) and the chart type (individuals for single readings, xbar for subgroups) are described in the Measures reference. Use individuals for a sensor value like this one.
Copy this script into the Ignition Designer Script Console (Tools → Script Console) and run it after the SPC steps. It checks there are enough Coolant Temperature records to analyze, the Coolant Temperature attribute and its Machine 1 tolerance, the link from the measure, the limits in the measure tag's spec folder, that spcCheckEnabled is on, that SPC flagged the measure out of control, and that Coolant Temp is back near 22. Each check prints PASS or FAIL with what to fix. It doesn't change anything.
Show the check script
# Kanoa Ops Training - check "Analyze Measures and SPC" (Measures, lesson 2)
#
# Run in the Ignition Designer Script Console (Tools > Script Console), in the
# project that inherits KANOA_SCRIPTS. It only reads; it changes nothing.
# Every check prints PASS or FAIL with what to fix, then a summary.
ASSET = 'CoreForge\\Philadelphia\\Machining\\Machine 1'
MEASURE = 'Coolant Temperature'
ATTRIBUTE = 'Coolant Temperature'
SIM_TAG = '[Kanoa]CoreForge/Philadelphia/Machining/Machine 1/Coolant Temp'
LIMITS = [('lsl', 18), ('lcl', 20), ('tgt', 22), ('ucl', 24), ('usl', 26)]
MIN_RECORDS = 15
LOOKBACK_HOURS = 24
results = []
def check(ok, what, fix=None):
results.append(bool(ok))
if ok:
print 'PASS %s' % what
else:
print 'FAIL %s' % what
if fix:
print ' -> %s' % fix
def note(what):
print 'NOTE %s' % what
def field(row, *names):
for n in names:
if n in row and row[n] is not None:
return row[n]
return None
def text(v):
return '' if v is None else unicode(v).strip()
def key(v):
return ' '.join(text(v).lower().split())
def truthy(v):
return str(v).lower() in ('true', '1')
def num(v):
try:
return float(v)
except:
return None
def rows(data):
# Kanoa returns a dataset, a list of dictionaries, or None
if data is None:
return []
if hasattr(data, 'getRowCount'):
cols = list(data.getColumnNames())
return [dict((c, data.getValueAt(i, c)) for c in cols) for i in range(data.getRowCount())]
return [dict(r) for r in data]
def exists(path):
try:
return bool(path) and system.tag.exists(path)
except:
return False
def readTag(path):
"""A member tag's value, or a UDT parameter's value, or None."""
qv = system.tag.readBlocking([path])[0]
if not qv.quality.isGood() and '/' in path:
folder, name = path.rsplit('/', 1)
qv = system.tag.readBlocking([folder + '/Parameters.' + name])[0]
return qv.value if qv.quality.isGood() else None
print '=' * 70
print 'Kanoa Ops Training - Analyze Measures and SPC check'
print '=' * 70
# The measure from the previous lesson
measures = rows(system.kanoa.measures.getMeasures({'assetPath': ASSET}))
measure = ([m for m in measures if key(field(m, 'measureName')) == key(MEASURE)] or [None])[0]
if measure is None:
check(False, '%s measure exists on Machine 1' % MEASURE, 'Complete the Configure a Measure lesson first.')
measureId = None
else:
measureId = field(measure, 'measureId')
# 1. Enough data to analyze
if measureId is not None:
print '\n-- Measure records (last %d hours)' % LOOKBACK_HOURS
end = system.date.now()
start = system.date.addHours(end, -LOOKBACK_HOURS)
try:
events = rows(system.kanoa.measures.getMeasureEvents({'measureId': measureId, 'tStampStart': start, 'tStampEnd': end}))
except:
events = []
check(len(events) >= MIN_RECORDS, '%s has at least %d records (%d)' % (MEASURE, MIN_RECORDS, len(events)),
'Let the measure run for at least 15 minutes (one record a minute), then run this again. '
'If the count is not growing, run the Configure a Measure check.')
values = [num(field(e, 'mean', 'measureValue', 'value')) for e in events]
values = [v for v in values if v is not None]
samples = [num(field(e, 'sampleCount')) for e in events]
samples = [s for s in samples if s is not None]
if values:
avg = sum(values) / len(values)
check(20.0 <= avg <= 24.0, 'Average value is near 22 (%.2f)' % avg,
'Check the Coolant Temp expression is back to a base value of 22.')
if samples:
note('About %.0f samples per record (expect about 60)' % (sum(samples) / len(samples)))
try:
summary = rows(system.kanoa.measures.getMeasureSummary({'measureId': measureId, 'tStampStart': start, 'tStampEnd': end}))
except:
summary = []
check(summary, 'getMeasureSummary returns a summary for %s' % MEASURE,
'No summary came back. Once there are records, the View Measure Records script should print one.')
# 2. SPC: the quality attribute and its tolerance
print '\n-- SPC attribute and limits'
try:
attributes = rows(system.kanoa.quality.config.getAttributes({'attributeName': ATTRIBUTE}))
quality = True
except:
attributes, quality = [], False
note("Couldn't read quality attributes. The SPC part of this lesson needs a Kanoa Quality license.")
attribute = ([a for a in attributes if key(field(a, 'attributeName')) == key(ATTRIBUTE)] or [None])[0]
attributeId = None
if quality:
if attribute is None:
check(False, '%s attribute exists' % ATTRIBUTE, 'In Configuration > Quality > Attributes, create an attribute named %s.' % ATTRIBUTE)
else:
attributeId = field(attribute, 'attributeId')
actual = text(field(attribute, 'attributeName'))
check(actual == ATTRIBUTE, '%s attribute exists' % ATTRIBUTE, "Found '%s'. Rename it to %s." % (actual, ATTRIBUTE))
if attributeId is not None:
assetId = system.kanoa.utilities.getFieldValue('assetId', system.kanoa.asset.getAssets({'assetPath': ASSET}))
tols = rows(system.kanoa.quality.config.getTolerances({'attributeId': attributeId}))
def blank(v):
return text(v) in ('', '0', '-1', 'None')
def forMachine1(t):
if not blank(field(t, 'assetId')):
return text(field(t, 'assetId')) == text(assetId)
name = key(field(t, 'assetPath', 'assetName'))
return name in (key(ASSET), key(ASSET.split('\\')[-1]))
mine = [t for t in tols if forMachine1(t) and blank(field(t, 'itemId'))]
check(mine, '%s has a tolerance for Machine 1' % ATTRIBUTE,
'Open View Attribute Settings > Tolerances and add one with the asset set to Machine 1. Leave the item blank.')
if not mine:
for t in tols:
note('Found a tolerance with: %s' % ', '.join('%s=%s' % (k, text(t[k])) for k in sorted(t.keys())
if 'asset' in k.lower() or 'item' in k.lower()))
if mine:
t = mine[0]
wrong = ['%s %s (expected %g)' % (k.upper(), text(field(t, k)) or 'blank', v) for k, v in LIMITS if num(field(t, k)) != v]
check(not wrong, 'Tolerance limits are 18 / 20 / 22 / 24 / 26', 'Edit the tolerance: %s.' % ', '.join(wrong))
if measure is not None:
linked = field(measure, 'attributeId')
check(text(linked) == text(attributeId), '%s measure is linked to the %s attribute' % (MEASURE, ATTRIBUTE),
'In Configuration > Ops > Measures, edit %s, set its attribute to %s and save.' % (MEASURE, ATTRIBUTE))
# 3. SPC on the measure tag
udt = None
if measure is not None and quality:
print '\n-- SPC on the measure tag'
try:
base = text(system.kanoa.asset.getAssetTagPath({'assetPath': ASSET}))
except:
base = ''
udt = base + '/measures/' + text(field(measure, 'measureName'))
if not (base and exists(udt)):
check(False, '%s measure tag exists' % MEASURE, 'Complete the Configure a Measure lesson first.')
udt = None
if udt:
specId = readTag(udt + '/spec/attributeId')
if attributeId is not None:
check(text(specId) == text(attributeId), 'spec/attributeId is the %s attribute' % ATTRIBUTE,
"It's %s. Link the attribute to the measure, then trigger spec/refresh." % (text(specId) or 'blank'))
wrong = []
for k, v in LIMITS:
got = num(readTag(udt + '/spec/' + k))
if got != v:
wrong.append('%s %s' % (k, '%g' % got if got is not None else 'blank'))
check(not wrong, 'The spec folder shows the limits 18 / 20 / 22 / 24 / 26',
'The spec folder has %s. Trigger spec/refresh on the measure tag to load the limits.' % ', '.join(wrong))
check(truthy(readTag(udt + '/spcCheckEnabled')), 'spcCheckEnabled is true', 'Set spcCheckEnabled on the measure tag to true.')
# 4. The out-of-control test
print '\n-- Out-of-control test'
# the tag shows the latest window; records show whether any window was flagged
tagFlag = truthy(readTag(udt + '/outOfControl'))
raw = readTag(udt + '/ruleViolations')
if raw is None or isinstance(raw, basestring):
raw = []
violations = [text(v) for v in raw if v is not None and text(v)]
flagged = [e for e in events if truthy(field(e, 'outOfControl', 'isOutOfControl', 'ooc'))]
check(tagFlag or violations or flagged, 'SPC has flagged the measure out of control',
'Change the Coolant Temp expression base value from 22 to 25 for a few minutes and watch outOfControl and '
'ruleViolations on the measure tag, then set it back to 22.')
if tagFlag:
note('outOfControl is true on the measure tag right now')
if violations:
note('Rules that fired: %s' % ', '.join(violations))
if flagged:
note('%d record(s) in the last %d hours were flagged out of control' % (len(flagged), LOOKBACK_HOURS))
current = num(readTag(SIM_TAG)) if exists(SIM_TAG) else None
if current is not None:
check(current < 24.0, 'Coolant Temp is back near 22 (%.2f)' % current,
'Edit the Coolant Temp expression and set the base value back to 22.')
passed = results.count(True)
print '\n' + '=' * 70
print 'Result: %d of %d checks passed' % (passed, len(results))
if passed == len(results):
print 'All good. Your measure is analyzed and SPC is running.'
else:
print 'Fix the FAIL items above, then run this again.'
print '=' * 70