//******************************************************************************************************
// PowerMultiCalculatorAdapter.cs - Gbtc
//
// Copyright © 2012, 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://www.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/02/2015 - Ryan McCoy
// Generated original version of source code.
// 12/02/2015 - J. Ritchie Carroll
// Updated power calculations to use common Power functions.
//
//******************************************************************************************************
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using GSF;
using GSF.Configuration;
using GSF.Data;
using GSF.Diagnostics;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
using GSF.Units;
using GSF.Units.EE;
namespace PowerCalculations.PowerMultiCalculator
{
///
/// Performs MW, MVA, and MVAR calculations based on current and voltage phasors input to the adapter
///
[Description("PowerMultiCalculatorAdapter: Performs MW, MVAR and MVA calculations based on current and voltage phasors input to the adapter")]
public class PowerMultiCalculatorAdapter : ActionAdapterBase
{
#region [ Members ]
// Constants
private const string DefaultTableName = "PowerCalculation";
private const double SqrtOf3 = 1.7320508075688772935274463415059D;
private const int ValuesToTrack = 5;
// Fields
private List m_configuredCalculations;
private Dictionary m_adjustmentStrategies;
private RunningAverage m_averageCalculationsPerFrame;
private RunningAverage m_averageCalculationTime;
private RunningAverage m_averageTotalCalculationTime;
private ConcurrentQueue m_lastActivePowerCalculations;
private ConcurrentQueue m_lastReactivePowerCalculations;
private ConcurrentQueue m_lastApparentPowerCalculations;
private double m_lastTotalCalculationTime;
private int m_lastTotalCalculations;
#endregion
#region [ Constructors ]
///
/// Creates the adapter
///
public PowerMultiCalculatorAdapter()
{
try
{
// Validate that data operation and adapter instance exist within database
PowerCalculationConfigurationValidation.ValidateDatabaseDefinitions();
}
catch
{
// This should never cause unhanded exception
}
}
#endregion
#region [ Properties ]
///
/// Gets or sets the name of the table this adapter will use to obtain its metadata.
///
[ConnectionStringParameter]
[Description("Defines the name of the table this adapter will use to obtain its metadata.")]
[DefaultValue(DefaultTableName)]
public string TableName { get; set; }
///
/// Gets or sets flag indicating whether or not this adapter will produce a result for all calculations. If this value is true and a calculation fails,
/// the adapter will produce NaN for that calculation. If this value is false and a calculation fails, the adapter will not produce any result.
///
[ConnectionStringParameter]
[Description("Defines flag that determines if adapter should always produce a result. When true, adapter will produce NaN for calculations that fail.")]
[DefaultValue(false)]
public bool AlwaysProduceResult { get; set; }
///
/// Gets or sets the default strategy used to adjust voltage values for based on the nature of the voltage measurements.
///
[ConnectionStringParameter]
[Description("Defines default strategy used to adjust voltage values for based on the nature of the voltage measurements.")]
[DefaultValue(VoltageAdjustmentStrategy.LineToNeutral)]
public VoltageAdjustmentStrategy AdjustmentStrategy { get; set; }
///
/// Gets or sets flag that determines if adapter should enable temporal processing support.
///
[ConnectionStringParameter]
[Description("Defines flag that determines if adapter should enable temporal processing support.")]
[DefaultValue(false)]
public bool EnableTemporalProcessing { get; set; }
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
public override bool SupportsTemporalProcessing => EnableTemporalProcessing;
///
/// Returns the adapter status, including real-time statistics about adapter operation
///
public override string Status
{
get
{
StringBuilder status = new StringBuilder();
VoltageAdjustmentStrategy[] strategies = m_adjustmentStrategies?.Values.ToArray() ?? Array.Empty();
status.Append(base.Status);
status.AppendLine($" Default Adjustment Strategy: {AdjustmentStrategy}");
status.AppendLine($" Total Per-Circuit Adjustments: {strategies.Length:N0}");
if (strategies.Length > 0)
{
int strategyCount(params VoltageAdjustmentStrategy[] targetStrategies) =>
strategies.Count(targetStrategies.Contains);
status.AppendLine(" -- Totals per Adjustment Strategy --");
status.AppendLine($" Est. 3-Phase Line-to-Line : {strategyCount(VoltageAdjustmentStrategy.LineToLine):N0}");
status.AppendLine($" Est. 3-Phase Line-to-Neutral : {strategyCount(VoltageAdjustmentStrategy.LineToNeutral):N0}");
status.AppendLine($" 1-Phase Line-to-Line : {strategyCount(VoltageAdjustmentStrategy.LineToLineSinglePhase):N0}");
status.AppendLine($" 1-Phase Line-to-Neutral/None : {strategyCount(VoltageAdjustmentStrategy.LineToNeutralSinglePhase, VoltageAdjustmentStrategy.None):N0}");
}
status.AppendLine($" Last Total Calculations: {m_lastTotalCalculations}");
status.AppendLine($" Average Total Calculations: {Math.Round(m_averageCalculationsPerFrame.Average):N3} per frame");
status.AppendLine($" Average Calculation Time: {m_averageCalculationTime.Average:N3} ms");
status.AppendLine($" Last Total Calculation Time: {m_lastTotalCalculationTime:N3} ms");
status.AppendLine($" Average Total Calculation Time: {m_averageTotalCalculationTime.Average:N3} ms");
status.AppendLine(" Last Active Power Measurements:");
if (!m_lastActivePowerCalculations.Any())
{
status.AppendLine("\tNot enough values...");
}
else
{
foreach (IMeasurement measurement in m_lastActivePowerCalculations)
status.AppendLine($"\t{measurement.Key} = {measurement.AdjustedValue:N3}");
}
status.AppendLine(" Last Reactive Power Measurements:");
if (!m_lastReactivePowerCalculations.Any())
{
status.AppendLine("\tNot enough values...");
}
else
{
foreach (IMeasurement measurement in m_lastReactivePowerCalculations)
status.AppendLine($"\t{measurement.Key} = {measurement.AdjustedValue:N3}");
}
status.AppendLine(" Last Apparent Power Measurements:");
if (!m_lastApparentPowerCalculations.Any())
{
status.AppendLine("\tNot enough values...");
}
else
{
foreach (IMeasurement measurement in m_lastApparentPowerCalculations)
status.AppendLine($"\t{measurement.Key} = {measurement.AdjustedValue:N3}");
}
status.AppendLine();
return status.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Loads configuration from the database and configures adapter to run with that configuration
///
public override void Initialize()
{
base.Initialize();
HashSet outputMeasurements = new HashSet();
m_configuredCalculations = new List();
m_adjustmentStrategies = new Dictionary();
m_averageCalculationsPerFrame = new RunningAverage();
m_averageCalculationTime = new RunningAverage();
m_averageTotalCalculationTime = new RunningAverage();
m_lastActivePowerCalculations = new ConcurrentQueue();
m_lastReactivePowerCalculations = new ConcurrentQueue();
m_lastApparentPowerCalculations = new ConcurrentQueue();
if (!Settings.TryGetValue(nameof(TableName), out string tableName))
tableName = DefaultTableName;
string query = "SELECT " +
// 0 1 2 3 4 5
"ID, CircuitDescription, VoltageAngleSignalID, VoltageMagSignalID, CurrentAngleSignalID, CurrentMagSignalID, " +
// 6 7 8
"ActivePowerOutputSignalID, ReactivePowerOutputSignalID, ApparentPowerOutputSignalID FROM " + tableName + " " +
"WHERE NodeId = {0} AND Enabled <> 0 ";
using (AdoDataConnection database = new AdoDataConnection("systemSettings"))
using (IDataReader rdr = database.ExecuteReader(query, ConfigurationFile.Current.Settings["systemSettings"]["NodeID"].ValueAs()))
{
while (rdr.Read())
{
m_configuredCalculations.Add(new PowerCalculation
{
PowerCalculationID = rdr.GetInt32(0),
CircuitDescription = rdr.GetString(1),
VoltageAngleMeasurementKey = MeasurementKey.LookUpBySignalID(Guid.Parse(rdr[2].ToString())),
VoltageMagnitudeMeasurementKey = MeasurementKey.LookUpBySignalID(Guid.Parse(rdr[3].ToString())),
CurrentAngleMeasurementKey = MeasurementKey.LookUpBySignalID(Guid.Parse(rdr[4].ToString())),
CurrentMagnitudeMeasurementKey = MeasurementKey.LookUpBySignalID(Guid.Parse(rdr[5].ToString())),
ActivePowerOutputMeasurement = AddOutputMeasurement(Guid.Parse(rdr[6].ToString()), outputMeasurements),
ReactivePowerOutputMeasurement = AddOutputMeasurement(Guid.Parse(rdr[7].ToString()), outputMeasurements),
ApparentPowerOutputMeasurement = AddOutputMeasurement(Guid.Parse(rdr[8].ToString()), outputMeasurements)
});
}
}
if (m_configuredCalculations.Any())
InputMeasurementKeys = m_configuredCalculations.SelectMany(pc => new[] { pc.CurrentAngleMeasurementKey, pc.CurrentMagnitudeMeasurementKey, pc.VoltageAngleMeasurementKey, pc.VoltageMagnitudeMeasurementKey }).ToArray();
else
throw new InvalidOperationException("Skipped initialization of power calculator: no defined power calculations...");
if (outputMeasurements.Any())
OutputMeasurements = outputMeasurements.ToArray();
Dictionary settings = Settings;
if (settings.TryGetValue(nameof(AlwaysProduceResult), out string setting))
AlwaysProduceResult = setting.ParseBoolean();
if (settings.TryGetValue(nameof(AdjustmentStrategy), out setting) && Enum.TryParse(setting, out VoltageAdjustmentStrategy adjustmentStrategy))
AdjustmentStrategy = adjustmentStrategy;
if (settings.TryGetValue(nameof(EnableTemporalProcessing), out setting))
EnableTemporalProcessing = setting.ParseBoolean();
// Define per power calculation line adjustment strategies
foreach (PowerCalculation powerCalculation in m_configuredCalculations)
{
if (powerCalculation.VoltageMagnitudeMeasurementKey is null || string.IsNullOrWhiteSpace(powerCalculation.CircuitDescription))
continue;
try
{
Dictionary circuitSettings = powerCalculation.CircuitDescription.ParseKeyValuePairs();
if (circuitSettings.TryGetValue(nameof(AdjustmentStrategy), out setting) && Enum.TryParse(setting, out adjustmentStrategy))
m_adjustmentStrategies[powerCalculation.VoltageMagnitudeMeasurementKey] = adjustmentStrategy;
}
catch (Exception ex)
{
OnStatusMessage(MessageLevel.Warning, $"Failed to parse settings from circuit description \"{powerCalculation.CircuitDescription}\": {ex.Message}");
}
}
}
///
/// Calculates MW, MVAR and MVA then publishes those measurements
///
/// Input values for calculation
/// Index of frame within second.
protected override void PublishFrame(IFrame frame, int index)
{
ConcurrentDictionary measurements = frame.Measurements;
Ticks totalCalculationTime = DateTime.UtcNow.Ticks;
Ticks lastCalculationTime = DateTime.UtcNow.Ticks;
List outputMeasurements = new List();
int calculations = 0;
foreach (PowerCalculation powerCalculation in m_configuredCalculations)
{
double activePower = double.NaN, reactivePower = double.NaN, apparentPower = double.NaN;
IMeasurement measurement;
try
{
double voltageMagnitude = 0.0D, voltageAngle = 0.0D, currentMagnitude = 0.0D, currentAngle = 0.0D;
bool allValuesReceivedWithGoodQuality = false;
lastCalculationTime = DateTime.UtcNow.Ticks;
if (measurements.TryGetValue(powerCalculation.VoltageMagnitudeMeasurementKey, out measurement) && measurement.ValueQualityIsGood())
{
voltageMagnitude = measurement.AdjustedValue;
if (!m_adjustmentStrategies.TryGetValue(powerCalculation.VoltageMagnitudeMeasurementKey, out VoltageAdjustmentStrategy adjustmentStrategy))
adjustmentStrategy = AdjustmentStrategy;
switch (adjustmentStrategy)
{
case VoltageAdjustmentStrategy.LineToNeutral:
voltageMagnitude *= 3.0D;
break;
case VoltageAdjustmentStrategy.LineToLine:
voltageMagnitude *= SqrtOf3;
break;
case VoltageAdjustmentStrategy.LineToLineSinglePhase:
voltageMagnitude /= SqrtOf3;
break;
}
if (measurements.TryGetValue(powerCalculation.VoltageAngleMeasurementKey, out measurement) && measurement.ValueQualityIsGood())
{
voltageAngle = measurement.AdjustedValue;
if (measurements.TryGetValue(powerCalculation.CurrentMagnitudeMeasurementKey, out measurement) && measurement.ValueQualityIsGood())
{
currentMagnitude = measurement.AdjustedValue;
if (measurements.TryGetValue(powerCalculation.CurrentAngleMeasurementKey, out measurement) && measurement.ValueQualityIsGood())
{
currentAngle = measurement.AdjustedValue;
allValuesReceivedWithGoodQuality = true;
}
}
}
}
if (allValuesReceivedWithGoodQuality)
{
// Calculate power (P), reactive power (Q) and apparent power (|S|)
Phasor voltage = new Phasor(PhasorType.Voltage, Angle.FromDegrees(voltageAngle), voltageMagnitude);
Phasor current = new Phasor(PhasorType.Current, Angle.FromDegrees(currentAngle), currentMagnitude);
activePower = Phasor.CalculateActivePower(voltage, current) / SI.Mega;
reactivePower = Phasor.CalculateReactivePower(voltage, current) / SI.Mega;
apparentPower = Phasor.CalculateApparentPower(voltage, current) / SI.Mega;
}
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Warning, ex);
}
finally
{
if (!(powerCalculation.ActivePowerOutputMeasurement is null))
{
Measurement activePowerMeasurement = Measurement.Clone(powerCalculation.ActivePowerOutputMeasurement, activePower, frame.Timestamp);
if (AlwaysProduceResult || !double.IsNaN(activePowerMeasurement.Value))
{
outputMeasurements.Add(activePowerMeasurement);
calculations++;
m_lastActivePowerCalculations.Enqueue(activePowerMeasurement);
while (m_lastActivePowerCalculations.Count > ValuesToTrack)
m_lastActivePowerCalculations.TryDequeue(out measurement);
}
}
if (!(powerCalculation.ReactivePowerOutputMeasurement is null))
{
Measurement reactivePowerMeasurement = Measurement.Clone(powerCalculation.ReactivePowerOutputMeasurement, reactivePower, frame.Timestamp);
if (AlwaysProduceResult || !double.IsNaN(reactivePowerMeasurement.Value))
{
outputMeasurements.Add(reactivePowerMeasurement);
calculations++;
m_lastReactivePowerCalculations.Enqueue(reactivePowerMeasurement);
while (m_lastReactivePowerCalculations.Count > ValuesToTrack)
m_lastReactivePowerCalculations.TryDequeue(out measurement);
}
}
if (!(powerCalculation.ApparentPowerOutputMeasurement is null))
{
Measurement apparentPowerMeasurement = Measurement.Clone(powerCalculation.ApparentPowerOutputMeasurement, apparentPower, frame.Timestamp);
if (AlwaysProduceResult || !double.IsNaN(apparentPowerMeasurement.Value))
{
outputMeasurements.Add(apparentPowerMeasurement);
calculations++;
m_lastApparentPowerCalculations.Enqueue(apparentPowerMeasurement);
while (m_lastApparentPowerCalculations.Count > ValuesToTrack)
m_lastApparentPowerCalculations.TryDequeue(out measurement);
}
}
m_averageCalculationTime.AddValue((DateTime.UtcNow.Ticks - lastCalculationTime).ToMilliseconds());
}
}
m_lastTotalCalculationTime = (DateTime.UtcNow.Ticks - totalCalculationTime).ToMilliseconds();
m_averageTotalCalculationTime.AddValue(m_lastTotalCalculationTime);
m_lastTotalCalculations = calculations;
m_averageCalculationsPerFrame.AddValue(calculations);
OnNewMeasurements(outputMeasurements);
}
#endregion
#region [ Static ]
// Static Methods
private static Measurement AddOutputMeasurement(Guid signalID, HashSet outputMeasurements)
{
Measurement measurement = GetMeasurement(signalID);
if (!(measurement is null))
outputMeasurements.Add(measurement);
return measurement;
}
private static Measurement GetMeasurement(Guid signalID)
{
MeasurementKey key = MeasurementKey.LookUpBySignalID(signalID);
if (key.SignalID == Guid.Empty)
return null;
Measurement measurement = new Measurement
{
Metadata = key.Metadata,
};
return measurement;
}
#endregion
}
}