/// <summary> /// Creates a new COMTRADE configuration <see cref="Schema"/>. /// </summary> /// <param name="metadata">Schema <see cref="ChannelMetadata"/> records.</param> /// <param name="stationName">Station name for the schema.</param> /// <param name="deviceID">Device ID for the schema.</param> /// <param name="dataStartTime">Data start time.</param> /// <param name="sampleCount">Total data samples (i.e., total number of rows).</param> /// <param name="isBinary">Determines if data file should be binary or ASCII - defaults to <c>true</c> for binary.</param> /// <param name="timeFactor">Time factor to use in schema - defaults to 1000.</param> /// <param name="samplingRate">Desired sampling rate - defaults to 33.3333Hz.</param> /// <param name="nominalFrequency">Nominal frequency - defaults to 60Hz.</param> /// <param name="includeFracSecDefinition">Determines if the FRACSEC word digital definitions should be included - defaults to <c>true</c>.</param> /// <returns>New COMTRADE configuration <see cref="Schema"/>.</returns> /// <remarks> /// This function is primarily intended to create a configuration based on synchrophasor data /// (see Annex H: Schema for Phasor Data 2150 Using the COMTRADE File Standard in IEEE C37.111-2010), /// it may be necessary to manually create a schema object for other COMTRADE needs. You can call /// the <see cref="Schema.FileImage"/> property to return a string that that can be written to a file /// that will be the contents of the configuration file. /// </remarks> public static Schema CreateSchema(IEnumerable<ChannelMetadata> metadata, string stationName, string deviceID, Ticks dataStartTime, int sampleCount, bool isBinary = true, double timeFactor = 1.0D, double samplingRate = 30.0D, double nominalFrequency = 60.0D, bool includeFracSecDefinition = true) { Schema schema = new Schema(); schema.StationName = stationName; schema.DeviceID = deviceID; SampleRate samplingFrequency = new SampleRate(); samplingFrequency.Rate = samplingRate; samplingFrequency.EndSample = sampleCount; schema.SampleRates = new[] { samplingFrequency }; Timestamp startTime; startTime.Value = dataStartTime; schema.StartTime = startTime; schema.TriggerTime = startTime; schema.FileType = isBinary ? FileType.Binary : FileType.Ascii; schema.TimeFactor = timeFactor; List<AnalogChannel> analogChannels = new List<AnalogChannel>(); List<DigitalChannel> digitalChannels = new List<DigitalChannel>(); int analogIndex = 1; int digitalIndex = 1; if (includeFracSecDefinition) { // Add default time quality digitals for IEEE C37.118 FRACSEC word. Note that these flags, as // defined in Annex H of the IEEE C37.111-2010 standard, assume full export was all from one // source device. This a poor assumption since data can be exported from historical data for any // number of points which could have come from any number of devices all with different FRACSEC // values. Regardless there is only one FRACSEC definition defined and, if included, it must // come as the first set of digitals in the COMTRADE configuration. for (int i = 0; i < 4; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_CNT" + i, PhaseID = "T" + digitalIndex++ }); } digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSPND", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSOCC", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSDIR", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "RSV", PhaseID = "T" + digitalIndex++ }); for (int i = 1; i < 9; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "RESV" + i, PhaseID = "T" + digitalIndex++ }); } } // Add meta data for selected points sorted analogs followed by status flags then digitals foreach (ChannelMetadata record in metadata.OrderBy(m => m, ChannelMetadataSorter.Default)) { if (record.IsDigital) { // Every synchrophasor digital is 16-bits for (int i = 0; i < 16; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name, PhaseID = "B" + i.ToString("X") }); } } else { switch (record.SignalType) { case SignalType.IPHM: // Current Magnitude analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pm", Units = "A", Multiplier = 0.05D }); break; case SignalType.VPHM: // Voltage Magnitude analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pm", Units = "V", Multiplier = 5.77362D }); break; case SignalType.IPHA: // Current Phase Angle case SignalType.VPHA: // Voltage Phase Angle analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pa", Units = "Rads", Multiplier = 1.0E-4D }); break; case SignalType.FREQ: // Frequency analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "F", Units = "Hz", Adder = (double)nominalFrequency, Multiplier = 0.001D }); break; case SignalType.DFDT: // Frequency Delta (dF/dt) analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "dF", Units = "Hz/s", Multiplier = 0.01D }); break; case SignalType.FLAG: // Status flags // Add synchrophasor status flag specific digitals int statusIndex = 0; for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":TRG" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 3; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":UNLK" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SEC" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":CFGCH", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":PMUTR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SORT", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SYNC", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":PMUERR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":DTVLD", PhaseID = "S" + statusIndex.ToString("X") }); break; default: // All other signals assumed to be analog values analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "" }); break; } } } schema.AnalogChannels = analogChannels.ToArray(); schema.DigitalChannels = digitalChannels.ToArray(); schema.NominalFrequency = nominalFrequency; return schema; }
/// <summary> /// Writes next COMTRADE record in ASCII format. /// </summary> /// <param name="output">Destination stream.</param> /// <param name="schema">Source schema.</param> /// <param name="timestamp">Record timestamp (implicitly castable as <see cref="DateTime"/>).</param> /// <param name="values">Values to write - 16-bit digitals should exist as a word in an individual double value, method will write out bits.</param> /// <param name="sample">User incremented sample index.</param> /// <param name="injectFracSecValue">Determines if FRACSEC value should be automatically injected into stream as first digital - defaults to <c>true</c>.</param> /// <param name="fracSecValue">FRACSEC value to inject into output stream - defaults to 0x0000.</param> /// <remarks> /// This function is primarily intended to write COMTRADE ASCII data records based on synchrophasor data /// (see Annex H: Schema for Phasor Data 2150 Using the COMTRADE File Standard in IEEE C37.111-2010), /// it may be necessary to manually write records for other COMTRADE needs (e.g., non 16-bit digitals). /// </remarks> public static void WriteNextRecordAscii(StreamWriter output, Schema schema, Ticks timestamp, double[] values, uint sample, bool injectFracSecValue = true, ushort fracSecValue = 0x0000) { // Make timestamp relative to beginning of file timestamp -= schema.StartTime.Value; uint microseconds = (uint)(timestamp.ToMicroseconds() / schema.TimeFactor); double value; StringBuilder line = new StringBuilder(); bool isFirstDigital = true; line.Append(sample); line.Append(','); line.Append(microseconds); for (int i = 0; i < values.Length; i++) { value = values[i]; if (i < schema.AnalogChannels.Length) { value -= schema.AnalogChannels[i].Adder; value /= schema.AnalogChannels[i].Multiplier; line.Append(','); line.Append(value); } else { if (isFirstDigital) { // Handle automatic injection of IEEE C37.118 FRACSEC digital value if requested isFirstDigital = false; if (injectFracSecValue) { for (int j = 0; j < 16; j++) { line.Append(','); line.Append(fracSecValue.CheckBits(BitExtensions.BitVal(j)) ? 1 : 0); } } } ushort digitalWord = (ushort)value; for (int j = 0; j < 16; j++) { line.Append(','); line.Append(digitalWord.CheckBits(BitExtensions.BitVal(j)) ? 1 : 0); } } } // Make sure FRACSEC values are injected if (isFirstDigital && injectFracSecValue) { for (int j = 0; j < 16; j++) { line.Append(','); line.Append(fracSecValue.CheckBits(BitExtensions.BitVal(j)) ? 1 : 0); } } output.WriteLine(line.ToString()); }
/// <summary> /// Writes next COMTRADE record in binary format. /// </summary> /// <param name="output">Destination stream.</param> /// <param name="schema">Source schema.</param> /// <param name="timestamp">Record timestamp (implicitly castable as <see cref="DateTime"/>).</param> /// <param name="values">Values to write - 16-bit digitals should exist as a word in an individual double value.</param> /// <param name="sample">User incremented sample index.</param> /// <param name="injectFracSecValue">Determines if FRACSEC value should be automatically injected into stream as first digital - defaults to <c>true</c>.</param> /// <param name="fracSecValue">FRACSEC value to inject into output stream - defaults to 0x0000.</param> /// <remarks> /// This function is primarily intended to write COMTRADE binary data records based on synchrophasor data /// (see Annex H: Schema for Phasor Data 2150 Using the COMTRADE File Standard in IEEE C37.111-2010), /// it may be necessary to manually write records for other COMTRADE needs (e.g., non 16-bit digitals). /// </remarks> public static void WriteNextRecordBinary(Stream output, Schema schema, Ticks timestamp, double[] values, uint sample, bool injectFracSecValue = true, ushort fracSecValue = 0x0000) { // Make timestamp relative to beginning of file timestamp -= schema.StartTime.Value; uint microseconds = (uint)(timestamp.ToMicroseconds() / schema.TimeFactor); double value; bool isFirstDigital = true; output.Write(LittleEndian.GetBytes(sample), 0, 4); output.Write(LittleEndian.GetBytes(microseconds), 0, 4); for (int i = 0; i < values.Length; i++) { value = values[i]; if (i < schema.AnalogChannels.Length) { value -= schema.AnalogChannels[i].Adder; value /= schema.AnalogChannels[i].Multiplier; } else if (isFirstDigital) { // Handle automatic injection of IEEE C37.118 FRACSEC digital value if requested isFirstDigital = false; if (injectFracSecValue) output.Write(LittleEndian.GetBytes(fracSecValue), 0, 2); } output.Write(LittleEndian.GetBytes((ushort)value), 0, 2); } // Make sure FRACSEC values are injected if (isFirstDigital && injectFracSecValue) output.Write(LittleEndian.GetBytes(fracSecValue), 0, 2); }
private void WriteDataFile(COMTRADEData comtradeData, Schema schema, string dataFilePath) { // Function to get the value at a given index from a data series Func<DataSeries, int, double> valueAt = (series, i) => (i < series.DataPoints.Count) ? series.DataPoints[i].Value : series.DataPoints.Last().Value; // Get the list of data series for every channel in the COMTRADE file IEnumerable<DataSeries> digitalSeriesList = comtradeData.DigitalChannelData .Select(channelData => channelData.Data) .Select((series, index) => series.Multiply(Math.Pow(2.0D, index % 16))) .Select((series, index) => Tuple.Create(index / 16, series)) .GroupBy(tuple => tuple.Item1) .Select(group => group.Select(tuple => tuple.Item2)) .Select(group => group.Aggregate((sum, series) => sum.Add(series))); List<DataSeries> allChannels = comtradeData.AnalogChannelData .Select(channelData => channelData.Data) .Concat(digitalSeriesList) .ToList(); // Use the longest data series as the series // from which time values will be used DataSeries timeSeries = allChannels .OrderByDescending(series => series.DataPoints.Count) .First(); // Open the data file for writing using (FileStream fileStream = File.Create(FilePath.GetAbsolutePath(dataFilePath))) { // Write the timestamp and values to each line of the data file for (int i = 0; i < comtradeData.SampleCount; i++) { Ticks timestamp = timeSeries.DataPoints[i].Time; double[] values = allChannels .Select(series => valueAt(series, i)) .ToArray(); Writer.WriteNextRecordBinary(fileStream, schema, timestamp, values, (uint)i, false); } } }
/// <summary> /// Populate known voltage and current data from PQDIF file. /// </summary> /// <param name="faultDataSet">Fault data set to be populated.</param> /// <param name="settings">Source parameters.</param> /// <param name="line">Associated XML event file definition.</param> public static void PopulateDataSet(FaultLocationDataSet faultDataSet, Dictionary<string, string> settings, Line line) { string fileName; if ((object)line == null) throw new ArgumentNullException("line"); if (!settings.TryGetValue("fileName", out fileName) || !File.Exists(fileName)) throw new ArgumentException("Parameters must define a valid \"fileName\" setting."); // Comtrade parsing will require a CFG file, make sure this exists... string directory = Path.GetDirectoryName(fileName) ?? string.Empty; string rootFileName = FilePath.GetFileNameWithoutExtension(fileName); string configurationFileName = Path.Combine(directory, rootFileName + ".cfg"); if (!File.Exists(configurationFileName)) throw new FileNotFoundException(string.Format("Associated CFG file \"{0}\" for COMTRADE data file does not exist - cannot parse COMTRADE file.", configurationFileName)); // Parse configuration file Schema schema = new Schema(configurationFileName); // Find <Channels> element in XML line definition XElement channels = line.ChannelsElement; if ((object)channels == null) throw new NullReferenceException("No \"<channels>\" element was found in event file definition - cannot load COMTRADE data file."); // Extract COMTRADE channel ID's for desired voltage and current elements IEnumerable<Tuple<int, int>> vaIndexes = GetValueIndex(schema, channels, "VA").ToList(); IEnumerable<Tuple<int, int>> vbIndexes = GetValueIndex(schema, channels, "VB").ToList(); IEnumerable<Tuple<int, int>> vcIndexes = GetValueIndex(schema, channels, "VC").ToList(); IEnumerable<Tuple<int, int>> iaIndexes = GetValueIndex(schema, channels, "IA").ToList(); IEnumerable<Tuple<int, int>> ibIndexes = GetValueIndex(schema, channels, "IB").ToList(); IEnumerable<Tuple<int, int>> icIndexes = GetValueIndex(schema, channels, "IC").ToList(); List<long> times = new List<long>(); List<double> vaValues = new List<double>(); List<double> vbValues = new List<double>(); List<double> vcValues = new List<double>(); List<double> iaValues = new List<double>(); List<double> ibValues = new List<double>(); List<double> icValues = new List<double>(); SampleRate sampleRate; ValidateIndexes("VA", vaIndexes); ValidateIndexes("VB", vbIndexes); ValidateIndexes("VC", vcIndexes); ValidateIndexes("IA", iaIndexes); ValidateIndexes("IB", ibIndexes); ValidateIndexes("IC", icIndexes); // Create a new COMTRADE file parser using (Parser parser = new Parser() { Schema = schema, FileName = fileName, InferTimeFromSampleRates = true }) { // Open COMTRADE data files parser.OpenFiles(); faultDataSet.Frequency = schema.NominalFrequency; sampleRate = schema.SampleRates.First(); if (sampleRate.Rate != 0) faultDataSet.SetSampleRates((int)(sampleRate.Rate / faultDataSet.Frequency)); // Read all COMTRADE records while (parser.ReadNext()) { times.Add(parser.Timestamp.Ticks); vaValues.Add(GetValue(parser, vaIndexes)); vbValues.Add(GetValue(parser, vbIndexes)); vcValues.Add(GetValue(parser, vcIndexes)); iaValues.Add(GetValue(parser, iaIndexes)); ibValues.Add(GetValue(parser, ibIndexes)); icValues.Add(GetValue(parser, icIndexes)); } } // Populate voltage data set faultDataSet.Voltages.AN.Times = times.ToArray(); faultDataSet.Voltages.AN.Measurements = vaValues.ToArray(); faultDataSet.Voltages.BN.Times = times.ToArray(); faultDataSet.Voltages.BN.Measurements = vbValues.ToArray(); faultDataSet.Voltages.CN.Times = times.ToArray(); faultDataSet.Voltages.CN.Measurements = vcValues.ToArray(); // Populate current data set faultDataSet.Currents.AN.Times = times.ToArray(); faultDataSet.Currents.AN.Measurements = iaValues.ToArray(); faultDataSet.Currents.BN.Times = times.ToArray(); faultDataSet.Currents.BN.Measurements = ibValues.ToArray(); faultDataSet.Currents.CN.Times = times.ToArray(); faultDataSet.Currents.CN.Measurements = icValues.ToArray(); }
// Gets the actual analog value indexes based on the schema based channel indexes private static IEnumerable<Tuple<int, int>> GetValueIndex(Schema schema, XElement channels, string channelName) { XElement element = channels.Element(channelName); string[] channelValues; int channelIndex, multiplier; string units; if ((object)element == null) throw new NullReferenceException(string.Format("No \"{0}\" element was found in defined \"<Channels>\" of device definition - cannot load COMTRADE data file.", channelName)); channelValues = element.Value.Split(','); // Index defined in COMTRADE schema may start at any number, return actual index into value array foreach (string channelValue in channelValues) { if (!int.TryParse(channelValue, out channelIndex)) throw new InvalidOperationException(string.Format("The \"{0}\" element in the device definition was not a comma-separated list of integers - cannot load COMTRADE data file.", channelName)); channelIndex = Math.Abs(channelIndex); multiplier = (channelValue.Trim()[0] == '-') ? -1 : 1; for (int valueIndex = 0; valueIndex < schema.AnalogChannels.Length; valueIndex++) { if (schema.AnalogChannels[valueIndex].Index == channelIndex) { units = schema.AnalogChannels[valueIndex].Units.ToUpper(); if (units.Contains("KA") || units.Contains("KV")) multiplier *= 1000; yield return new Tuple<int, int>(multiplier, valueIndex); break; } } } }
public static void Write(Meter meter, DataGroup waveFormData, FaultLocationData.FaultCurveDataTable faultCurveTable, List<FaultSegment> segments, string originalFilePath, string filePath) { List<DataSeries> waveFormSeriesList = GetWaveFormSeriesList(waveFormData); DataGroup faultLocationData = GetFaultLocationData(meter, faultCurveTable); string absoluteFilePath = FilePath.GetAbsolutePath(filePath); using (StreamWriter fileStream = new StreamWriter(File.OpenWrite(absoluteFilePath))) { string originalDirectory; string originalRootFileName; string originalSchemaFilePath; string absoluteOriginalFilePath; Schema originalSchema = null; string headerRow; absoluteOriginalFilePath = FilePath.GetAbsolutePath(originalFilePath); if (File.Exists(absoluteOriginalFilePath)) { originalDirectory = FilePath.GetDirectoryName(absoluteOriginalFilePath); originalRootFileName = FilePath.GetFileNameWithoutExtension(originalFilePath); originalSchemaFilePath = Path.Combine(originalDirectory, originalRootFileName + ".cfg"); originalSchema = new Schema(originalSchemaFilePath); } headerRow = waveFormData.DataSeries .Select(series => GetOriginalChannelName(originalSchema, series)) .Concat(faultCurveTable.Select(row => string.Format("Fault Location ({0} Algorithm)", row.Algorithm))) .Aggregate("Time", (s, s1) => s + "," + s1); fileStream.WriteLine(headerRow); for (int i = 0; i < waveFormData.Samples; i++) { DateTime time = waveFormSeriesList[0].DataPoints[i].Time; double[] values = waveFormSeriesList .Select(series => series.DataPoints[i].Value) .Concat(faultLocationData.DataSeries.Select(series => series.DataPoints.Count > i ? series.DataPoints[i].Value : 0.0D)) .ToArray(); fileStream.WriteLine(values.Aggregate(time.ToString("yyyy-MM-dd HH:mm:ss.ffffff"), (s, d) => s + "," + d)); } } }
private static string GetOriginalChannelName(Schema originalSchema, DataSeries series) { int index; return ((object)originalSchema != null && int.TryParse(series.SeriesInfo.SourceIndexes, out index)) ? originalSchema.AnalogChannels[Math.Abs(index) - 1].Name : series.SeriesInfo.Channel.Name; }