Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype implementation of triggered timeline visualizer #80

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Bonsai.Harp.Visualizers/TimelineGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ internal class VisualizerController
public override Expression Build(IEnumerable<Expression> arguments)
{
var source = arguments.First();
var parameterType = source.Type.GetGenericArguments()[0];
Controller = new VisualizerController
{
TimeSpan = TimeSpan,
Expand Down
25 changes: 14 additions & 11 deletions Bonsai.Harp.Visualizers/TimelineGraphVisualizer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
Expand Down Expand Up @@ -65,31 +66,33 @@ public override void Load(IServiceProvider provider)

var currentTime = 0.0;
var absoluteMinTime = double.MaxValue;
var registerMap = new Dictionary<int, BoundedPointPairList>();
CompositeDisposable subscriptions = new();
view.HandleCreated += delegate
{
subscriptions.Add(controller.Registers.Subscribe(register =>
{
var label = GetRegisterInfo(register, out int address);
var color = GraphControl.GetColor(address);
var points = new BoundedPointPairList();
view.BeginInvoke((Action)(() =>
{
var series = view.Graph.CreateSeries(label, points, color);
view.Graph.GraphPane.CurveList.Add(series);
view.Graph.Invalidate();
}));

subscriptions.Add(register
.Select(message => message.GetTimestamp())
.Buffer(() => timerTick)
.Subscribe(buffer =>
{
if (buffer.Count == 0) return;
foreach (var timestamp in buffer)
foreach (var message in buffer)
{
var address = message.Address;
var timestamp = message.GetTimestamp();
absoluteMinTime = Math.Min(absoluteMinTime, timestamp);
currentTime = Math.Max(currentTime, timestamp);
if (!registerMap.TryGetValue(address, out var points))
{
points = new BoundedPointPairList();
registerMap.Add(address, points);
var color = GraphControl.GetColor(address);
var series = view.Graph.CreateSeries(label, points, color);
view.Graph.GraphPane.CurveList.Add(series);
}

points.Add(timestamp, address);
}

Expand Down
153 changes: 153 additions & 0 deletions Bonsai.Harp.Visualizers/TriggerTimelineGraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Bonsai.Expressions;

namespace Bonsai.Harp.Visualizers
{
/// <summary>
/// Represents an operator that configures a visualizer to plot each Harp message
/// in the sequence in a synchronized rolling graph.
/// </summary>
[TypeVisualizer(typeof(TriggerTimelineGraphVisualizer))]
[Description("A visualizer that plots each Harp message in the sequence in a synchronized rolling graph.")]
public class TriggerTimelineGraphBuilder : ExpressionBuilder
{
static readonly Range<int> argumentRange = Range.Create(lowerBound: 2, upperBound: 2);

/// <summary>
/// Gets the range of input arguments that this expression builder accepts.
/// </summary>
public override Range<int> ArgumentRange
{
get { return argumentRange; }
}

/// <summary>
/// Gets or sets the optional maximum time range captured in the timeline graph.
/// If no time span is specified, all data points will be displayed.
/// </summary>
[Category("Range")]
[Description("The optional maximum time range captured in the timeline graph. If no time span is specified, all data points will be displayed.")]
public double? TimeSpan { get; set; }

internal VisualizerController Controller { get; set; }

internal class VisualizerController
{
internal double? TimeSpan;
internal ReplaySubject<IGroupedObservable<Timestamped<double>, Timestamped<LabeledRegister>>> Triggers;
}

internal struct LabeledRegister
{
public string Label;
public int Address;

public LabeledRegister(string label, int address)
{
Label = label;
Address = address;
}
}

/// <inheritdoc/>
public override Expression Build(IEnumerable<Expression> arguments)
{
var sources = arguments.ToArray();
var triggerType = sources[1].Type.GetGenericArguments()[0];
if (!triggerType.IsGenericType || triggerType.GetGenericTypeDefinition() != typeof(Timestamped<>))
{
throw new InvalidOperationException("The trigger input must be Harp timestamped.");
}

triggerType = triggerType.GetGenericArguments()[0];
Controller = new VisualizerController
{
TimeSpan = TimeSpan,
Triggers = new()
};
var combinator = Expression.Constant(this);
return Expression.Call(combinator, nameof(Process), new[] { triggerType }, sources);
}

IObservable<TSource> Process<TSource, TTrigger>(
IObservable<TSource> source,
Func<IObservable<TSource>, IObservable<Timestamped<LabeledRegister>>> selector,
IObservable<Timestamped<TTrigger>> trigger)
{
return source.Publish(ps => trigger.Publish(pt => ps.Merge(
selector(ps).Window(pt).Skip(1).Zip(pt, (window, offset) =>
TimelineObservable.Create(
Timestamped.Create(Convert.ToDouble(offset.Value), offset.Seconds),
window))
.Do(Controller.Triggers)
.IgnoreElements()
.Cast<TSource>())));
}

IObservable<HarpMessage> Process<TTrigger>(
IObservable<HarpMessage> source,
IObservable<Timestamped<TTrigger>> trigger)
{
return Process(source, ps => ps.Select(
message => Timestamped.Create(
new LabeledRegister(message.Address.ToString(), message.Address),
message.GetTimestamp())),
trigger);
}

IObservable<IGroupedObservable<int, HarpMessage>> Process<TTrigger>(
IObservable<IGroupedObservable<int, HarpMessage>> source,
IObservable<Timestamped<TTrigger>> trigger)
{
return Process(source, ps => ps.SelectMany(group => group.Select(
message => Timestamped.Create(
new LabeledRegister(message.Address.ToString(), message.Address),
message.GetTimestamp()))),
trigger);
}

IObservable<IGroupedObservable<Type, HarpMessage>> Process<TTrigger>(
IObservable<IGroupedObservable<Type, HarpMessage>> source,
IObservable<Timestamped<TTrigger>> trigger)
{
return Process(source, ps => ps.SelectMany(group => group.Select(
message => Timestamped.Create(
new LabeledRegister(group.Key.Name, message.Address),
message.GetTimestamp()))),
trigger);
}

static class TimelineObservable
{
public static IGroupedObservable<Timestamped<double>, Timestamped<TLabel>> Create<TLabel>(
Timestamped<double> key, IObservable<Timestamped<TLabel>> source)
{
return new TimelineObservable<double, TLabel>(key, source);
}
}

class TimelineObservable<TKey, TLabel> : IGroupedObservable<Timestamped<TKey>, Timestamped<TLabel>>
{
readonly IObservable<Timestamped<TLabel>> timestamps;

public TimelineObservable(Timestamped<TKey> key, IObservable<Timestamped<TLabel>> source)
{
Key = key;
timestamps = source;
}

public Timestamped<TKey> Key { get; }

public IDisposable Subscribe(IObserver<Timestamped<TLabel>> observer)
{
return timestamps.Subscribe(observer);
}
}
}
}
143 changes: 143 additions & 0 deletions Bonsai.Harp.Visualizers/TriggerTimelineGraphVisualizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Windows.Forms;
using Bonsai.Design;
using Bonsai.Design.Visualizers;
using Bonsai.Expressions;
using ZedGraph;

namespace Bonsai.Harp.Visualizers
{
/// <summary>
/// Provides a type visualizer to display a sequence of Harp messages as a synchronized rolling graph.
/// </summary>
public class TriggerTimelineGraphVisualizer : DialogTypeVisualizer
{
const int TargetInterval = 1000 / 50;
TriggerTimelineGraphBuilder.VisualizerController controller;
TimelineGraphView view;
Timer timer;

/// <summary>
/// Gets or sets the maximum time range, in seconds, displayed at any one moment in the graph.
/// </summary>
public double TimeSpan { get; set; }

static string GetRegisterInfo(IObservable<HarpMessage> register, out int address)
{
switch (register)
{
case IGroupedObservable<int, HarpMessage> addressRegister:
address = addressRegister.Key;
return address.ToString();
case IGroupedObservable<Type, HarpMessage> typedRegister:
var addressField = typedRegister.Key.GetField(nameof(WhoAmI.Address), BindingFlags.Static | BindingFlags.Public);
address = (int)addressField.GetValue(null);
return typedRegister.Key.Name;
default:
throw new ArgumentException("Unsupported register type.", nameof(register));
}
}

/// <inheritdoc/>
public override void Load(IServiceProvider provider)
{
var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
var timelineBuilder = (TriggerTimelineGraphBuilder)ExpressionBuilder.GetVisualizerElement(context.Source).Builder;
controller = timelineBuilder.Controller;

timer = new Timer();
timer.Interval = TargetInterval;
var timerTick = Observable.FromEventPattern<EventHandler, EventArgs>(
handler => timer.Tick += handler,
handler => timer.Tick -= handler);
timer.Start();

view = new TimelineGraphView();
view.Dock = DockStyle.Fill;
view.Graph.AutoScaleX = false;
view.TimeSpan = controller.TimeSpan.GetValueOrDefault(TimeSpan);
view.CanEditTimeSpan = !controller.TimeSpan.HasValue;
GraphHelper.FormatTimeAxis(view.Graph.GraphPane.XAxis);
GraphHelper.SetAxisLabel(view.Graph.GraphPane.XAxis, "Time");
GraphHelper.SetAxisLabel(view.Graph.GraphPane.YAxis, "Trial");
if (view.TimeSpan > 0)
{
view.Graph.XMin = 0;
view.Graph.XMax = view.TimeSpan;
}

var currentTime = 0.0;
var registerMap = new Dictionary<string, PointPairList>();
CompositeDisposable subscriptions = new();
view.HandleCreated += delegate
{
subscriptions.Add(controller.Triggers.Subscribe(group =>
{
var trigger = group.Key;
subscriptions.Add(group
.Buffer(() => timerTick)
.Subscribe(buffer =>
{
if (buffer.Count == 0) return;
foreach (var message in buffer)
{
var register = message.Value;
var timestamp = message.Seconds - trigger.Seconds;
currentTime = Math.Max(currentTime, timestamp);
if (!registerMap.TryGetValue(register.Label, out var points))
{
points = new PointPairList();
registerMap.Add(register.Label, points);
var color = GraphControl.GetColor(register.Address);
var series = view.Graph.CreateSeries(register.Label, points, color);
view.Graph.GraphPane.CurveList.Add(series);
}

points.Add(timestamp, trigger.Value);
}

if (view.TimeSpan <= 0)
{
view.Graph.XMin = 0;
view.Graph.XMax = currentTime;
}
view.Graph.Invalidate();
}));
}));
};

view.HandleDestroyed += delegate
{
subscriptions.Dispose();
TimeSpan = view.TimeSpan;
};

var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
visualizerService?.AddControl(view);
}

/// <inheritdoc/>
public override void Show(object value)
{
}

/// <inheritdoc/>
public override IObservable<object> Visualize(IObservable<IObservable<object>> source, IServiceProvider provider)
{
return Observable.Empty<object>();
}

/// <inheritdoc/>
public override void Unload()
{
view?.Dispose();
timer?.Dispose();
view = null;
controller = null;
}
}
}