//******************************************************************************************************
// CorrectiveParser.cs - Gbtc
//
// Copyright © 2014, 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:
// ----------------------------------------------------------------------------------------------------
// 12/18/2014 - Stephen C. Wills
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Linq;
namespace GSF.EMAX
{
///
/// EMAX data file(s) parser that calculates timestamps based
/// on sample rate and attempts to correct inverted values.
///
public class CorrectiveParser : IDisposable
{
#region [ Members ]
// Fields
private Parser m_parser;
private DateTime m_currentSecond;
private Ticks[] m_subsecondDistribution;
private int m_currentIndex;
private DateTime m_calculatedTimestamp;
private double[] m_correctedValues;
private TimeZoneInfo m_sourceTimeZone;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public CorrectiveParser()
{
m_parser = new Parser();
}
#endregion
#region [ Properties ]
///
/// Gets or sets associated EMAX control file for this .
///
///
/// This is similar in function to a COMTRADE schema file.
///
public ControlFile ControlFile
{
get
{
return m_parser.ControlFile;
}
set
{
m_parser.ControlFile = value;
if ((object)value != null)
{
m_correctedValues = new double[Values.Length];
m_sourceTimeZone = value.SystemParameters.GetTimeZoneInfo();
}
else
{
m_correctedValues = null;
}
}
}
///
/// Gets or sets EMAX data filename.
///
///
/// If there are more than one data files in a set (e.g., RCL/RCU), this should be set to first file name in the set, e.g., DATA123.RCL.
///
public string FileName
{
get
{
return m_parser.FileName;
}
set
{
m_parser.FileName = value;
}
}
///
/// Gets timestamp of current record, parsed from the file, in the timezone of provided IRIG signal.
///
public DateTime ParsedTimestamp
{
get
{
return m_parser.Timestamp;
}
}
///
/// Attempts to get current timestamp, parsed from the file, converted to UTC.
///
///
/// This will only be accurate if timezone configured in device matches IRIG clock.
///
public DateTime ParsedTimestampAsUtc
{
get
{
return TimeZoneInfo.ConvertTimeToUtc(m_calculatedTimestamp, m_sourceTimeZone);
}
}
///
/// Gets calculated timestamp of current record in the timezone of provided IRIG signal.
///
public DateTime CalculatedTimestamp
{
get
{
return m_calculatedTimestamp;
}
}
///
/// Attempts to get calculated timestamp of current record converted to UTC.
///
///
/// This will only be accurate if timezone configured in device matches IRIG clock.
///
public DateTime CalculatedTimestampAsUtc
{
get
{
return TimeZoneInfo.ConvertTimeToUtc(m_calculatedTimestamp, m_sourceTimeZone);
}
}
///
/// Gets values of current record.
///
public double[] Values
{
get
{
return m_parser.Values;
}
}
///
/// Gets corrected values, which may be inverted based on the difference
/// between the calculated timestamp and the parsed timestamp.
///
public double[] CorrectedValues
{
get
{
return m_correctedValues;
}
}
///
/// Gets event groups for current record.
///
public ushort[] EventGroups
{
get
{
return m_parser.EventGroups;
}
}
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
CloseFiles();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
///
/// Opens all EMAX data file streams.
///
public void OpenFiles()
{
long previousMilliseconds;
long milliseconds;
int count;
int sampleRate;
int distributionIndex;
m_parser.OpenFiles();
sampleRate = m_parser.ControlFile.SystemParameters.samples_per_second;
m_subsecondDistribution = Ticks.SubsecondDistribution(sampleRate);
using (Parser parser = new Parser())
{
parser.ControlFile = m_parser.ControlFile;
parser.FileName = m_parser.FileName;
// Open EMAX data file
parser.OpenFiles();
m_currentSecond = DateTime.MinValue;
previousMilliseconds = -1;
milliseconds = 0;
count = 0;
while (parser.ReadNext())
{
if (parser.TimeError)
continue;
// Set currentSecond to this frame's timestamp
m_currentSecond = parser.Timestamp;
// Get total milliseconds since epoch
milliseconds = m_currentSecond.Ticks / Ticks.PerMillisecond;
// If the milliseconds are exactly one millisecond greater than the previous
// timestamp's milliseconds, we can accurately find the timestamp of this frame
if (previousMilliseconds > 0 && milliseconds - previousMilliseconds == 1)
break;
// Update previousMilliseconds and count
previousMilliseconds = milliseconds;
count++;
}
if (m_currentSecond == DateTime.MinValue)
{
// If there was an error reading the timestamps in the parser,
// calculate the timestamp from the data in the control file
TimeZoneInfo tzInfo = parser.ControlFile.SystemParameters.GetTimeZoneInfo();
DateTime faultTime = parser.ControlFile.SystemParameters.FaultTime;
string daylightName = parser.ControlFile.SystemParameters.time_zone_information.DaylightName;
short faultMilliseconds = parser.ControlFile.SystemParameters.mS_time;
short prefaultSamples = parser.ControlFile.SystemParameters.prefault_samples;
short startOffsetSamples = parser.ControlFile.SystemParameters.start_offset_samples;
// Set currentSecond to the timestamp of the first frame
m_currentSecond = faultTime.AddMilliseconds(faultMilliseconds).AddSeconds(-(prefaultSamples + startOffsetSamples) / (double)sampleRate);
// Get total milliseconds since epoch
milliseconds = m_currentSecond.Ticks / Ticks.PerMillisecond;
// Timestamps in the control file are stored in UTC (or close to it),
// but it seems that sometimes EMAX adjusts timestamps without applying DST
// rules so we use the DaylightName of the time zone info as a sanity check
// and fall back on the BaseUtcOffset property in these weird cases
if (daylightName == tzInfo.DaylightName || daylightName != tzInfo.Id)
m_currentSecond = TimeZoneInfo.ConvertTimeFromUtc(m_currentSecond, tzInfo);
else
m_currentSecond += tzInfo.BaseUtcOffset;
m_currentSecond = DateTime.SpecifyKind(m_currentSecond, DateTimeKind.Unspecified);
}
// Remove subseconds from currentSecond
m_currentSecond = m_currentSecond.AddTicks(-(m_currentSecond.Ticks % Ticks.PerSecond));
// Get the milliseconds since the top of the second
milliseconds %= 1000;
// This should get very near to the index of the
// desired value in the subsecond distribution
distributionIndex = ((int)milliseconds * sampleRate) / 1000;
// Scan forward in the subsecond distribution until we are sure we've found the correct index
while ((long)m_subsecondDistribution[distributionIndex].ToMilliseconds() != milliseconds)
distributionIndex++;
// Set currentIndex to the index in the distribution of the first timestamp in this file
m_currentIndex = distributionIndex - count;
// Subtract seconds from currentSecond and add the equivalent number of indexes
// to currentIndex until currentIndex is greater than or equal to zero
while (m_currentIndex < 0)
{
m_currentSecond = m_currentSecond.AddSeconds(-1.0D);
m_currentIndex += sampleRate;
}
}
}
///
/// Closes all EMAX data file streams.
///
public void CloseFiles()
{
if ((object)m_parser != null)
{
m_parser.Dispose();
m_parser = null;
}
}
///
/// Reads next EMAX record.
///
/// true if read succeeded; otherwise false if end of data set was reached.
public bool ReadNext()
{
double frequency;
double diff;
int halfCycles;
if (m_parser.ReadNext())
{
// Calculate the timestamp
m_calculatedTimestamp = m_currentSecond.AddTicks(m_subsecondDistribution[m_currentIndex]);
if (!m_parser.TimeError)
{
// Correct the values
frequency = m_parser.ControlFile.SystemParameters.frequency;
diff = Math.Abs(m_calculatedTimestamp.Subtract(m_parser.Timestamp).TotalSeconds);
halfCycles = (int)Math.Round(diff * frequency * 2.0D);
if (halfCycles % 2 == 0)
Array.Copy(Values, m_correctedValues, m_correctedValues.Length);
else
Values.Select(v => -v).ToList().CopyTo(m_correctedValues);
}
else
{
// We can't determine whether the values need correction
// without a proper timestamp from the underlying parser
Array.Copy(Values, m_correctedValues, m_correctedValues.Length);
}
// Move to the next subsecond in the distribution
m_currentIndex++;
if (m_currentIndex == m_subsecondDistribution.Length)
{
// If we reach the end of the subsecond distribution,
// add one to currentSecond and reset the currentIndex to zero
m_currentSecond = m_currentSecond.AddSeconds(1.0D);
m_currentIndex = 0;
}
return true;
}
return false;
}
#endregion
}
}