//****************************************************************************************************** // 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 } }