/* ======================================================================== * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Text; namespace Opc.Ua.Aggregates { /// /// All aggregators implement this interface. It describes the relationship between the /// aggregator and any TimeSlice instances processed by it. /// public interface IAggregator { /// /// Compute a processed value from raw values in a slice of time. /// /// /// /// /// DataValue Compute(IAggregationContext context, TimeSlice bucket, AggregateState state); /// /// Determine whether there is sufficient data in a TimeSlice with respect to the /// AggregateState to permit reliable computation of a processed value. This decision /// is largely governed by the requirements for interpolation or extrapolation. /// /// /// /// bool WaitForMoreData(TimeSlice bucket, AggregateState state); /// /// Take snapshot data from the AggregationState in order to determine bounding values /// for the TimeSlice. /// /// /// void UpdateBoundingValues(TimeSlice bucket, AggregateState state); } /// /// An interface that allows the basic information about an aggregate query to be /// communicated /// public interface IAggregationContext { /// /// The start of the time window we are aggregating over. Note this may be later /// than the EndTime. /// DateTime StartTime { get; } /// /// The end time of the window we are aggregating over, if known. Note this may be /// earlier than the StartTime. /// DateTime EndTime { get; } /// /// The size (in milliseconds) of each sampling interval in the time window. If this /// is zero, then the entire window is treated as one sampling interval. /// double ProcessingInterval { get; } /// /// Indicates that the time window for aggregation has a start time later than its /// end time, and that raw data will be presented in reverse order. /// This value is computed from StartTime and EndTime, however EndTime will be null /// if the aggregation is used as a filter in a subscription. /// bool IsReverseAggregation { get; } /// /// The maximum percentage of points in a sampling interval that may be bad for /// the processed value to have a non-bad status /// byte PercentDataBad { get; } /// /// The minimum percentage of points in a sampling interval that must be good /// for the processed value to have a good status /// byte PercentDataGood { get; } /// /// Indicator thet determines whether stepped or sloped extrapolation should /// be used /// bool UseSlopedExtrapolation { get; } /// /// Indicator that determines whether stepped or sloped interpolation should /// be used /// bool SteppedVariable { get; } /// /// Indicates that raw data points with status Uncertain should be handled as if they /// were bad points rather than as good points. /// bool TreatUncertainAsBad { get; } } /// /// An interface that allows new processed data points to be generated as a response to /// new raw data /// public interface IAggregationActor { /// /// Causes the derivation of 0 or more new processed data points from the given raw /// data point and the current state of the aggregator. /// /// /// void UpdateProcessedData(DataValue rawValue, AggregateState state); /// /// Allows those processed data points already derived to be released to the outside /// world. /// /// IList ProcessedValues(); } /// /// An interface that captures the original active API of the AggregateCalculator class /// required to integrate with the subscription code. /// public interface IAggregateCalculator { /// /// Processes an incoming value. /// /// /// Returns a set of processed data values if any intervals are complete. /// IList ProcessValue(DataValue value, ServiceResult result); /// /// Processes the fact that there is no more data. /// /// /// Returns a set of processed data values if any intervals is remain to be processed. /// IList ProcessTermination(ServiceResult result); } /// /// Coordinates aggregation over a time series of raw data points to yield a time series of processed data points. /// public abstract class AggregateCalculatorImpl : IAggregateCalculator, IAggregationContext, IAggregationActor, IAggregator { #region IAggregationContext Members /// /// The start time. /// public DateTime StartTime { get; set; } /// /// The end time. /// public DateTime EndTime { get; set; } /// /// Whether time flows backwards /// public bool IsReverseAggregation { get { return EndTime < StartTime; } } /// /// The percentage data that can be bad. /// public byte PercentDataBad { get { return Configuration.PercentDataBad; } } /// /// The percentage data that must be good. /// public byte PercentDataGood { get { return Configuration.PercentDataGood; } } /// /// Whether to use sloped extrapolation. /// public bool UseSlopedExtrapolation { get { return Configuration.UseSlopedExtrapolation; } } /// /// Whether value sematics of the underlying data require stepped interpolation. /// public bool SteppedVariable { get; set; } /// /// How to treat uncertain data. /// public bool TreatUncertainAsBad { get { return Configuration.TreatUncertainAsBad; } } /// /// THe width of the processing interval. /// public double ProcessingInterval { get; set; } #endregion #region IAggregateCalculator Members /// /// Processes the next value returns the calculated values up until the last complete interval. /// public IList ProcessValue(DataValue value, ServiceResult result) { if (m_state == null) InitializeAggregation(); m_state.AddRawData(value); return ProcessedValues(); } /// /// Processes all remaining intervals. /// public IList ProcessTermination(ServiceResult result) { if (m_state == null) InitializeAggregation(); m_state.EndOfData(); return ProcessedValues(); } #endregion #region IAggregationActor Members /// /// Updates the data processed by the aggregator. /// public void UpdateProcessedData(DataValue rawValue, AggregateState state) { // step 1: compute new TimeSlice instances to enqueue, until we reach the one the // rawValue belongs in or we've reached the one that goes to the EndTime. Ensure // that the raw value is added to the last one created. TimeSlice tmpTS = null; if (m_pending == null) m_pending = new Queue(); if (m_latest == null) { tmpTS = TimeSlice.CreateInitial(StartTime, EndTime, ProcessingInterval); if (tmpTS != null) { m_pending.Enqueue(tmpTS); m_latest = tmpTS; } } else { tmpTS = m_latest; } DateTime latestTime = (StartTime > EndTime) ? StartTime : EndTime; while ((tmpTS != null) && (state.HasTerminated || !tmpTS.AcceptValue(rawValue))) { tmpTS = TimeSlice.CreateNext(latestTime, ProcessingInterval, tmpTS); if (tmpTS != null) { m_pending.Enqueue(tmpTS); m_latest = tmpTS; } } // step 2: apply the aggregator to the head of the queue to see if we can convert // it into a processed point. If so, dequeue it and add the processed value to the // m_released list. Keep doing it until one of the TimeSlices returns null or we // run out of enqueued TimeSlices (should only happen on termination). if (m_released == null) m_released = new List(); foreach (TimeSlice b in m_pending) UpdateBoundingValues(b, state); bool active = true; while ((m_pending.Count > 0) && active) { TimeSlice top = m_pending.Peek(); DataValue computed = null; if (!WaitForMoreData(top, state)) computed = Compute(this, top, state); if (computed != null) { m_released.Add(computed); m_pending.Dequeue(); } else { active = false; } } } /// /// Returns the values processed by the aggregator. /// public IList ProcessedValues() { IList retval = null; retval = (m_released != null) ? m_released : new List(); m_released = null; return retval; } #endregion #region IAggregator Members /// /// Computes the aggregate value for the time slice. /// public abstract DataValue Compute(IAggregationContext context, TimeSlice bucket, AggregateState state); /// /// Returns true if more data is required for the next interval. /// public abstract bool WaitForMoreData(TimeSlice bucket, AggregateState state); /// /// Updates the bounding values for the time slice. /// public abstract void UpdateBoundingValues(TimeSlice bucket, AggregateState state); #endregion #region Public Members /// /// The configuration to use when calculating aggregates. /// public AggregateConfiguration Configuration { get; set; } /// /// Computes the status code for the processing interval using the percent good/bad information in the context. /// protected virtual StatusCode ComputeStatus(IAggregationContext context, int numGood, int numBad, TimeSlice bucket) { int total = numGood + numBad; if (total > 0) { double pbad = (numBad * 100) / total; if (pbad > context.PercentDataBad) return StatusCodes.Bad; double pgood = (numGood * 100) / total; if (pgood >= context.PercentDataGood) return StatusCodes.Good; return StatusCodes.Uncertain; } else { return StatusCodes.GoodNoData; } } #endregion #region Private Methods /// /// Initializes the aggregation. /// private void InitializeAggregation() { m_state = new AggregateState(this, this); } #endregion #region Private Fields private AggregateState m_state = null; private TimeSlice m_latest; private Queue m_pending; private List m_released; #endregion } /// /// Allows aggregates to be calculated without interpolation. /// public abstract class NonInterpolatingCalculator : AggregateCalculatorImpl { /// /// Returns true if more data is required for the next interval. /// public override bool WaitForMoreData(TimeSlice bucket, AggregateState state) { bool wait = false; if (!state.HasTerminated) { if (bucket.ContainsTime(state.LatestTimestamp)) { wait = true; } } return wait; } /// /// Updates the bounding values for the time slice. /// public override void UpdateBoundingValues(TimeSlice bucket, AggregateState state) { } } /// /// Calculates aggregates with interpolation. /// public abstract class InterpolatingCalculator : AggregateCalculatorImpl { /// /// Returns true if more data is required for the next interval. /// public override bool WaitForMoreData(TimeSlice bucket, AggregateState state) { if (!state.HasTerminated) { if (bucket.ContainsTime(state.LatestTimestamp)) { return true; } if (this.IsReverseAggregation) { if (state.LatestTimestamp < bucket.To) { return false; } } else { if (state.LatestTimestamp > bucket.To) { return false; } } if ((bucket.EarlyBound.Value == null) || (bucket.LateBound.Value == null)) { return true; } } return false; } /// /// Calculates the status for the time slice. /// protected override StatusCode ComputeStatus(IAggregationContext context, int numGood, int numBad, TimeSlice bucket) { StatusCode code = (bucket.EarlyBound.Value == null && numGood + numBad == 0) ? // no inital bound, do not extrapolate StatusCodes.BadNoData : base.ComputeStatus(context, numGood, numBad, bucket); return code; } /// /// Determines the best good point before the end bound. /// protected void UpdatePriorPoint(BoundingValue bound, AggregateState state) { if (state.HasTerminated && (state.LatePoint == null) && bound.PriorPoint == null) { bound.PriorPoint = state.PriorPoint; bound.PriorBadPoints = state.PriorBadPoints; bound.DerivationType = UseSlopedExtrapolation ? BoundingValueType.SlopedExtrapolation : BoundingValueType.SteppedExtrapolation; } } } /// /// Calculates aggreates based on the point values. /// public abstract class FloatInterpolatingCalculator : InterpolatingCalculator { /// /// Updates the bounding values for the time slice. /// public override void UpdateBoundingValues(TimeSlice bucket, AggregateState state) { BoundingValue EarlyBound = bucket.EarlyBound; BoundingValue LateBound = bucket.LateBound; if (bucket.ExactMatch(state.LatestTimestamp) && StatusCode.IsGood(state.LatestStatus)) { EarlyBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; EarlyBound.DerivationType = BoundingValueType.Raw; } else { if (EarlyBound.DerivationType != BoundingValueType.Raw) { if (EarlyBound.EarlyPoint == null) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.From)) { EarlyBound.EarlyPoint = state.EarlyPoint; } } if (EarlyBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.From)) { EarlyBound.LatePoint = state.LatePoint; if (SteppedVariable) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); } else { EarlyBound.CurrentBadPoints = state.CurrentBadPoints; } EarlyBound.DerivationType = SteppedVariable ? BoundingValueType.SteppedInterpolation : BoundingValueType.SlopedInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { if (SteppedVariable) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); } else { EarlyBound.CurrentBadPoints = state.CurrentBadPoints; } } } if (bucket.EndMatch(state.LatestTimestamp) && StatusCode.IsGood(state.LatestStatus)) { LateBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; LateBound.DerivationType = BoundingValueType.Raw; } else { if (LateBound.DerivationType != BoundingValueType.Raw) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.To)) LateBound.EarlyPoint = state.EarlyPoint; if (LateBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.To)) { LateBound.LatePoint = state.LatePoint; if (SteppedVariable) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); } else { LateBound.CurrentBadPoints = state.CurrentBadPoints; } LateBound.DerivationType = SteppedVariable ? BoundingValueType.SteppedInterpolation : BoundingValueType.SlopedInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { if (SteppedVariable) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); } else { LateBound.CurrentBadPoints = state.CurrentBadPoints; } } UpdatePriorPoint(LateBound, state); } } } /// /// Calculates aggreates based something other that the value. /// public abstract class SteppedInterpolatingCalculator : InterpolatingCalculator { /// /// Updates the bounding values for the time slice. /// public override void UpdateBoundingValues(TimeSlice bucket, AggregateState state) { BoundingValue EarlyBound = bucket.EarlyBound; BoundingValue LateBound = bucket.LateBound; if (bucket.ExactMatch(state.LatestTimestamp) && StatusCode.IsGood(state.LatestStatus)) { EarlyBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; EarlyBound.DerivationType = BoundingValueType.Raw; } else { if (EarlyBound.DerivationType != BoundingValueType.Raw) { if (EarlyBound.EarlyPoint == null) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.From)) { EarlyBound.EarlyPoint = state.EarlyPoint; } } if (EarlyBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.From)) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); EarlyBound.DerivationType = BoundingValueType.SteppedInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); EarlyBound.DerivationType = BoundingValueType.SteppedExtrapolation; } } if (bucket.EndMatch(state.LatestTimestamp) && StatusCode.IsGood(state.LatestStatus)) { LateBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; LateBound.DerivationType = BoundingValueType.Raw; } else { if (LateBound.DerivationType != BoundingValueType.Raw) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.To)) LateBound.EarlyPoint = state.EarlyPoint; if (LateBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.To)) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); LateBound.DerivationType = BoundingValueType.SteppedInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); if (EarlyBound.PriorPoint == null) { EarlyBound.PriorPoint = state.PriorPoint; EarlyBound.PriorBadPoints = state.PriorBadPoints; EarlyBound.DerivationType = UseSlopedExtrapolation ? BoundingValueType.SlopedExtrapolation : BoundingValueType.SteppedExtrapolation; } LateBound.DerivationType = BoundingValueType.SteppedExtrapolation; } } } } /// /// Calculates aggreates based on the quality or duration. /// public abstract class QualityDurationCalculator : InterpolatingCalculator { /// /// Checks if the point has the status that meets the aggregate criteria. /// protected abstract bool RightStatusCode(DataValue dv); /// /// Calculates the value for the time slice. /// public override DataValue Compute(IAggregationContext context, TimeSlice bucket, AggregateState state) { DataValue retval = new DataValue { SourceTimestamp = bucket.From }; ; StatusCode code = StatusCodes.Good; DataValue previous = new DataValue { SourceTimestamp = bucket.From }; if (bucket.EarlyBound.Value != null) previous.StatusCode = (StatusCode)bucket.EarlyBound.Value.WrappedValue.Value; else previous.StatusCode = StatusCodes.Bad; if (!RightStatusCode(previous)) previous = null; double total = 0.0; foreach (DataValue v in bucket.Values) { if (previous != null) total += (v.SourceTimestamp - previous.SourceTimestamp).TotalMilliseconds; if (RightStatusCode(v)) previous = v; else previous = null; } if (previous != null) total += (bucket.To - previous.SourceTimestamp).TotalMilliseconds; retval.Value = total; code.AggregateBits = AggregateBits.Calculated; if (bucket.Incomplete) code.AggregateBits |= AggregateBits.Partial; retval.StatusCode = code; return retval; } /// /// Updates the bounding values for the time slice. /// public override void UpdateBoundingValues(TimeSlice bucket, AggregateState state) { BoundingValue EarlyBound = bucket.EarlyBound; BoundingValue LateBound = bucket.LateBound; if (bucket.ExactMatch(state.LatestTimestamp)) { EarlyBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; EarlyBound.DerivationType = BoundingValueType.QualityRaw; } else { if (EarlyBound.DerivationType != BoundingValueType.QualityRaw) { if (EarlyBound.EarlyPoint == null) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.From)) { EarlyBound.EarlyPoint = state.EarlyPoint; } } if (EarlyBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.From)) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); EarlyBound.DerivationType = BoundingValueType.QualityInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { EarlyBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < EarlyBound.Timestamp) EarlyBound.CurrentBadPoints.Add(dv); EarlyBound.DerivationType = BoundingValueType.QualityExtrapolation; } } if (bucket.EndMatch(state.LatestTimestamp)) { LateBound.RawPoint = state.LatePoint == null ? state.EarlyPoint : state.LatePoint; LateBound.DerivationType = BoundingValueType.QualityRaw; } else { if (LateBound.DerivationType != BoundingValueType.QualityRaw) { if ((state.EarlyPoint != null) && (state.EarlyPoint.SourceTimestamp < bucket.To)) LateBound.EarlyPoint = state.EarlyPoint; if (LateBound.LatePoint == null) { if ((state.LatePoint != null) && (state.LatePoint.SourceTimestamp >= bucket.To)) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); LateBound.DerivationType = BoundingValueType.QualityInterpolation; } } } if (state.HasTerminated && (state.LatePoint == null)) { LateBound.CurrentBadPoints = new List(); foreach (DataValue dv in state.CurrentBadPoints) if (dv.SourceTimestamp < LateBound.Timestamp) LateBound.CurrentBadPoints.Add(dv); LateBound.DerivationType = BoundingValueType.QualityExtrapolation; } } } } }