diff --git a/Source/Libraries/Adapters/PowerCalculations/PhasorAddition.cs b/Source/Libraries/Adapters/PowerCalculations/PhasorAddition.cs new file mode 100644 index 0000000000..4aa79c9507 --- /dev/null +++ b/Source/Libraries/Adapters/PowerCalculations/PhasorAddition.cs @@ -0,0 +1,247 @@ +//****************************************************************************************************** +// PhasorAddition.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: +// ---------------------------------------------------------------------------------------------------- +// 05/14/2025 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using GSF; +using GSF.Collections; +using GSF.TimeSeries; +using GSF.TimeSeries.Adapters; +using GSF.Units; +using GSF.Units.EE; +using PhasorProtocolAdapters; + +namespace PowerCalculations; + +/// +/// Calculates sum or difference between two voltages or currents. +/// +[Description("Phasor Addition: Computes the sum or difference between two phasors")] +public class PhasorAddition : CalculatedMeasurementBase +{ + #region [ Members ] + + // Constants + private const double Rad120 = 2.0D * Math.PI / 3.0D; + private const bool DefaultDifference = false; + // Fields + private MeasurementKey[] m_angles; + private MeasurementKey[] m_magnitudes; + private double m_lastMagnitudeResult; + private Angle m_lastAngleResult; + + /// + /// Defines the output measurements for the . + /// + /// + /// One output measurement should be defined for each enumeration value, in order: + /// + public enum Output + { + /// + /// Magnitude measurement. + /// + Magnitude, + /// + /// Angle measurement. + /// + Phase + } + + #endregion + + #region [ Properties ] + + /// + /// Gets or sets flag that determines if the second phasor should be subtracted. + /// + [ConnectionStringParameter] + [Description("Flag that determines if the second phasor should be subtracted.")] + [DefaultValue(DefaultDifference)] + public bool Difference { get; set; } = DefaultDifference; + + /// + /// Gets the flag indicating if this adapter supports temporal processing. + /// + public override bool SupportsTemporalProcessing => true; + + /// + /// Returns the detailed status of the . + /// + public override string Status + { + get + { + StringBuilder status = new(); + + status.Append(base.Status); + + status.AppendLine($" Phasor 1 magnitude: {m_magnitudes[0]}"); + status.AppendLine($" Phasor 2 magnitude: {m_magnitudes[1]}"); + status.AppendLine($" Phase 1 angle: {m_angles[0]}"); + status.AppendLine($" Phase 2 angle: {m_angles[1]}"); + status.AppendLine($" Difference: {Difference}"); + + status.AppendLine(); + status.Append(" Last calculated angle: "); + + status.Append(!double.IsNaN(m_lastAngleResult) ? $"{m_lastAngleResult.ToDegrees():0.00}°" : "No values calculated yet..."); + + status.AppendLine(); + status.Append(" Last calculated magnitude: "); + + status.Append(!double.IsNaN(m_lastMagnitudeResult) ? $"{m_lastMagnitudeResult:0.00}" : + "No values calculated yet..."); + + status.AppendLine(); + status.AppendLine(); + + return status.ToString(); + } + } + + #endregion + + #region [ Methods ] + + /// + /// Initializes the . + /// + public override void Initialize() + { + base.Initialize(); + + Dictionary settings = Settings; + + // Load parameters + if (settings.TryGetValue(nameof(Difference), out string setting)) + Difference = setting.ParseBoolean(); + + // Load needed phase angle measurement keys from defined InputMeasurementKeys + m_angles = InputMeasurementKeys.Where((_, index) => InputMeasurementKeyTypes[index] == SignalType.VPHA || InputMeasurementKeyTypes[index] == SignalType.IPHA).ToArray(); + + if (m_angles.Length != 2) + { + throw new InvalidOperationException("Exactly 2 angle input measurements are required."); + } + + // Load needed phase magnitude measurement keys from defined InputMeasurementKeys + m_magnitudes = InputMeasurementKeys.Where((_, index) => InputMeasurementKeyTypes[index] == SignalType.VPHM || InputMeasurementKeyTypes[index] == SignalType.IPHM).ToArray(); + + if (m_magnitudes.Length != 2) + { + throw new InvalidOperationException("Exactly 2 magnitude input measurements are required."); + } + + + // Make sure only these phasor measurements are used as input + InputMeasurementKeys = m_angles.Concat(m_magnitudes).ToArray(); + + // Validate output measurements + if (OutputMeasurements.Length < 2) + throw new InvalidOperationException("Not enough output measurements were specified for the phasor addition, expecting measurements for the \"Magnitude\", and \"Angle\" - in this order."); + + m_lastMagnitudeResult = double.NaN; + m_lastAngleResult = double.NaN; + + } + + /// + /// Publish frame of time-aligned collection of measurement values that arrived within the defined lag time. + /// + /// Frame of measurements with the same timestamp that arrived within lag time that are ready for processing. + /// Index of frame within a second ranging from zero to frames per second - 1. + protected override void PublishFrame(IFrame frame, int index) + { + ComplexNumber result = nanSeq; + + try + { + ConcurrentDictionary measurements = frame.Measurements; + double m1 = 0.0D, a1 = 0.0D, m2 = 0.0D, a2 = 0.0D; + bool allInputsReceived = false; + + // Get all needed measurement values from this frame + if (measurements.TryGetValue(m_magnitudes[0], out IMeasurement measurement)) + { + // Get first magnitude value + m1 = measurement.AdjustedValue; + + if (measurements.TryGetValue(m_angles[0], out measurement)) + { + // Get first angle value + a1 = measurement.AdjustedValue; + + if (measurements.TryGetValue(m_magnitudes[1], out measurement)) + { + // Get second magnitude value + m2 = measurement.AdjustedValue; + + if (measurements.TryGetValue(m_angles[1], out measurement)) + { + // Get second angle value + a2 = measurement.AdjustedValue; + } + } + } + } + + if (!allInputsReceived) + return; + + ComplexNumber phasor1 = new(Angle.FromDegrees(a1), m1); + ComplexNumber phasor2 = new(Angle.FromDegrees(a2), m2); + + if (Difference) + result = phasor1 - phasor2; + else + result = phasor1 + phasor2; + } + finally + { + IMeasurement[] outputMeasurements = OutputMeasurements; + + // Provide calculated measurements for external consumption + OnNewMeasurements(new IMeasurement[] + { + Measurement.Clone(outputMeasurements[0], result.Magnitude, frame.Timestamp), + Measurement.Clone(outputMeasurements[1], result.Angle.ToDegrees(), frame.Timestamp) + }); + } + } + + #endregion + + #region [ Static ] + + // Static Fields + + // a = e^((2/3) * pi * i) + private static readonly ComplexNumber nanSeq = new(double.NaN, double.NaN); + + #endregion +} \ No newline at end of file diff --git a/Source/Libraries/Adapters/PowerCalculations/PowerCalculations.csproj b/Source/Libraries/Adapters/PowerCalculations/PowerCalculations.csproj index faea7b5ab7..fb86af1069 100755 --- a/Source/Libraries/Adapters/PowerCalculations/PowerCalculations.csproj +++ b/Source/Libraries/Adapters/PowerCalculations/PowerCalculations.csproj @@ -71,6 +71,7 @@ +