//******************************************************************************************************
// Schema.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/18/2011 - J. Ritchie Carroll
// Generated original version of source code.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Text;
using GSF.IO;
namespace GSF.COMTRADE
{
#region [ Enumerations ]
///
/// file type enumeration.
///
public enum FileType
{
///
/// ASCII file type.
///
Ascii,
///
/// Binary file type, analog channel size = uint16.
///
Binary,
///
/// Binary file type, analog channel size = uint32.
///
Binary32,
///
/// Binary file type, analog channel size = float32.
///
Float32
}
///
/// time quality indicator code.
///
public enum TimeQualityIndicatorCode : byte
{
///
/// Clock locked, Normal operation.
///
Locked = 0x0,
///
/// Clock fault, time not reliable.
///
Failure = 0xF,
///
/// Clock unlocked, time within 10^1s.
///
Unlocked10Seconds = 0xB,
///
/// Clock unlocked, time within 10^0s.
///
Unlocked1Second = 0xA,
///
/// Clock unlocked, time within 10^-1s.
///
UnlockedPoint1Seconds = 0x9,
///
/// Clock unlocked, time within 10^-2s.
///
UnlockedPoint01Seconds = 0x8,
///
/// Clock unlocked, time within 10^-3s.
///
UnlockedPoint001Seconds = 0x7,
///
/// Clock unlocked, time within 10^-4s.
///
UnlockedPoint0001Seconds = 0x6,
///
/// Clock unlocked, time within 10^-5s.
///
UnlockedPoint00001Seconds = 0x5,
///
/// Clock unlocked, time within 10^-6s.
///
UnlockedPoint000001Seconds = 0x4,
///
/// Clock unlocked, time within 10^-7s.
///
UnlockedPoint0000001Seconds = 0x3,
///
/// Clock unlocked, time within 10^-8s.
///
UnlockedPoint00000001Seconds = 0x2,
///
/// Clock unlocked, time within 10^-9s.
///
UnlockedPoint000000001Seconds = 0x1
}
// 4-bits, 1-nibble
///
/// leap second indicator.
///
public enum LeapSecondIndicator : byte
{
///
/// No leap second adjustment was added during recorded data set.
///
NoLeapSecondAdjustment = 0,
///
/// Leap second adjustment was added during recorded data set.
///
LeapSecondWasAdded = 1,
///
/// Leap second adjustment was subtracted during recorded data set.
///
LeapSecondWasSubtracted = 2,
///
/// Leap second adjustment not capable for recording device.
///
NoLeapSecondCapacity = 3
}
#endregion
///
/// Represents the schema for a configuration file in the COMTRADE file standard, IEEE Std C37.111-1999/2013.
///
public class Schema
{
#region [ Members ]
// Fields
private double m_nominalFrequency;
private SampleRate[] m_sampleRates;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the .
///
public Schema()
{
Version = 1999;
m_nominalFrequency = 60.0D;
FileType = FileType.Binary;
TimeFactor = 1.0D;
SampleRates = null;
}
///
/// Creates a new instance of the from an existing configuration file name.
///
/// File name of configuration file to parse.
public Schema(string fileName)
: this(fileName, false)
{
}
///
/// Creates a new instance of the from an existing configuration file name.
///
/// File name of configuration file to parse.
/// Indicates whether to relax validation on the number of line image elements.
public Schema(string fileName, bool useRelaxedValidation)
{
if (!File.Exists(fileName))
throw new FileNotFoundException($"Configuration file \"{fileName}\" does not exist.");
// Cache configuration file name used to create schema
FileName = fileName;
IsCombinedFileFormat = HasCFFExtension(FileName);
string[] lines;
if (IsCombinedFileFormat)
{
// Read out configuration file section
using (StreamReader fileReader = new StreamReader(FileName))
{
List fileLines = new List();
bool firstLine = true;
do
{
string line = fileReader.ReadLine();
if (firstLine)
{
if (!IsFileSectionSeparator(line, out string sectionType) || sectionType != "CFG")
throw new InvalidOperationException($"Unexpected file section separator for configuration file type: expected \"--- file type: CFG ---\"{Environment.NewLine}Image = {line}");
firstLine = false;
continue;
}
if (line is null || IsFileSectionSeparator(line))
break;
fileLines.Add(line);
}
while (true);
lines = fileLines.ToArray();
}
}
else
{
lines = File.ReadAllLines(fileName);
}
int lineNumber = 0;
// Parse version line
string[] parts = lines[lineNumber++].Split(',');
if (parts.Length < 2 || (!useRelaxedValidation && parts.Length != 2 && parts.Length != 3))
throw new InvalidOperationException($"Unexpected number of line image elements for first configuration file line: {parts.Length} - expected 2 or 3{Environment.NewLine}Image = {lines[lineNumber - 1]}");
StationName = parts[0].Trim();
DeviceID = parts[1].Trim();
if (parts.Length >= 3 && !string.IsNullOrWhiteSpace(parts[2]))
Version = int.Parse(parts[2].Trim());
else
Version = 1991;
// Parse totals line
parts = lines[lineNumber++].Split(',');
if (parts.Length < 3 || (!useRelaxedValidation && parts.Length != 3))
throw new InvalidOperationException($"Unexpected number of line image elements for second configuration file line: {parts.Length} - expected 3{Environment.NewLine}Image = {lines[lineNumber - 1]}");
int totalChannels = int.Parse(parts[0].Trim());
int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0]);
int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0]);
if (totalChannels != totalAnalogChannels + totalDigitalChannels)
throw new InvalidOperationException($"Total defined channels must equal the sum of the total number of analog and digital channel definitions.{Environment.NewLine}Image = {lines[lineNumber - 1]}");
// Cache analog line image definitions - will parse after file type is known
List analogLineImages = new List();
for (int i = 0; i < totalAnalogChannels; i++)
analogLineImages.Add(lines[lineNumber++]);
// Parse digital definitions
List digitalChannels = new List();
for (int i = 0; i < totalDigitalChannels; i++)
digitalChannels.Add(new DigitalChannel(lines[lineNumber++], Version, useRelaxedValidation));
DigitalChannels = digitalChannels.ToArray();
// Parse line frequency
NominalFrequency = double.Parse(lines[lineNumber++]);
// Parse total number of sample rates
int totalSampleRates = int.Parse(lines[lineNumber++]);
if (totalSampleRates == 0)
totalSampleRates = 1;
// Parse each sample rate
List sampleRates = new List();
for (int i = 0; i < totalSampleRates; i++)
sampleRates.Add(new SampleRate(lines[lineNumber++], useRelaxedValidation));
SampleRates = sampleRates.ToArray();
// Parse timestamps
StartTime = new Timestamp(lines[lineNumber++]);
TriggerTime = new Timestamp(lines[lineNumber++]);
// Parse file type
Enum.TryParse(lines[lineNumber++], true, out FileType fileType);
FileType = fileType;
// Parse analog definitions - we do this after knowing file type to better assign default linear scaling factors
List analogChannels = new List();
bool targetFloatingPoint = fileType == FileType.Float32;
for (int i = 0; i < analogLineImages.Count; i++)
analogChannels.Add(new AnalogChannel(analogLineImages[i], Version, targetFloatingPoint, useRelaxedValidation));
AnalogChannels = analogChannels.ToArray();
// Parse time factor
TimeFactor = lines.Length < lineNumber ? double.Parse(lines[lineNumber++]) : 1;
// Parse time information line
if (lines.Length < lineNumber)
{
parts = lines[lineNumber++].Split(',');
if (parts.Length > 0)
TimeCode = new TimeOffset(parts[0].Trim());
if (parts.Length > 1)
LocalCode = new TimeOffset(parts[1].Trim());
}
// Parse time state line
if (lines.Length < lineNumber)
{
parts = lines[lineNumber/*++*/].Split(',');
if (parts.Length > 0)
TimeQualityIndicatorCode = (TimeQualityIndicatorCode)byte.Parse(parts[0], NumberStyles.HexNumber);
if (parts.Length > 1)
LeapSecondIndicator = (LeapSecondIndicator)byte.Parse(parts[1]);
}
}
#endregion
#region [ Properties ]
///
/// Gets the file name used to generate this schema when constructed with one; otherwise, null.
///
public string FileName { get; }
///
/// Gets flag that determines if is a Combined File Format (.cff) file.
///
public bool IsCombinedFileFormat { get; }
///
/// Gets or sets free-form station name for this .
///
public string StationName { get; set; }
///
/// Gets or sets free-form device ID for this .
///
public string DeviceID { get; set; }
///
/// Gets or sets version number of the IEEE Std C37.111 used by this .
///
public int Version { get; set; }
///
/// Gets total number of analog and digital channels of this .
///
public int TotalChannels => TotalAnalogChannels + TotalDigitalChannels;
///
/// Gets total number of analog channels of this .
///
public int TotalAnalogChannels => AnalogChannels?.Length ?? 0;
///
/// Gets total number of digital channels of this .
///
public int TotalDigitalChannels => DigitalChannels?.Length ?? 0;
///
/// Gets or sets analog channels of this .
///
public AnalogChannel[] AnalogChannels { get; set; }
///
/// Gets or sets digital channels of this .
///
public DigitalChannel[] DigitalChannels { get; set; }
///
/// Gets or sets nominal frequency of this .
///
public double NominalFrequency
{
get => m_nominalFrequency;
set
{
m_nominalFrequency = value;
// Cascade nominal frequency update to analog channels
if (!(AnalogChannels is null))
{
foreach (AnalogChannel analogChannel in AnalogChannels)
analogChannel.NominalFrequency = m_nominalFrequency;
}
}
}
///
/// Gets or sets sampling rates of this . A file of phasor data will normally be made using a single sampling rate, so this will usually be 1.
///
public SampleRate[] SampleRates
{
get => m_sampleRates;
set => m_sampleRates = value ?? new[] { new SampleRate { Rate = 0, EndSample = 1 } };
}
///
/// Gets total number of sample rates of this .
///
public int TotalSampleRates => m_sampleRates.Length == 1 && m_sampleRates[0].Rate == 0.0D ? 0 : m_sampleRates.Length;
///
/// Gets total number of samples, i.e., rows per timestamp, as reported by the sample rates.
///
public long TotalSamples => m_sampleRates.Max(sampleRate => sampleRate.EndSample);
///
/// Gets total number of channels values, i.e., TotalChannels * TotalSamples.
///
public long TotalChannelValues => TotalChannels * TotalSamples;
///
/// Gets or sets start timestamp of this .
///
public Timestamp StartTime { get; set; }
///
/// Gets or sets trigger timestamp of this .
///
public Timestamp TriggerTime { get; set; }
///
/// Gets or sets file type of this .
///
public FileType FileType { get; set; }
///
/// Gets or sets the multiplication factor for the time differential (timestamp) field in this .
///
public double TimeFactor { get; set; }
///
/// Gets or sets UTC offset for channel timestamps - format HhMM.
///
public TimeOffset TimeCode { get; set; } = new TimeOffset("0h00");
///
/// Gets or sets UTC offset for timezone of recording device, or "x" for not applicable - format HhMM.
///
public TimeOffset LocalCode { get; set; } = new TimeOffset("x");
///
/// Gets or sets the time quality indicator code for the recorded data set.
///
public TimeQualityIndicatorCode TimeQualityIndicatorCode { get; set; } = TimeQualityIndicatorCode.Locked;
///
/// Gets or sets the leap second indicator for the recorded data set.
///
public LeapSecondIndicator LeapSecondIndicator { get; set; } = LeapSecondIndicator.NoLeapSecondAdjustment;
///
/// Gets the total digital words for given number of digital values when the file type is binary.
///
public int DigitalWords => (int)Math.Ceiling(DigitalChannels.Length / 16.0D);
///
/// Calculates the size of a record, in bytes, when the file type is binary.
///
public int BinaryRecordLength => 8 + 2 * AnalogChannels.Length + 2 * DigitalWords;
///
/// Calculates the size of a record, in bytes, when the file type is binary32.
///
public int Binary32RecordLength => 8 + 4 * AnalogChannels.Length + 2 * DigitalWords;
///
/// Calculates the size of a record, in bytes, when the file type is float32.
///
public int Float32RecordLength => 8 + 4 * AnalogChannels.Length + 2 * DigitalWords;
///
/// Gets the file image of this .
///
[JsonIgnore]
public string FileImage
{
get
{
StringBuilder fileImage = new StringBuilder();
void appendLine(string line)
{
// The standard .NET "Environment.NewLine" constant can just be a line feed on some operating systems,
// but the COMTRADE standard requires that end of line markers be both a carriage return and line feed.
fileImage.Append(line);
fileImage.Append(Writer.CRLF);
}
// Write version line
appendLine($"{StationName},{DeviceID}{(Version >= 1999 ? $",{Version}" : "")}");
// Write totals line
appendLine($"{TotalChannels},{TotalAnalogChannels}A,{TotalDigitalChannels}D");
// Write analog definitions
for (int i = 0; i < TotalAnalogChannels; i++)
appendLine(AnalogChannels[i].ToString());
// Write digital definitions
for (int i = 0; i < TotalDigitalChannels; i++)
appendLine(DigitalChannels[i].ToString());
// Write line frequency
appendLine(NominalFrequency.ToString(CultureInfo.InvariantCulture));
// Write total number of sample rates (zero signifies no fixed sample rates)
appendLine(TotalSampleRates.ToString());
int totalSampleRates = TotalSampleRates;
if (totalSampleRates == 0)
totalSampleRates = 1;
// Write sample rates
for (int i = 0; i < totalSampleRates; i++)
appendLine(SampleRates[i].ToString());
// Write timestamps
appendLine(StartTime.ToString());
appendLine(TriggerTime.ToString());
// Write file type
appendLine(FileType.ToString().ToUpper());
// Write time factor
if (Version >= 1999)
appendLine(TimeFactor.ToString(CultureInfo.InvariantCulture));
// Write per data set time info and state
if (Version >= 2013)
{
appendLine($"{TimeCode},{LocalCode}");
appendLine($"{(byte)TimeQualityIndicatorCode:X},{(byte)LeapSecondIndicator}");
}
return fileImage.ToString();
}
}
#endregion
#region [ Static ]
// Static Methods
internal static bool HasCFFExtension(string fileName) =>
!(fileName is null) && string.Equals(FilePath.GetExtension(fileName), ".cff", StringComparison.OrdinalIgnoreCase);
internal static bool IsFileSectionSeparator(string line) =>
IsFileSectionSeparator(line, out _, out _);
internal static bool IsFileSectionSeparator(string line, out string sectionType) =>
IsFileSectionSeparator(line, out sectionType, out _);
internal static bool IsFileSectionSeparator(string line, out string sectionType, out long byteCount)
{
if (line?.Trim().StartsWith("---") ?? false)
{
string[] parts = line.Replace("---", "").Trim().Split(':');
if (parts.Length >= 2 && string.Equals(parts[0].Trim(), "file type", StringComparison.OrdinalIgnoreCase))
{
sectionType = parts[1].Trim().ToUpperInvariant();
if (parts.Length > 2 && sectionType == "DAT BINARY")
long.TryParse(parts[2].Trim(), out byteCount);
else
byteCount = default;
return true;
}
}
sectionType = default;
byteCount = default;
return false;
}
#endregion
}
}