Skip to content
Snippets Groups Projects
Commit 33f9be60 authored by Tim Übelhör's avatar Tim Übelhör
Browse files

Merging the central value repository into master.

parents 2468110e 9dd21574
No related branches found
No related tags found
No related merge requests found
Showing
with 972 additions and 475 deletions
......@@ -4,3 +4,6 @@
[submodule "FmiWrapper"]
path = FmiWrapper
url = https://github.com/Tuebel/FmiWrapper.git
[submodule "CircullarBuffer-CSharp"]
path = CircullarBuffer-CSharp
url = https://github.com/joaoportela/CircullarBuffer-CSharp.git
......@@ -38,20 +38,21 @@
<OutputPath>bin\x64\Debug\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="Optional, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Optional.4.0.0\lib\net45\Optional.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Collections.Immutable, Version=1.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
......@@ -59,24 +60,26 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Classes\Channel.cs" />
<Compile Include="Classes\CircularBuffer.cs" />
<Compile Include="Classes\EnumerationChannel.cs" />
<Compile Include="Classes\DataSourceBase.cs" />
<Compile Include="Classes\LinearChannelLink.cs" />
<Compile Include="Collections\BufferedSamplesTable.cs" />
<Compile Include="Collections\FileSamplesTable.cs" />
<Compile Include="Collections\ConcurrentTable.cs" />
<Compile Include="Classes\BufferedSamplesTable.cs" />
<Compile Include="Classes\FileSamplesTable.cs" />
<Compile Include="Classes\CyclicSamplesTable.cs" />
<Compile Include="Classes\DataRepository.cs" />
<Compile Include="Classes\SimulationDataStorer.cs" />
<Compile Include="Interfaces\IChannel.cs" />
<Compile Include="Interfaces\IDataSource.cs" />
<Compile Include="Interfaces\IEnumerationChannel.cs" />
<Compile Include="Interfaces\IFmuModel.cs" />
<Compile Include="Interfaces\IModel.cs" />
<Compile Include="Interfaces\IChannelHack.cs" />
<Compile Include="Interfaces\IModelInstance.cs" />
<Compile Include="Interfaces\IModelRepository.cs" />
<Compile Include="Interfaces\ISettableHack.cs" />
<Compile Include="Interfaces\ISimulation.cs" />
<Compile Include="Interfaces\ISamplesStorage.cs" />
<Compile Include="Interfaces\IDataRepositorry.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
......
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Threading.Tasks;
namespace ModeliChart.Basics
......@@ -10,39 +9,42 @@ namespace ModeliChart.Basics
/// Stores the samples in a buffer and additionally in a persistent DB.
/// Data has to be added for a timestep and retrieved for a channel.
/// </summary>
public class BufferedSamplesTable : ISamplesStorage, IDisposable
public class BufferedSamplesTable : ISamplesStorage
{
// Use specialized storage classes
private ConcurrentTable<uint, double> _buffer;
private FileSamplesTable _file;
private CyclicSamplesTable buffer;
private FileSamplesTable fileStorage;
public BufferedSamplesTable(IEnumerable<uint> valueRefs, int bufferSize = 60000)
{
_buffer = new ConcurrentTable<uint, double>(valueRefs, bufferSize);
_file = new FileSamplesTable(valueRefs);
buffer = new CyclicSamplesTable(valueRefs, bufferSize);
fileStorage = new FileSamplesTable(valueRefs);
}
public void AddSamples(double time, IEnumerable<uint> valueRefs, IEnumerable<double> values)
{
_buffer.AddSamples(time, valueRefs, values);
_file.AddSamples(time, valueRefs, values);
buffer.AddSamples(time, valueRefs, values);
fileStorage.AddSamples(time, valueRefs, values);
}
public IEnumerable<(double Time, double Value)> GetBufferedSamples(uint valueRef)
public IEnumerable<(double Time, double Value)> GetValues(uint valueRef)
{
return _buffer.GetSamples(valueRef);
return buffer.GetSamples(valueRef);
}
/// <summary>
/// Returns all the values from the persistent storage.
/// Warning: Will take way longer than GetBufferedSamples.
/// Get for a specified enumerable of channels and a given start and end time all the values.
/// Handy for exporting multiple channels.
/// </summary>
/// <param name="valueRef"></param>
/// <param name="modelInstanceName"></param>
/// <param name="valueRefs"></param>
/// <param name="startTime"></param>
/// <param name="endTime"></param>
/// <returns></returns>
public async Task<IEnumerable<(double Time, double Value)>> GetPeristentSamplesAsync(uint valueRef)
public Task<DataTable> GetValuesAsync(IEnumerable<IChannel> channels, double startTime, double endTime)
{
return await _file.GetAllSamplesAsync(valueRef).ConfigureAwait(false);
return fileStorage.GetValuesAsync(channels, startTime, endTime);
}
/// <summary>
......@@ -50,13 +52,8 @@ namespace ModeliChart.Basics
/// </summary>
public async Task ClearAsync()
{
_buffer.ClearHistory();
await _file.ClearHistory().ConfigureAwait(false);
}
public (double Time, double Value) GetLast(uint valueRef)
{
return _buffer.GetLast(valueRef);
buffer.Clear();
await fileStorage.Clear().ConfigureAwait(false);
}
#region IDisposable Support
......@@ -68,7 +65,7 @@ namespace ModeliChart.Basics
{
if (disposing)
{
_file.Dispose();
fileStorage.Dispose();
}
disposedValue = true;
}
......@@ -86,6 +83,11 @@ namespace ModeliChart.Basics
Dispose(true);
// GC.SuppressFinalize(this);
}
public Task<IEnumerable<IEnumerable<(double time, double value)>>> GetValuesAsync(string modelInstanceName, IEnumerable<uint> valueRefs, double startTime, double endTime)
{
throw new NotImplementedException();
}
#endregion
}
}
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Optional;
namespace ModeliChart.Basics
namespace ModeliChart.Basics
{
public class Channel : IChannel, IChannelHack
public class Channel : IChannel
{
// Properties of an IChannel
private string _dataSourceName;
private Option<IDataSource> _dataSource;
public string Name { get; }
public string Description { get; }
public string Name { get; set; }
public string Description { get; set; }
public string DisplayedUnit { get; set; }
/// <summary>
/// Be able to identify if this Channel is Int, Double, or Bool
/// </summary>
public ChannelType ChannelType { get; set; }
/// <summary>
/// Returns the name of the DataSource or if it does not exist the value that has been set.
/// </summary>
public string DataSourceName
{
get => _dataSource.Match(d => d.Name, () => _dataSourceName);
set => _dataSourceName = value;
}
/// <summary>
/// Reference which will be used to get the values from a DataSource
/// Different Channels might share the same ValueRef, FMI 2.0 Standard
/// </summary>
public uint ValueRef { get; set; }
/// <summary>
/// Check this in the setValue method
/// </summary>
public bool Settable { get; set; }
/// <summary>
/// True: Channel accepts new value; False -> not
/// </summary>
public string ModelInstanceName { get; set; }
public uint ValueRef { get; }
public bool Enabled { get; set; }
// For legacy support
public Channel()
public bool Settable { get; }
public Channel(string name, uint valueRef, string description, bool settable)
{
Name = "";
Description = "";
DisplayedUnit = "";
DataSourceName = "";
Name = name;
ValueRef = valueRef;
Description = description;
Settable = settable;
Enabled = true;
Settable = false;
}
/// <summary>
/// Create a deep copy of the channel.
/// </summary>
/// <param name="channel"></param>
public Channel(IChannel channel) : this()
public Channel(IChannel channel)
{
Name = channel.Name;
Description = channel.Description;
DisplayedUnit = channel.DisplayedUnit;
ChannelType = channel.ChannelType;
ValueRef = channel.ValueRef;
ChannelType = channel.ChannelType;
Settable = channel.Settable;
Description = channel.Description;
DisplayedUnit = channel.DisplayedUnit;
ModelInstanceName = channel.ModelInstanceName;
Enabled = channel.Enabled;
if (channel is Channel c)
{
_dataSource = c._dataSource;
}
}
public void SetDataSource(IDataSource dataSource) => _dataSource = Option.Some(dataSource);
public void SetValue(double value)
{
if (Settable && !double.IsNaN(value))
{
// Set the value
_dataSource.MatchSome(d => d.SetValue(ValueRef, value));
}
}
/// <summary>
/// Returns the last value that has been set or 0 if no datasource is available.
/// </summary>
public double LastValue => _dataSource.Match(d => d.GetLastValue(ValueRef).Value, () => 0);
public double CurrentTime => _dataSource.Match(d => d.GetLastValue(ValueRef).Time, () => 0);
public IEnumerable<(double Time, double Value)> GetValues() =>
_dataSource.Match(d => d.GetBufferedValues(ValueRef), () => Enumerable.Empty<(double, double)>());
public Task<IEnumerable<(double Time, double Value)>> GetPeristentValuesAsync()
{
if (_dataSource.HasValue)
{
// Will not be null ;)
return _dataSource.ValueOr((IDataSource)null).GetValuesFromDisk(ValueRef);
}
else
{
return Task.FromResult(Enumerable.Empty<(double, double)>());
}
}
public void SetValueUnsafe(double value)
{
if (!double.IsNaN(value))
{
// Set the value
_dataSource.MatchSome(d => d.SetValue(ValueRef, value));
}
}
public IChannel WithModelInstanceName(string modelInstanceName) =>
new Channel(this) { ModelInstanceName = modelInstanceName };
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Collections;
namespace CircularBuffer
{
/// <inheritdoc/>
/// <summary>
/// Circular buffer.
///
/// When writing to a full buffer:
/// PushBack -> removes this[0] / Front()
/// PushFront -> removes this[Size-1] / Back()
///
/// this implementation is inspired by
/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html
/// because I liked their interface.
/// </summary>
public class CircularBuffer<T> : IEnumerable<T>
{
private readonly T[] _buffer;
/// <summary>
/// The _start. Index of the first element in buffer.
/// </summary>
private int _start;
/// <summary>
/// The _end. Index after the last element in the buffer.
/// </summary>
private int _end;
/// <summary>
/// The _size. Buffer size.
/// </summary>
private int _size;
public CircularBuffer(int capacity)
: this(capacity, new T[] { })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CircularBuffer{T}"/> class.
///
/// </summary>
/// <param name='capacity'>
/// Buffer capacity. Must be positive.
/// </param>
/// <param name='items'>
/// Items to fill buffer with. Items length must be less than capacity.
/// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from
/// any enumerable.
/// </param>
public CircularBuffer(int capacity, T[] items)
{
if (capacity < 1)
{
throw new ArgumentException(
"Circular buffer cannot have negative or zero capacity.", nameof(capacity));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Length > capacity)
{
throw new ArgumentException(
"Too many items to fit circular buffer", nameof(items));
}
_buffer = new T[capacity];
Array.Copy(items, _buffer, items.Length);
_size = items.Length;
_start = 0;
_end = _size == capacity ? 0 : _size;
}
/// <summary>
/// Maximum capacity of the buffer. Elements pushed into the buffer after
/// maximum capacity is reached (IsFull = true), will remove an element.
/// </summary>
public int Capacity { get { return _buffer.Length; } }
public bool IsFull
{
get
{
return Size == Capacity;
}
}
public bool IsEmpty
{
get
{
return Size == 0;
}
}
/// <summary>
/// Current buffer size (the number of elements that the buffer has).
/// </summary>
public int Size { get { return _size; } }
/// <summary>
/// Element at the front of the buffer - this[0].
/// </summary>
/// <returns>The value of the element of type T at the front of the buffer.</returns>
public T Front()
{
ThrowIfEmpty();
return _buffer[_start];
}
/// <summary>
/// Element at the back of the buffer - this[Size - 1].
/// </summary>
/// <returns>The value of the element of type T at the back of the buffer.</returns>
public T Back()
{
ThrowIfEmpty();
return _buffer[(_end != 0 ? _end : Capacity) - 1];
}
public T this[int index]
{
get
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
return _buffer[actualIndex];
}
set
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
_buffer[actualIndex] = value;
}
}
/// <summary>
/// Pushes a new element to the back of the buffer. Back()/this[Size-1]
/// will now return this element.
///
/// When the buffer is full, the element at Front()/this[0] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the back of the buffer</param>
public void PushBack(T item)
{
if (IsFull)
{
_buffer[_end] = item;
Increment(ref _end);
_start = _end;
}
else
{
_buffer[_end] = item;
Increment(ref _end);
++_size;
}
}
/// <summary>
/// Pushes a new element to the front of the buffer. Front()/this[0]
/// will now return this element.
///
/// When the buffer is full, the element at Back()/this[Size-1] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the front of the buffer</param>
public void PushFront(T item)
{
if (IsFull)
{
Decrement(ref _start);
_end = _start;
_buffer[_start] = item;
}
else
{
Decrement(ref _start);
_buffer[_start] = item;
++_size;
}
}
/// <summary>
/// Removes the element at the back of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopBack()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
Decrement(ref _end);
_buffer[_end] = default;
--_size;
}
/// <summary>
/// Removes the element at the front of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopFront()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
_buffer[_start] = default;
Increment(ref _start);
--_size;
}
/// <summary>
/// Copies the buffer contents to an array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <returns>A new array with a copy of the buffer contents.</returns>
public T[] ToArray()
{
T[] newArray = new T[Size];
int newArrayOffset = 0;
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
Array.Copy(segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count);
newArrayOffset += segment.Count;
}
return newArray;
}
#region IEnumerable<T> implementation
public IEnumerator<T> GetEnumerator()
{
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
for (int i = 0; i < segment.Count; i++)
{
yield return segment.Array[segment.Offset + i];
}
}
}
#endregion
#region IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
#endregion
private void ThrowIfEmpty(string message = "Cannot access an empty buffer.")
{
if (IsEmpty)
{
throw new InvalidOperationException(message);
}
}
/// <summary>
/// Increments the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Increment(ref int index)
{
if (++index == Capacity)
{
index = 0;
}
}
/// <summary>
/// Decrements the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Decrement(ref int index)
{
if (index == 0)
{
index = Capacity;
}
index--;
}
/// <summary>
/// Converts the index in the argument to an index in <code>_buffer</code>
/// </summary>
/// <returns>
/// The transformed index.
/// </returns>
/// <param name='index'>
/// External index.
/// </param>
private int InternalIndex(int index)
{
return _start + (index < (Capacity - _start) ? index : index - Capacity);
}
// doing ArrayOne and ArrayTwo methods returning ArraySegment<T> as seen here:
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b
// should help a lot with the code.
#region Array items easy access.
// The array is composed by at most two non-contiguous segments,
// the next two methods allow easy access to those.
private ArraySegment<T> ArrayOne()
{
if (_start < _end)
{
return new ArraySegment<T>(_buffer, _start, _end - _start);
}
else
{
return new ArraySegment<T>(_buffer, _start, _buffer.Length - _start);
}
}
private ArraySegment<T> ArrayTwo()
{
if (_start < _end)
{
return new ArraySegment<T>(_buffer, _end, 0);
}
else
{
return new ArraySegment<T>(_buffer, 0, _end);
}
}
#endregion
}
}
using System.Collections.Generic;
using System.Linq;
using CircularBuffer;
namespace ModeliChart.Basics
{
// Using the Beer-Ware (hooray!) licensed circular buffer implementation by Joao Portela
// to prevent the gc from collecting lots of samples.
public class CyclicSamplesTable
{
private readonly CircularBuffer<double> timeQueue;
private readonly IDictionary<uint, (CircularBuffer<double> Queue, double LastValue)> data;
private readonly object dataLock = new object();
// Limit the memory usage
private readonly int capacity;
public CyclicSamplesTable(IEnumerable<uint> valueRefs, int capacity = 60000)
{
this.capacity = capacity;
timeQueue = new CircularBuffer<double>(capacity);
data = valueRefs
.ToDictionary(vr => vr,
vr => (new CircularBuffer<double>(capacity), 0.0));
}
public void AddSamples(double time, IEnumerable<uint> valueRefs, IEnumerable<double> values)
{
var zipped = valueRefs.Zip(values, (vr, value) => (vr, value));
lock (dataLock)
{
timeQueue.PushBack(time);
foreach (var (vr, value) in zipped)
{
var (queue, lastValue) = data[vr];
queue.PushBack(value);
lastValue = value;
}
}
}
public void Clear()
{
lock (dataLock)
{
while (!timeQueue.IsEmpty)
{
timeQueue.PopFront();
}
// Use key because we cannot modify the iterated variables... I know good Hackerboi :D
foreach (var key in data.Keys)
{
var (queue, lastValue) = data[key];
while (!queue.IsEmpty)
{
queue.PopFront();
}
lastValue = 0;
}
}
}
public double GetLast(uint valueRef)
{
lock (dataLock)
{
return data[valueRef].LastValue;
}
}
public IEnumerable<(double Time, double Value)> GetSamples(uint valueRef)
{
lock (dataLock)
{
if (timeQueue.IsEmpty)
{
// Empty queue fails to execute ToArray
return Enumerable.Empty<(double Time, double Value)>();
}
else
{
// It is important to copy the data, because the linq query will be executed delayed
return timeQueue
.Zip(data[valueRef].Queue,
(time, value) => (time, value))
.ToArray();
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace ModeliChart.Basics
{
/// <summary>
/// A default implementation of <see cref="IDataRepository"/>
/// using a Dictionary of DataSource names and ISamplesStorages.
/// </summary>
public class DataRepository : IDataRepository
{
// Factory function for storage
private readonly Func<IEnumerable<uint>, ISamplesStorage> createStorage;
private readonly Dictionary<string, ISamplesStorage> storages = new Dictionary<string, ISamplesStorage>();
public DataRepository(Func<IEnumerable<uint>, ISamplesStorage> createStorage)
{
this.createStorage = createStorage;
}
public void AddValues(string modelInstanceName, double time, IEnumerable<uint> valueRefs, IEnumerable<double> values)
{
// Create new storage if needed
if (!storages.ContainsKey(modelInstanceName))
{
storages.Add(modelInstanceName, createStorage(valueRefs));
}
storages[modelInstanceName].AddSamples(time, valueRefs, values);
}
public IEnumerable<(double Time, double Value)> GetValues(IChannel channel)
{
if (storages.ContainsKey(channel.ModelInstanceName))
{
return storages[channel.ModelInstanceName].GetValues(channel.ValueRef);
}
else
{
return Enumerable.Empty<(double Time, double Value)>();
}
}
public async Task<DataTable> GetValuesAsync(IEnumerable<IChannel> channels, double startTime, double endTime)
{
// Only use channels with data
var groups = channels
.Where(c => storages.ContainsKey(c.ModelInstanceName))
.GroupBy(c => c.ModelInstanceName);
// Get the tables
var tables = await Task.WhenAll(groups
.Select(g => storages[g.Key].GetValuesAsync(g, startTime, endTime)))
.ConfigureAwait(false);
// Merge into the first table
for (int i = 1; i < tables.Length; i++)
{
tables[0].Merge(tables[i]);
}
return tables.FirstOrDefault();
}
public void Dispose()
{
foreach (var storage in storages.Values)
{
storage.Dispose();
}
}
public Task ClearAsync() => Task.WhenAll(storages.Values.Select(s => s.ClearAsync()));
}
}
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ModeliChart.Basics
{
/// <summary>
/// Takes care of the channels and the samples for you.
/// </summary>
public abstract class DataSourceBase : IDataSource
{
// Fmi Standard: Channels might share the same valueRef. For fast assembling of values.
protected ILookup<uint, IChannel> _channelsByValueRef;
// Data
protected ISamplesStorage _storage;
public string Name
{
get;
private set;
}
public IModel Model { get; private set; }
/// <summary>
/// The key is the channels name.
/// </summary>
public IEnumerable<IChannel> Channels { get; private set; }
public async Task InitializeAsync(string instanceName, IModel model)
{
Name = instanceName;
Model = model;
// Create concrete channels which are connected to this datasource.
Channels = model.Channels
.Select(c =>
{
var nc = new Channel(c);
nc.SetDataSource(this);
return nc;
});
_channelsByValueRef = Channels.ToLookup(c => c.ValueRef);
_storage = new BufferedSamplesTable(_channelsByValueRef.Select(g => g.Key));
// Initialize child class as final step
await InitializeChildAsync().ConfigureAwait(false);
}
protected abstract Task InitializeChildAsync();
protected abstract void DisposeChild();
/// <summary>
/// Simulate the values to the currentTime_sec
/// </summary>
/// <param name="currentTime_sec">Current time</param>
public abstract void DoStep(double currentTime_sec, double stepSize);
/// <summary>
/// Sets one value for the valueRef
/// </summary>
/// <typeparam name="T">Type that should be set</typeparam>
/// <param name="valueRef">The unique reference of this value</param>
/// <param name="value">Value that will be set</param>
public abstract Task SetValue(uint valueRef, double value);
/// <summary>
/// Reset the datasource to the initial state.
/// </summary>
public abstract Task ResetAsync();
public IEnumerable<(double Time, double Value)> GetBufferedValues(uint valueRef) => _storage.GetBufferedSamples(valueRef);
public (double Time, double Value) GetLastValue(uint valueRef) => _storage.GetLast(valueRef);
public async Task<IEnumerable<(double Time, double Value)>> GetValuesFromDisk(uint valueRef)
=> (await _storage.GetPeristentSamplesAsync(valueRef).ConfigureAwait(false));
public void Dispose()
{
DisposeChild();
_storage.Dispose();
}
}
}
\ No newline at end of file
......@@ -4,31 +4,23 @@ namespace ModeliChart.Basics
{
public class EnumerationChannel : Channel, IEnumerationChannel
{
IDictionary<int, string> _entrys;
public IDictionary<int, string> Entries { get; }
public EnumerationChannel() { }
public new ChannelType ChannelType => ChannelType.Enum;
public EnumerationChannel(IChannel channel, IDictionary<int, string> entrys)
: base(channel)
public EnumerationChannel(string name, uint valueRef, string description, bool settable, IDictionary<int, string> entries)
: base(name, valueRef, description, settable)
{
_entrys = entrys;
Entries = entries;
}
public IDictionary<int, string> Entries
public EnumerationChannel(IChannel channel, IDictionary<int, string> entries)
: base(channel)
{
get => _entrys;
Entries = entries;
}
/// <summary>
/// Be able to identify if this Channel is Int, Double, or Bool
/// </summary>
public new ChannelType ChannelType
{
get => ChannelType.Enum;
set
{ // Do not change
}
}
}
}
......@@ -3,6 +3,9 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using System.Data;
namespace ModeliChart.Basics
{
......@@ -11,14 +14,18 @@ namespace ModeliChart.Basics
/// </summary>
public class FileSamplesTable : IDisposable
{
// Uses a custom dense binary serialization: [double[] values, double time]
// Appens newest value to the end.
// Save the storage to disk
private FileStream _stream;
private FileStream stream;
// Write in another Task
private Task _writeTask;
private Task writeTask;
// Cache for writing, blocks until content is inside
private BlockingCollection<double[]> _writeCache;
private BlockingCollection<double[]> writeCache;
// Efficient access via dictionaries
private Dictionary<uint, int> _posByValueRef;
private readonly Dictionary<uint, int> posByValueRef;
private readonly int bufferSize;
/// <summary>
/// The valueRefs must be known in advance, since calculations depend on a fixed number of channels.
......@@ -27,135 +34,178 @@ namespace ModeliChart.Basics
public FileSamplesTable(IEnumerable<uint> valueRefs)
{
// Init variables
_writeCache = new BlockingCollection<double[]>();
_posByValueRef = new Dictionary<uint, int>();
writeCache = new BlockingCollection<double[]>();
posByValueRef = new Dictionary<uint, int>();
foreach (uint vr in valueRefs)
{
if (!_posByValueRef.ContainsKey(vr))
if (!posByValueRef.ContainsKey(vr))
{
_posByValueRef.Add(vr, _posByValueRef.Count);
posByValueRef.Add(vr, posByValueRef.Count);
}
}
bufferSize = posByValueRef.Count + 1;
CreateTempFile();
// Run the writing in a parallel Task
startWriteTask();
StartWriteTask();
}
// Definition of the buffer access.
private double GetBufferValue(double[] buffer, uint valueRef) =>
buffer[posByValueRef[valueRef]];
private void SetBufferValue(double[] buffer, uint valueRef, double value) =>
buffer[posByValueRef[valueRef]] = value;
private double GetBufferTime(double[] buffer) => buffer[buffer.Length - 1];
private void SetBufferTIme(double[] buffer, double time) => buffer[buffer.Length - 1] = time;
/// <summary>
/// Creates a new temp file. The old one will be closed and deleted.
/// </summary>
private void CreateTempFile()
{
// Free the old file
if (_stream != null)
if (stream != null)
{
_stream.Dispose();
stream.Dispose();
}
// Create the temp with DeleteOnClose flag.
_stream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
stream = new FileStream(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
}
private void startWriteTask()
private void StartWriteTask()
{
// Create new write cache if it has been completed.
if (_writeCache.IsCompleted)
if (writeCache.IsCompleted)
{
_writeCache = new BlockingCollection<double[]>();
writeCache = new BlockingCollection<double[]>();
}
_writeTask = Task.Run(() =>
// Create longrunning! task
writeTask = new Task(() =>
{
while (!_writeCache.IsCompleted)
try
{
// Use less resources by waiting for new values
if (_writeCache.TryTake(out var valueBuffer, 100))
while (!writeCache.IsCompleted)
{
// Use less resources by waiting for new values
var valueBuffer = writeCache.Take();
// Convert to byte array and write it
byte[] byteBuffer = new byte[valueBuffer.Length * sizeof(double)];
Buffer.BlockCopy(valueBuffer, 0, byteBuffer, 0, byteBuffer.Length);
_stream.Write(byteBuffer, 0, byteBuffer.Length);
stream.Write(byteBuffer, 0, byteBuffer.Length);
}
}
});
catch (InvalidOperationException)
{
// The collection has been completed while taking.
}
}, TaskCreationOptions.LongRunning);
writeTask.Start();
}
private async Task stopWriteTaskAsync()
private async Task StopWriteTaskAsync()
{
// Stop adding data to the buffer
_writeCache.CompleteAdding();
await _writeTask.ConfigureAwait(false);
writeCache.CompleteAdding();
await writeTask.ConfigureAwait(false);
}
/// <summary>
/// Clears all the data: Cache and disk.
/// New samples are ignored While this method executes.
/// </summary>
public async Task ClearHistory()
public async Task Clear()
{
// Finish wrting
await stopWriteTaskAsync().ConfigureAwait(false);
await StopWriteTaskAsync().ConfigureAwait(false);
// Use a new cache
CreateTempFile();
// Restart writing
startWriteTask();
StartWriteTask();
}
/// <summary>
/// The fast method for adding values from the current simulation step.
/// </summary>
/// <param name="time"></param>
/// <param name="valueRefs"></param>
/// <param name="values"></param>
public void AddSamples(double time, IEnumerable<uint> valueRefs, IEnumerable<double> values)
{
// Buffer = Timestamp + values
var buffer = new double[_posByValueRef.Count + 1];
buffer[0] = time;
// Insert the values in buffer
var vrEnumerator = valueRefs.GetEnumerator();
var valueEnumerator = values.GetEnumerator();
while (vrEnumerator.MoveNext() && valueEnumerator.MoveNext())
var buffer = new double[bufferSize];
var zipped = from valueRef in valueRefs
from value in values
select (valueRef, value);
foreach (var (valueRef, value) in zipped)
{
buffer[_posByValueRef[vrEnumerator.Current] + 1] = valueEnumerator.Current;
SetBufferValue(buffer, valueRef, value);
}
SetBufferTIme(buffer, time);
// Append to the writer buffer
_writeCache.Add(buffer);
writeCache.Add(buffer);
}
public async Task<IEnumerable<(double Time, double Value)>> GetAllSamplesAsync(uint valueRef)
{
var resList = new List<(double Time, double Value)>();
/// <summary>
/// Get for a specified enumerable of channels and a given start and end time all the values.
/// Handy for exporting multiple channels.
/// </summary>
/// <returns>DataTable: Time; valuerefs</returns>
public async Task<DataTable> GetValuesAsync(IEnumerable<IChannel> channels, double startTime, double endTime)
{
// Init res DataTable
var table = new DataTable();
var timeColumn = new DataColumn("Time", typeof(double));
table.Columns.Add(timeColumn);
table.Columns.AddRange(channels
.Select(c => new DataColumn(c.Name, typeof(double)))
.ToArray());
// Get access to the file, stop writing
await stopWriteTaskAsync().ConfigureAwait(false);
await StopWriteTaskAsync().ConfigureAwait(false);
// Read the file from the begin
_stream.Seek(0, SeekOrigin.Begin);
// larger chunks for better performance
int sizeofStep = (_posByValueRef.Count + 1) * sizeof(double);
int valueOffset = (_posByValueRef[valueRef] + 1) * sizeof(double);
var buffer = new byte[64 * sizeofStep];
stream.Seek(0, SeekOrigin.Begin);
// larger chunks for better performance, + 1 for the time
var buffer = new byte[bufferSize * sizeof(double)];
var doubleBuffer = new double[bufferSize];
// Read the whole file
while (_stream.CanRead)
while (stream.CanRead)
{
// Read samples for one timepoint into buffer (larger chunks for performance)
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead == 0)
{
// Reached the end of the file
break;
}
// Extract the values
for (int posInBuffer = 0; posInBuffer < bytesRead; posInBuffer += sizeofStep)
// Extract the values in the same order as the valueRefs
Buffer.BlockCopy(buffer, 0, doubleBuffer, 0, buffer.Length);
var time = GetBufferTime(doubleBuffer);
// Only add data that is requested
if (time >= startTime && time <= endTime)
{
resList.Add(
(BitConverter.ToDouble(buffer, posInBuffer), BitConverter.ToDouble(buffer, posInBuffer + valueOffset)));
// Add new row to table
var row = table.NewRow();
row[timeColumn] = time;
foreach (var channel in channels)
{
row[channel.Name] = GetBufferValue(doubleBuffer, channel.ValueRef);
}
}
}
// Restart the writing Task
startWriteTask();
return resList;
StartWriteTask();
return table;
}
public void Dispose()
{
// Dispose the managed resources
stopWriteTaskAsync().Wait();
StopWriteTaskAsync().Wait();
// File will be deleted because of the DeleteOnClose flag.
_stream.Dispose();
stream.Dispose();
}
}
}
......@@ -15,7 +15,6 @@ namespace ModeliChart.Basics
Factor = factor;
Offset = offset;
Enabled = enabled;
}
/// <summary>
......@@ -53,17 +52,6 @@ namespace ModeliChart.Basics
public double Factor { get; }
public double Offset { get; }
/// <summary>
/// If enabled the slave channels value is set via the linear function.
/// </summary>
public void Execute()
{
if (Enabled)
{
SlaveChannel.SetValue(MasterChannel.LastValue * Factor + Offset);
}
}
// Generatedby VS for IEquatable
public override int GetHashCode()
{
......
......
using System.Collections.Generic;
namespace ModeliChart.Basics
{
/// <summary>
/// Observers the ISimulations NewValuesAvailable event and forwards
/// the values to a IDataRepository.
/// </summary>
public class SimulationDataStorer
{
private readonly IDataRepository dataRepository;
public SimulationDataStorer(ISimulation simulation, IDataRepository dataRepository)
{
this.dataRepository = dataRepository;
simulation.NewValuesAvailable += Simulation_NewValuesAvailable;
}
private void Simulation_NewValuesAvailable(object sender, (string ModelInstanceName, double Time, IEnumerable<uint> ValueRefs, IEnumerable<double> Values) e)
{
dataRepository.AddValues(e.ModelInstanceName, e.Time, e.ValueRefs, e.Values);
}
}
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace ModeliChart.Basics
{
/// <summary>
/// This class stores the Data as matrix
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public class ConcurrentTable<TKey, TValue>
{
// Store everything in this matrix
private ConcurrentQueue<TimeStepWrapper<TValue>> _queue;
// Manage the keys and their position in the matrix
private Dictionary<TKey, int> _posByKey;
// Store the last values for efficient use in ChannelLinks and keeping them alive for parameter files.
private TimeStepWrapper<TValue> _lastTimeStep;
// Limit the memory usage
private int _maxCount;
/// <summary>
///
/// </summary>
/// <param name="keys"></param>
/// <param name="maxCount"></param>
public ConcurrentTable(IEnumerable<TKey> keys, int maxCount = 60000)
{
_posByKey = new Dictionary<TKey, int>();
foreach (TKey key in keys)
{
if (!_posByKey.ContainsKey(key))
{
_posByKey.Add(key, _posByKey.Count);
}
}
_queue = new ConcurrentQueue<TimeStepWrapper<TValue>>();
_maxCount = maxCount;
_lastTimeStep = new TimeStepWrapper<TValue>(_posByKey.Count);
}
public void AddSamples(double time, IEnumerable<TKey> keys, IEnumerable<TValue> values)
{
// Create double buffer and fill it with values
var timeStep = new TimeStepWrapper<TValue>(_posByKey.Count) { Time = time };
var keysEnum = keys.GetEnumerator();
var samplesEnum = values.GetEnumerator();
while (keysEnum.MoveNext() && samplesEnum.MoveNext())
{
timeStep.Values[_posByKey[keysEnum.Current]] = samplesEnum.Current;
}
// Add to matrix and exchange lastValues
_queue.Enqueue(timeStep);
Interlocked.Exchange(ref _lastTimeStep, timeStep);
// Respect the memory limit
while (_queue.Count > _maxCount)
{
if (!_queue.TryDequeue(out var temp))
{
break;
}
}
}
public void ClearHistory()
{
// Exchange thread safe, garbage collector will cleanup old matirx
Interlocked.Exchange(ref _queue, new ConcurrentQueue<TimeStepWrapper<TValue>>());
// DO NOT exhange the last value buffer!
}
public (double Time, TValue Value) GetLast(TKey key)
{
return (_lastTimeStep.Time, _lastTimeStep.Values[_posByKey[key]]);
}
public IEnumerable<(double Time, TValue Value)> GetSamples(TKey key)
{
// Ceate a return array
return _queue.Select(line => (line.Time, line.Values[_posByKey[key]]));
}
}
/// <summary>
/// Serves as a wrapper for one timestep.
/// </summary>
/// <typeparam name="T"></typeparam>
class TimeStepWrapper<T>
{
private T[] _values;
private double _time;
public TimeStepWrapper(int valuesCount)
{
_values = new T[valuesCount];
}
public T[] Values { get => _values; }
public double Time { get => _time; set => _time = value; }
}
}
using System.Collections.Generic;
using System.Threading.Tasks;
using Optional;
namespace ModeliChart.Basics
{
......@@ -17,9 +16,7 @@ namespace ModeliChart.Basics
}
/// <summary>
/// The values serve as pipe to the DataSources and are pure descriptive.
/// They must not save their values but get the data from the datasources instead.
/// It turned out that the GarbageCollector and IO operations become a bottleneck if they are not handled centrally.
/// A Channel bundles the information to access values from a data repository and set values in a modelinstance.
/// </summary>
public interface IChannel
{
......@@ -27,69 +24,49 @@ namespace ModeliChart.Basics
/// <summary>
/// Must be unique: FMI 2.0 standard
/// </summary>
string Name { get; set; }
string Name { get; }
/// <summary>
/// Some notes for the enduser.
/// </summary>
string Description { get; set; }
string Description { get; }
/// <summary>
/// The unit of the channels
/// </summary>
string DisplayedUnit { get; set; }
string DisplayedUnit { get; }
/// <summary>
/// Be able to identify if this Channel is Int, Double, or Bool
/// </summary>
ChannelType ChannelType { get; set; }
ChannelType ChannelType { get; }
/// <summary>
/// The data source is accessed by the channel when getting the values.
/// The DataSource can be found using the DataSourceName.
/// Get the unique name of the model instance.
/// </summary>
void SetDataSource(IDataSource dataSource);
string ModelInstanceName { get; }
/// <summary>
/// Was used for serialization in earlier versions.
/// Now remains because changing all dependencies isn't worth the effort of cleaning up.
/// Create a copy of this channel with a new model instance name.
/// </summary>
string DataSourceName { get; }
/// <param name="modelInstanceName"></param>
/// <returns></returns>
IChannel WithModelInstanceName(string modelInstanceName);
/// <summary>
/// Reference which will be used to get the values from a DataSource
/// Different Channels might share the same ValueRef, FMI 2.0 Standard
/// </summary>
uint ValueRef { get; set; }
/// <summary>
/// Check this in the setValue method
/// </summary>
bool Settable { get; set; }
uint ValueRef { get; }
/// <summary>
/// True: Channel accepts new value; False -> not
/// </summary>
bool Enabled { get; set; }
// Methods
/// <summary>
/// Returns an enumerable for the data.
/// The data is taken from the datasource.
/// Wheter the channels value can be changed.
/// </summary>
/// <returns></returns>
IEnumerable<(double Time, double Value)> GetValues();
/// <summary>
/// Set the value, convert double to int or bool (0->false, !=0->true)
/// </summary>
/// <param name="value"></param>
void SetValue(double value);
/// <summary>
/// Efficient access to the latest value for use in channel links.
/// Additionally this value is not cleared after stop.
/// </summary>
/// <returns></returns>
double LastValue { get; }
/// <summary>
/// Returns the data from the datasource.
/// The data is from the disk so this method is slow.
/// Use the GetValues method for fast access of the buffered data.
/// </summary>
/// <returns></returns>
Task<IEnumerable<(double Time, double Value)>> GetPeristentValuesAsync();
bool Settable { get; }
}
}
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace ModeliChart.Basics
{
/// <summary>
/// Store the simulation data.
/// The data can be accessed via a DataSourceName and a ValueRef.
/// The design is immutable
/// </summary>
public interface IDataRepository : IDisposable
{
/// <summary>
/// Add raw values from the current simulation step for one modelInstance.
/// </summary>
/// <param name="modelInstanceName"></param>
/// <param name="time"></param>
/// <param name="valueRefs"></param>
/// <param name="values"></param>
void AddValues(string modelInstanceName, double time, IEnumerable<uint> valueRefs, IEnumerable<double> values);
/// <summary>
/// Retrieves the values of one channel which are stored in memory.
/// Use case is getting the data for the instruments.
/// </summary>
/// <param name="channel"></param>
/// <returns>The values or an empty enumerable if no channel matches.</returns>
IEnumerable<(double Time, double Value)> GetValues(IChannel channel);
/// <summary>
/// Reads the values for each timestep into an enumerable.
/// The medium might take some time to read the values.
/// </summary>
/// <returns>
/// A DataTable with the first column for the time and the other ones for the channels values.
/// </returns>
Task<DataTable> GetValuesAsync(IEnumerable<IChannel> channels, double startTime, double endTime);
/// <summary>
/// Removes all values.
/// </summary>
/// <returns></returns>
Task ClearAsync();
}
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ModeliChart.Basics
{
/// <summary>
/// Interface to be able to obtain values from an backend.
/// The values can obtain their values via their uint value references.
/// For our simulation purpose it makes sense to support double only, generic makes the whole data storing too complex.
/// For storing the data it is adviced to use the BufferedSamplesTable, which can store values for each step and recall
/// them for channels efficiently.
/// </summary>
public interface IDataSource: IDisposable
{
/// <summary>
/// Must be a unique identification
/// Use it for a Channel to identify it's ValuePort after loading from serialization
/// </summary>
string Name
{
get;
}
/// <summary>
/// The model of the datasource contains the channels, location, guid, ...
/// </summary>
IModel Model { get; }
/// <summary>
/// The channels connected to this datasource instance.
/// </summary>
IEnumerable<IChannel> Channels { get; }
/// <summary>
/// Execute heavy or long running initialization here to stay responsive.
/// </summary>
/// <returns></returns>
Task InitializeAsync(string instanceName, IModel model);
/// <summary>
/// Execute one discrete simulation step.
/// </summary>
/// <param name="currentTime">Current time in seconds.</param>
/// <param name="stepSize">Step size in seconds</param>
void DoStep(double currentTime, double stepSize);
/// <summary>
/// Set the value for the
/// </summary>
/// <param name="channel"></param>
/// <param name="value"></param>
Task SetValue(uint valueRef, double value);
/// <summary>
/// Returns the values for the channel which are stored in the RAM.
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
IEnumerable<(double Time, double Value)> GetBufferedValues(uint valueRef);
/// <summary>
/// Returns the values for the channel from ROM.
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
Task<IEnumerable<(double Time, double Value)>> GetValuesFromDisk(uint valueRef);
/// <summary>
/// Returns the last simulated value for the valueRef.
/// </summary>
/// <param name="valueRef"></param>
/// <returns></returns>
(double Time, double Value) GetLastValue(uint valueRef);
/// <summary>
/// Reset the datasource to the initial state.
/// </summary>
Task ResetAsync();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ModeliChart.Basics.Interfaces
{
interface IFmuModel
{
}
}
......@@ -3,9 +3,7 @@
namespace ModeliChart.Basics
{
/// <summary>
/// Represents the FMU model the model must be available ready to use, extracted on the disk.
/// Access the FMU variables as channels.
/// The Models computations can be obtained through instantiation of an ManagedFmu.
/// Represents an arbitrary model as part of the Simulation.
/// </summary>
public interface IModel
{
......
......
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ModeliChart.Basics
{
/// <summary>
/// There might be several instances of a model distinguished by an unique name.
/// </summary>
public interface IModelInstance : IDisposable
{
/// <summary>
/// Unique name of the instance
/// </summary>
string Name { get; }
// Favour composition over inheratince for cleaner implementations of the instances.
/// <summary>
/// The model that has bee used to create the instance.
/// </summary>
IModel Model { get; }
// Proxy to the model, because this attribute is accessed frequently
/// <summary>
/// The Channels of the simulation.
/// </summary>
IEnumerable<IChannel> Channels { get; }
}
}
using System.Collections.Generic;
using ModeliChart.Basics;
namespace ModeliChart.Basics
{
/// <summary>
/// Manages the model entities. Models can be added or removed from the repository.
/// </summary>
public interface IModelRepository
{
/// <summary>
/// Get all models from the repository.
/// </summary>
/// <returns></returns>
IEnumerable<IModel> Models { get; }
/// <summary>
/// Retrieves the model if it allready exists.
/// If it does not exist yet, the model is added to the repository and returned.
/// </summary>
/// <param name="model"></param>
IModel AddOrGetModel(string path);
/// <summary>
/// Removes the model if it exists. Otherwise nothing is done.
/// </summary>
/// <param name="model"></param>
bool RemoveModel(IModel model);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment