//******************************************************************************************************
// BestValueSelector.cs - Gbtc
//
// Copyright © 2017, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 11/06/2017 - Stephen C. Wills
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using GSF;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
namespace DataQualityMonitoring
{
///
/// Produces a new signal by selecting the best values
/// from a collection of concentrated data points.
///
[Description("Best Value: Produces a new signal by selecting only the best values from a collection of input signals.")]
public class BestValueSelector : ActionAdapterBase
{
#region [ Members ]
// Constants
///
/// Defines the default value for the property.
///
public const bool DefaultPublishBadValues = false;
///
/// Defines the default value for the property.
///
public const MeasurementStateFlags DefaultBadFlags =
MeasurementStateFlags.AlarmHigh |
MeasurementStateFlags.AlarmLow |
MeasurementStateFlags.BadData |
MeasurementStateFlags.CalculationError |
MeasurementStateFlags.ComparisonAlarm |
MeasurementStateFlags.FlatlineAlarm |
MeasurementStateFlags.MeasurementError |
MeasurementStateFlags.OverRangeError |
MeasurementStateFlags.ReceivedAsBad |
MeasurementStateFlags.SuspectData |
MeasurementStateFlags.UnderRangeError;
///
/// Defines the default value for the property.
///
public const bool DefaultHandleZeroAsBad = false;
///
/// Defines the default value for the property.
///
public const bool DefaultHandleNaNAsBad = true;
///
/// Defines the default value for the property.
///
public const bool DefaultSupportsTemporalProcessing = false;
// Fields
private bool m_publishBadValues;
private MeasurementStateFlags m_badFlags;
private bool m_handleZeroAsBad;
private bool m_handleNaNAsBad;
private bool m_supportsTemporalProcessing;
private MeasurementKey m_lastSelectedMeasurement;
#endregion
#region [ Properties ]
///
/// Gets or sets output measurements that the action adapter will produce, if any.
///
[ConnectionStringParameter,
Description("Defines primary keys of output measurements the action adapter expects; can be one of a filter expression, measurement key, point tag or Guid."),
CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")]
public override IMeasurement[] OutputMeasurements
{
get
{
return base.OutputMeasurements;
}
set
{
base.OutputMeasurements = value;
}
}
///
/// Gets or sets the value that determines whether the adapter should
/// publish measurements if all of its inputs are found to be bad.
///
[ConnectionStringParameter,
DefaultValue(DefaultPublishBadValues),
Description("Define the flag that indicates whether this adapter should publish measurements if all inputs are bad.")]
public bool PublishBadValues
{
get
{
return m_publishBadValues;
}
set
{
m_publishBadValues = value;
}
}
///
/// Gets or sets the flags that should be considered bad by this adapter.
///
[ConnectionStringParameter,
Description("Define the flags that are considered bad by this adapter."),
DefaultValue(DefaultBadFlags)]
public MeasurementStateFlags BadFlags
{
get
{
return m_badFlags;
}
set
{
m_badFlags = value;
}
}
///
/// Gets the flag indicating if this adapter considers zero to be a bad value.
///
[ConnectionStringParameter,
Description("Define the flag indicating if this adapter considers zero to be a bad value."),
DefaultValue(DefaultHandleZeroAsBad)]
public bool HandleZeroAsBad
{
get
{
return m_handleZeroAsBad;
}
set
{
m_handleZeroAsBad = value;
}
}
///
/// Gets the flag indicating if this adapter considers not-a-number to be a bad value.
///
[ConnectionStringParameter,
Description("Define the flag indicating if this adapter considers not-a-number to be a bad value."),
DefaultValue(DefaultHandleNaNAsBad)]
public bool HandleNaNAsBad
{
get
{
return m_handleNaNAsBad;
}
set
{
m_handleNaNAsBad = value;
}
}
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
[ConnectionStringParameter,
Description("Define the flag indicating if this adapter supports temporal processing."),
DefaultValue(DefaultSupportsTemporalProcessing)]
public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing;
///
/// Returns the detailed status of the action adapter.
///
public override string Status
{
get
{
StringBuilder statusBuilder = new StringBuilder(base.Status);
IEnumerable badFlags = Enum.GetValues(typeof(MeasurementStateFlags))
.Cast()
.Where(flag => flag != MeasurementStateFlags.Normal && m_badFlags.HasFlag(flag));
string badFlagsText = string.Join("," + Environment.NewLine + " ", badFlags);
statusBuilder.AppendLine();
statusBuilder.AppendLine($" Publish bad values: {(m_publishBadValues ? "Yes" : "No")}");
statusBuilder.AppendLine($" Bad Flags: {badFlagsText}");
statusBuilder.AppendLine($" Handle Zero As Bad: {(m_handleZeroAsBad ? "Yes" : "No")}");
statusBuilder.AppendLine($" Handle NaN As Bad: {(m_handleNaNAsBad ? "Yes" : "No")}");
statusBuilder.AppendLine($"Supports Temporal Processing: {(m_supportsTemporalProcessing ? "Yes" : "No")}");
statusBuilder.AppendLine($" Last Selected Measurement: {m_lastSelectedMeasurement}");
return statusBuilder.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Initializes the .
///
public override void Initialize()
{
base.Initialize();
if (OutputMeasurements?.Length != 1 && OutputMeasurements?.Length != 2)
throw new ArgumentException($"Exactly one or two output measurement must be defined. Amount defined: {OutputMeasurements?.Length ?? 0}");
Dictionary settings = Settings;
string setting;
if (settings.TryGetValue(nameof(PublishBadValues), out setting))
m_publishBadValues = setting.ParseBoolean();
else
m_publishBadValues = DefaultHandleZeroAsBad;
if (!settings.TryGetValue(nameof(BadFlags), out setting) || !Enum.TryParse(setting, out m_badFlags))
m_badFlags = DefaultBadFlags;
if (settings.TryGetValue(nameof(HandleZeroAsBad), out setting))
m_handleZeroAsBad = setting.ParseBoolean();
else
m_handleZeroAsBad = DefaultHandleZeroAsBad;
if (settings.TryGetValue(nameof(HandleNaNAsBad), out setting))
m_handleNaNAsBad = setting.ParseBoolean();
else
m_handleNaNAsBad = DefaultHandleNaNAsBad;
if (settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting))
m_supportsTemporalProcessing = setting.ParseBoolean();
else
m_supportsTemporalProcessing = DefaultSupportsTemporalProcessing;
}
///
/// Publish of time-aligned collection of values that arrived within the
/// concentrator's defined .
///
/// of measurements with the same timestamp that arrived within that are ready for processing.
/// Index of within a second ranging from zero to - 1.
protected override void PublishFrame(IFrame frame, int index)
{
IMeasurement bestMeasurement = GetBestMeasurement(frame);
if ((object)bestMeasurement == null)
return;
IMeasurement[] newMeasurements = OutputMeasurements
.Select(measurement => Measurement.Clone(measurement))
.ToArray();
newMeasurements[0].Timestamp = frame.Timestamp;
newMeasurements[0].StateFlags = bestMeasurement.StateFlags;
newMeasurements[0].Value = bestMeasurement.Value;
if (newMeasurements.Length > 1)
{
newMeasurements[1].Timestamp = frame.Timestamp;
newMeasurements[1].StateFlags = bestMeasurement.StateFlags;
newMeasurements[1].Value = Array.IndexOf(InputMeasurementKeys, bestMeasurement.Key) + 1;
}
OnNewMeasurements(newMeasurements);
m_lastSelectedMeasurement = bestMeasurement.Key;
}
private IMeasurement GetBestMeasurement(IFrame frame)
{
IMeasurement measurement;
if (m_publishBadValues)
{
return InputMeasurementKeys
.Select(key => frame.Measurements.TryGetValue(key, out measurement) ? measurement : null)
.Where(m => (object)m != null)
.OrderBy(m => (m_handleNaNAsBad && double.IsNaN(m.Value)) ? 1 : 0)
.ThenBy(m => (m_handleZeroAsBad && m.Value == 0.0D) ? 1 : 0)
.ThenBy(m => CountFlags(m.StateFlags & m_badFlags))
.FirstOrDefault();
}
return InputMeasurementKeys
.Select(key => frame.Measurements.TryGetValue(key, out measurement) ? measurement : null)
.Where(m => (object)m != null)
.Where(m => !m_handleNaNAsBad || !double.IsNaN(m.Value))
.Where(m => !m_handleZeroAsBad || m.Value != 0.0D)
.Where(m => (m.StateFlags & m_badFlags) == MeasurementStateFlags.Normal)
.FirstOrDefault();
}
private int CountFlags(MeasurementStateFlags flags)
{
if (flags == MeasurementStateFlags.Normal)
return 0;
uint count = (uint)flags;
count = count - ((count >> 1) & 0x55555555u);
count = ((count >> 2) & 0x33333333u) + (count & 0x33333333u);
count = ((count >> 4) + count) & 0x0F0F0F0Fu;
count = ((count >> 8) + count) & 0x00FF00FFu;
count = ((count >> 16) + count) & 0x0000FFFFu;
return (int)count;
}
#endregion
}
}