diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index 7e42dc58..94a3a8ec 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -23,6 +23,11 @@ + + + + + @@ -101,6 +106,11 @@ + + + + + @@ -146,6 +156,11 @@ + + + + + diff --git a/.gitignore b/.gitignore index 414a43fb..5a1ef8da 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ bin obj Packages Bonsai.exe.WebView2 -.bonsai/Settings/ +.bonsai/Settings/** *.bin *.avi *.dll @@ -87,4 +87,7 @@ ipython_config.py # IDEs and editors .vscode/ .idea -.sln \ No newline at end of file +.sln +*.editor + +*.layout diff --git a/src/Extensions.csproj b/src/Extensions.csproj index fa0f74c2..8628665e 100644 --- a/src/Extensions.csproj +++ b/src/Extensions.csproj @@ -18,6 +18,8 @@ - + + + \ No newline at end of file diff --git a/src/Extensions/AsObject.cs b/src/Extensions/AsObject.cs new file mode 100644 index 00000000..5bf8b43d --- /dev/null +++ b/src/Extensions/AsObject.cs @@ -0,0 +1,18 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Newtonsoft.Json.Linq; + +[Combinator] +[Description("")] +[WorkflowElementCategory(ElementCategory.Transform)] +public class AsObject +{ + public IObservable Process(IObservable> source) + { + return source.Select(value => JObject.FromObject(value)); + } +} diff --git a/src/Extensions/BehaviorVisualizer.bonsai b/src/Extensions/BehaviorVisualizer.bonsai new file mode 100644 index 00000000..0e983271 --- /dev/null +++ b/src/Extensions/BehaviorVisualizer.bonsai @@ -0,0 +1,702 @@ + + + + + + + + + + + + + + + BehaviorPlot + + + + + + + Behavior + + + + + + Behavior1 + true + 1 + None + + 0 + 0 + + 0 + + + true + + + EventPlot + true + + -1 + 150 + + NoTitle + AutoFit + RangeFit + + + EventPlot + + + true + + + ContinuousPlot + true + + -1 + 150 + + NoTitle + AutoFit + None + + + ContinuousPlot + + + + EventPlot + + + + + 0 + 1 + + + R + L + + East + Outside + + + + + + + ContinuousPlot + + + + + 0 + 1 + + + East + Outside + + + + + + + + + AutoRewardLeft + + + Value + + + + 0.9 + + + + Seconds + + + + + + new(it.Item1 as AutoReward, it.Item2) + + + 1000 + Item2 + AutoReward + + + EventPlot + + + + Diamond + 3 + 0 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + 1 + + + + true + None + + + + + + AutoRewardRight + + + Value + + + + 0.1 + + + + Seconds + + + + + + new(it.Item1 as AutoReward, it.Item2) + + + 1000 + Item2 + AutoReward + + + EventPlot + + + + Diamond + 3 + 0 + 0 + 1 + 1 + 0 + 1 + 1 + 1 + 1 + + + + true + None + + + + + + LickLeft + + + Value + + + + 0.9 + + + + Seconds + + + + + + new(it.Item1 as LickLeft, it.Item2) + + + 1000 + Item2 + LickLeft + + + EventPlot + + + + Square + 3 + 0 + 0 + 0 + 1 + 0 + 1 + 1 + 1 + 1 + + + + true + None + + + + + + LickRight + + + Value + + + + 0.1 + + + + Seconds + + + + + + new(it.Item1 as LickRight, it.Item2) + + + 1000 + Item2 + LickRight + + + EventPlot + + + + Square + 3 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 1 + + + + true + None + + + + + + TimedTrial + + + + + + Value.ChoiceCategory + + + NoChoice + + + + Source1 + + + + NoChoice + + + + + + + + + + + + + 0.5 + + + + LeftChoice + + + + Source1 + + + + LeftChoice + + + + + + + + + + + + + 0.85 + + + + RightChoice + + + + Source1 + + + + RightChoice + + + + + + + + + + + + + 0.15 + + + + + + + Seconds + + + + + + + + + new(it.Seconds, it.Value as Choice) + + + 1000 + Seconds + Choice + + + EventPlot + + + + Circle + 3 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + 1 + + + + true + None + + + Value.TrialCategory + + + LeftRewarded + + + + Source1 + + + + LeftRewarded + + + + + + + + + + + + + 0.8 + + + + RightRewarded + + + + Source1 + + + + RightRewarded + + + + + + + + + + + + + 0.2 + + + + + + + + + + + + + new(it.Seconds, it.Value as Choice) + + + 1000 + Seconds + Choice + + + EventPlot + + + + Circle + 3 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 1 + + + + true + None + + + TimedTrial + + + new(it.Seconds, it.Value.Trial.Metadata.PRewardLeft as PRewardLeft, it.Value.Trial.Metadata.PRewardRight as PRewardRight ) + + + 1000 + Seconds + PRewardLeft + + + ContinuousPlot + + + + 1 + 0 + 0 + 1 + 2 + + + + true + None + + + 1000 + Seconds + PRewardRight + + + ContinuousPlot + + + + 0 + 0 + 1 + 1 + 2 + + + + true + None + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/BiasVisualizer.bonsai b/src/Extensions/BiasVisualizer.bonsai new file mode 100644 index 00000000..0425f86e --- /dev/null +++ b/src/Extensions/BiasVisualizer.bonsai @@ -0,0 +1,411 @@ + + + + + + + + + + + + BiasVisualizer + + + + + + + Bias + + + BiasPlot + true + + -1 + 150 + + NoTitle + AutoFit + None + + + BiasPlot + + + + BiasPlot + + + + + -1 + 1 + + + R + L + + East + Outside + 1.1 + -1.1 + + + + + + + LeftBiasCorrection + + + + 0.5 + + + + TrialIndexedBias + + + TrialIndex + + + + + + + new (it.Item1 as StageCorrection, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + StageCorrection + + + BiasPlot + + + + Up + 3 + 1 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 1 + + + + true + None + + + + + + RightBiasCorrection + + + + -0.5 + + + + TrialIndexedBias + + + TrialIndex + + + + + + + new (it.Item1 as StageCorrection, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + StageCorrection + + + BiasPlot + + + + Down + 3 + 0 + 0 + 1 + 1 + 0 + 0 + 1 + 1 + 1 + + + + true + None + + + + + + TrialIndexedBias + + + 1000 + TrialIndex + Bias + + + BiasPlot + + + + 0 + 0 + 0 + 1 + 2 + + + + true + None + + + 1 + TrialIndex + Bias + + + BiasPlot + + + + Circle + 5 + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 1 + + + + + true + None + + + + Bias + + + + BiasPlot + + + {0:N2} + it + + + + + + + + TrialIndex + + + + + + + + + + + + + + + true + 0.70 + 9 + 0.7 + + 0 + -10 + + None + + + + + 0.5 + + + + + + + new (it.Item1 as LeftBias, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + LeftBias + + + BiasPlot + + + + 1 + 0 + 0 + 0.7 + 1 + + + + true + None + + + + -0.5 + + + + + + + new (it.Item1 as RightBias, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + RightBias + + + BiasPlot + + + + 0 + 0 + 1 + 0.7 + 1 + + + + true + None + + + TaskLogicParameters + + + TrialGenerator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/CameraPreviewVisualizer.bonsai b/src/Extensions/CameraPreviewVisualizer.bonsai new file mode 100644 index 00000000..c8d3fc52 --- /dev/null +++ b/src/Extensions/CameraPreviewVisualizer.bonsai @@ -0,0 +1,429 @@ + + + + + + Source1 + + + + + + + + + + + + CameraPreview + + + + + + + Camera + + + true + 1 + None + + 0 + 0 + + 0 + + + Table + + + + + + TriggeredCamerasStream + + + + + + + + + Source1 + + + Key + + + + + + + BodyCamera + + + + + + + + + + + + + + + + + CameraStream + + + Table + + + true + + + CameraStream + + + EndExperiment + + + + + + Value.Image + + + + + + + 2 + + + + + + + Flipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + Item1 + + + Item2 + + + + + + + + + Vertical + + + + NotFlipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + + + Item1 + + + + + + new ( + it.Size.Width / 2 as X, + it.Size.Height / 2 as Y +) + + + + + + + + + + 320 + 240 + + + + + + + + + + + + + + 320 + 240 + + + 0 + 0 + + 0 + + 1 + 1 + + + + + + + + + + + 1,0,0;0,1,0 + Linear + + 0 + 0 + 0 + 0 + + + + + Table + + + + Rgba + Repeat + Repeat + Linear + Linear + + + + OverlayAlpha + + + + + + + + + + + + + 0.5 + 0.1 + 0.9 + + + + + + + + + PreviewImage + true + + 320 + 320 + + + 0 + 0 + + + 1 + 1 + + + + Table + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + true + Alpha + + + true + + + true + 0 + 1 + None + 0.5 + + + OverlayAlpha + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/CategorizeTrial.cs b/src/Extensions/CategorizeTrial.cs new file mode 100644 index 00000000..1235e82b --- /dev/null +++ b/src/Extensions/CategorizeTrial.cs @@ -0,0 +1,87 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Bonsai.Harp; +using AindDynamicForagingDataSchema; +using System.Xml.Serialization; + +[Combinator] +[Description("")] +[WorkflowElementCategory(ElementCategory.Transform)] +public class CategorizeTrial +{ + public IObservable> Process(IObservable> source) + { + return source.Select(value => + { + var trial = value.Value; + bool isAutoResponse = trial.Trial.IsAutoRewardRight.HasValue; + var categorizedTrial = new CategorizedTrial(); + if (!trial.IsRightChoice.HasValue) + { + categorizedTrial.ChoiceCategory = Choice.NoChoice; + } + else if (trial.IsRightChoice.Value) + { + if (isAutoResponse) + { + if (trial.IsRewarded) + { + categorizedTrial.TrialCategory = Category.RightRewardedAuto; + } + else + { + categorizedTrial.TrialCategory = Category.RightUnrewardedAuto; + } + } + else + { + categorizedTrial.ChoiceCategory = Choice.RightChoice; + if (trial.IsRewarded) + { + categorizedTrial.TrialCategory = Category.RightRewarded; + } + else + { + categorizedTrial.TrialCategory = Category.RightUnrewarded; + } + } + } + else + { + if (isAutoResponse) + { + if (trial.IsRewarded) + { + categorizedTrial.TrialCategory = Category.LeftRewardedAuto; + } + else + { + categorizedTrial.TrialCategory = Category.LeftUnrewardedAuto; + } + } + else + { + categorizedTrial.ChoiceCategory = Choice.LeftChoice; + if (trial.IsRewarded) + { + categorizedTrial.TrialCategory = Category.LeftRewarded; + } + else + { + categorizedTrial.TrialCategory = Category.LeftUnrewarded; + } + } + } + + + return new Timestamped(categorizedTrial, value.Seconds); + }); + } +} + + + diff --git a/src/Extensions/CategorizedTrial.cs b/src/Extensions/CategorizedTrial.cs new file mode 100644 index 00000000..293258b3 --- /dev/null +++ b/src/Extensions/CategorizedTrial.cs @@ -0,0 +1,24 @@ +public enum Choice +{ + NoChoice, + RightChoice, + LeftChoice +} + +public enum Category +{ + RightRewardedAuto, + LeftRewardedAuto, + RightUnrewardedAuto, + LeftUnrewardedAuto, + RightRewarded, + RightUnrewarded, + LeftRewarded, + LeftUnrewarded, +} + +public class CategorizedTrial +{ + public Choice ChoiceCategory {get; set;} + public Category TrialCategory {get; set;} +} \ No newline at end of file diff --git a/src/Extensions/EnableableButtonBuilder.cs b/src/Extensions/EnableableButtonBuilder.cs new file mode 100644 index 00000000..33b067d2 --- /dev/null +++ b/src/Extensions/EnableableButtonBuilder.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel; +using System.Numerics; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai.ImGui; +using OpenCV.Net; +using ImGui = Hexa.NET.ImGui.ImGui; + +/// +/// Represents an operator that draws a button control and generates +/// a sequence of notifications whenever the button is clicked. +/// +[Description("Draws a button control and generates a sequence of notifications whenever the button is clicked.")] +public class EnableableButtonBuilder : TextControlBuilder +{ + public bool Enabled {get; set;} + public Size Size {get; set;} + + private Vector2 buttonSize + { + get + { + return new Vector2(Size.Width, Size.Height); + } + } + + /// + protected override IObservable Generate(IObservable source) + { + return Observable.Create(observer => + { + var capturedName = Name; + var capturedText = Text; + var idSuffix = "##" + capturedName; + var label = capturedText + idSuffix; + var output = capturedName ?? capturedText; + var sourceObserver = Observer.Create( + _ => + { + var text = Text; + if (text != capturedText) + { + capturedText = text; + label = text + idSuffix; + output = capturedName ?? text; + } + if (Visible) + { + if (Enabled) + { + if (ImGui.Button(label, buttonSize)) + observer.OnNext(output); + } else + { + ImGui.BeginDisabled(); + ImGui.Button(label, buttonSize); + ImGui.EndDisabled(); + } + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} \ No newline at end of file diff --git a/src/Extensions/ExtractChoiceValues.cs b/src/Extensions/ExtractChoiceValues.cs new file mode 100644 index 00000000..eb673a7e --- /dev/null +++ b/src/Extensions/ExtractChoiceValues.cs @@ -0,0 +1,28 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Bonsai.Harp; +using AindDynamicForagingDataSchema; + +[Combinator] +[Description("")] +[WorkflowElementCategory(ElementCategory.Transform)] +public class ExtractChoiceValues +{ + public IObservable Process(IObservable> source) + { + return source.Select(value => { + var choice = value.Value.IsRightChoice; + if (choice.HasValue) + { + return choice.Value ? 1.0f : 0.0f; + } else + { + return (float?)null; + } + }); + } +} diff --git a/src/Extensions/IntraSessionVisualizer.bonsai b/src/Extensions/IntraSessionVisualizer.bonsai new file mode 100644 index 00000000..f21e3c75 --- /dev/null +++ b/src/Extensions/IntraSessionVisualizer.bonsai @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + + + + InterSessionVisualizer + + + + + + + StartExperiment + + + + true + + + + + false + + + + + + + ExperimentStarted + + + + + + IntraSession + + + true + 3 + Borders + + 0 + 0 + + 0 + + + true + + + true + None + + + true + Manual + None + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + ExperimentStarted + + + + + + + + GiveLeft + true + Give Left + true + + 0 + 0 + + + + + + + + UiGiveLeft + + + true + + + ExperimentStarted + + + + + + + + GiveRight + true + Give Right + true + + 0 + 0 + + + + + + + + UiGiveRight + + + true + Go Cue + None + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + ExperimentStarted + + + + + + + + GiveLeftCue + true + Give Left Cue + true + + 0 + 0 + + + + + + + + UiCueGiveLeft + + + true + + + ExperimentStarted + + + + + + + + GiveRightCue + true + Give Right Cue + true + + 0 + 0 + + + + + + + + UiCueGiveRight + + + + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + ExperimentStarted + + + + + + + + MoveLeft + true + Move Left + true + + 0 + 0 + + + + + + + + UiMoveLeft + + + true + + + ExperimentStarted + + + + + + + + MoveRight + true + Move Right + true + + 0 + 0 + + + + + + + + UiMoveRight + + + + + + + + + true + + + + + + SessionMetadata + + + + + + + + + true + + + + true + + + Notes + true + Notes + 16384 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/LineStyling.cs b/src/Extensions/LineStyling.cs new file mode 100644 index 00000000..d8fc7dde --- /dev/null +++ b/src/Extensions/LineStyling.cs @@ -0,0 +1,38 @@ +using Hexa.NET.ImPlot; +using System; +using System.ComponentModel; +using Bonsai.ImPlot; + +using ImPlot = Hexa.NET.ImPlot.ImPlot; +using Bonsai; +using System.Reactive.Linq; +using System.Reactive; +using System.Linq; +using System.Drawing; +using System.Numerics; + +[Combinator] +public class LineStyling +{ + public float ColorR {get; set;} + public float ColorG {get; set;} + public float ColorB {get; set;} + public float ColorA {get; set;} + public float LineThickness {get; set;} + + public unsafe IObservable Process(IObservable source) + { + return Observable.Create(observer => + { + var sourceObserver = Observer.Create( + value => + { + ImPlot.SetNextLineStyle(new Vector4(ColorR, ColorG, ColorB, ColorA), LineThickness); + observer.OnNext(value); + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} \ No newline at end of file diff --git a/src/Extensions/MetadataVisualizer.bonsai b/src/Extensions/MetadataVisualizer.bonsai new file mode 100644 index 00000000..64d8e52a --- /dev/null +++ b/src/Extensions/MetadataVisualizer.bonsai @@ -0,0 +1,349 @@ + + + + + + Source1 + + + + + + + + MetadataVisualizer + + + + Source1 + + + CurrentMetadata + + + + + + Metadata + + + true + + + true + + + Stage + WidthFixed + 0 + + + Experimenter + WidthFixed + 0 + + + Subject + WidthFixed + 0 + + + Experiment + WidthFixed + 0 + + + Habituation + WidthStretch + 0 + + + Control + WidthStretch + 0 + + + BordersInnerH BordersOuterH BordersInnerV BordersOuterV BordersH BordersV BordersInner BordersOuter Borders SizingFixedFit + + 0 + 0 + + 0 + true + + + true + + + CurrentMetadata + + + + Item2.StageName + + + + + + + + true + Task6 + + + true + + + CurrentMetadata + + + Item1.Experimenter + + + + + + Source1 + + + Count + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + + + true + Expo + + + true + + + CurrentMetadata + + + Item1.Subject + + + + + + + + true + Plimbo + + + true + + + CurrentMetadata + + + Item1.Experiment + + + + + + + + + true + Task6 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + true + Habituation + + + true + + + Habituation + true + None + 0 + 0 + 0 + + + + + + UiHabituationTimeUpdate + + + + + + true + + + true + + + Start + WidthStretch + 0 + + + Stop + WidthStretch + 0 + + + None + + 0 + 0 + + 0 + false + + + true + + + StartButton + true + START + true + + 100 + 50 + + + + + + + + StartExperimentShortcut + + + true + + + StopButton + true + STOP + true + + 100 + 50 + + + + + EndExperiment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/MultiLineInputTextBuilder.cs b/src/Extensions/MultiLineInputTextBuilder.cs new file mode 100644 index 00000000..1bf56860 --- /dev/null +++ b/src/Extensions/MultiLineInputTextBuilder.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai.ImGui; +using ImGui = Hexa.NET.ImGui.ImGui; + +/// +/// Represents an operator that draws an input text box and generates +/// a sequence of notifications whenever the text changes. +/// +[Description("Draws a multi-line input text box and generates a sequence of notifications whenever the text changes.")] +public class MultiLineInputTextBuilder : TextControlBuilder +{ + /// + /// Gets or sets the maximum number of characters allowed in the text box. + /// + [Description("The maximum number of characters allowed in the text box.")] + public ulong Capacity { get; set; } + + /// + protected override IObservable Generate(IObservable source) + { + return Observable.Create(observer => + { + var buf = Text ?? string.Empty; + observer.OnNext(buf); + var bufSize = (System.UIntPtr)Capacity; + var label = "##" + Name; + var sourceObserver = Observer.Create( + _ => + { + if (Visible && ImGui.InputTextMultiline(label, ref buf, bufSize)) + observer.OnNext(buf); + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} \ No newline at end of file diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index 2f914b1d..67ae506f 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -7,8 +7,8 @@ xmlns:beh="clr-namespace:Harp.Behavior;assembly=Harp.Behavior" xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions" xmlns:sys="clr-namespace:System;assembly=mscorlib" - xmlns:p2="clr-namespace:Harp.StepperDriver;assembly=Harp.StepperDriver" - xmlns:p3="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core" + xmlns:p2="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core" + xmlns:p3="clr-namespace:Harp.StepperDriver;assembly=Harp.StepperDriver" xmlns:p4="clr-namespace:;assembly=Extensions" xmlns:p5="clr-namespace:AllenNeuralDynamics.AindManipulator;assembly=AllenNeuralDynamics.AindManipulator" xmlns:p6="clr-namespace:Harp.SoundCard;assembly=Harp.SoundCard" @@ -660,23 +660,6 @@ ManipulatorTask - - IsRightTriggerQuickRetract - - - - it ? 2 : 4 - - - Write - - - - StepperDriverCommands - - - SpoutAvailableState - StartExperiment @@ -698,22 +681,249 @@ InitialManipulatorPosition - - InitialManipulatorPosition + + InitialManipulatorPosition SoftwareEvent + + EndExperiment + + + ExtendedManipulatorPosition + + + + + + + 1 + + + + Item2 + + + + FinalManipulatorPosition + + + + SoftwareEvent + + + ManipulatorPosition + + + + 1 + + + + ExtendedManipulatorPosition + + + IsRightTriggerQuickRetract + + + UpdateExtendedPosition + + + + IsRetracted + + + + 1 + + + + + + + + Source1 + + + + + + + + + + ManipulatorPosition + + + + + + Item2 + + + ExtendedManipulatorPosition + + + + false + + + + + + + Source1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SetRetractedState + + + + + true + + + + IsRetracted + + + + + + + + + + + + it ? 2 : 4 + + + Write + + + + StepperDriverCommands + + + SpoutAvailableState + SpoutAvailableState + + UpdateExtendedPosition + + + + IsRetracted + + + + 1 + + + + + + + + Source1 + + + + + + + + + + ManipulatorPosition + + + + + + Item2 + + + ExtendedManipulatorPosition + + + + false + + + + + + + Source1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CalculatePositions - InitialManipulatorPosition + ExtendedManipulatorPosition ManipulatorPosition @@ -756,6 +966,107 @@ MoveTo + + Retraction State Logic + + + + SetRetractedState + + + + SpoutAvailableState + + + + 1 + + + + + IsRetracted + + + ManipulatorPosition + + + + + + Source1 + + + + + + (Math.Round(Item1.X, 2) == Math.Round(Item2.X, 2)) && +(Math.Round(Item1.Y1, 2) == Math.Round(Item2.Y1, 2)) && +(Math.Round(Item1.Y2, 2) == Math.Round(Item2.Y2, 2)) && +(Math.Round(Item1.Z, 2) == Math.Round(Item2.Z, 2)) + + + + + + Source1 + + + + + + + + + + SpoutAvailableState + + + + 1 + + + + + + + + Source1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ManualSpoutDelta @@ -829,7 +1140,7 @@ ManipulatorBiasTracker - InitialManipulatorPosition + ManipulatorPosition X @@ -858,8 +1169,8 @@ ManipulatorBiasTracker - - ManipulatorBiasTracker + + ManipulatorBiasTracker @@ -867,37 +1178,50 @@ - - + + - - - - - + + + + + + + - - - - + + - - - + + - - + + - + + + + + + + + + + + + + + + diff --git a/src/Extensions/PlotStyling.cs b/src/Extensions/PlotStyling.cs new file mode 100644 index 00000000..9761c1c2 --- /dev/null +++ b/src/Extensions/PlotStyling.cs @@ -0,0 +1,54 @@ +using Hexa.NET.ImPlot; +using System; +using System.ComponentModel; +using Bonsai.ImPlot; + +using ImPlot = Hexa.NET.ImPlot.ImPlot; +using Bonsai; +using System.Reactive.Linq; +using System.Reactive; +using System.Linq; + +[Combinator] +public class PlotStyling +{ + public double[] TickPositions {get; set;} + public string[] TickLabels {get; set;} + public ImPlotLocation ImPlotLocationFlags {get; set;} + public ImPlotLegendFlags ImPlotLegendFlags {get; set;} + public double? YAxisMax {get; set;} + public double? YAxisMin {get; set;} + + public unsafe IObservable Process(IObservable source) + { + return Observable.Create(observer => + { + var sourceObserver = Observer.Create( + value => + { + if (TickPositions.Count() > 0) + { + if (TickLabels.Count() == TickPositions.Count()) + { + ImPlot.SetupAxisTicks(ImAxis.Y1, ref TickPositions[0], TickPositions.Count(), TickLabels); + } else + { + ImPlot.SetupAxisTicks(ImAxis.Y1, ref TickPositions[0], TickPositions.Count()); + } + } + + if (YAxisMax != null && YAxisMin != null) + { + ImPlot.SetupAxisLimits(ImAxis.Y1, (double)YAxisMax, (double)YAxisMin, ImPlotCond.Always); + } + + ImPlot.SetupLegend(ImPlotLocationFlags, ImPlotLegendFlags); + observer.OnNext(value); + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} + diff --git a/src/Extensions/PreSessionVisualizer.bonsai b/src/Extensions/PreSessionVisualizer.bonsai new file mode 100644 index 00000000..ae31e7ec --- /dev/null +++ b/src/Extensions/PreSessionVisualizer.bonsai @@ -0,0 +1,923 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreSessionVisualizer + + + + LeftOpenState + + + RightOpenState + + + BothOpenState + + + + + + PreSession + + + true + 2 + Borders + + 0 + 0 + + 0 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + BothOpenState + + + + + + + + + KeepLeftOpen + true + KeepLeftOpen + false + Open Left + Close Left + true + + + LeftOpenState + + + + + + UiKeepLeftOpen + + + + + + + + KeepRightOpen + true + KeepRightOpen + false + Open Right + Close Right + true + + + RightOpenState + + + + + + UiKeepRightOpen + + + LeftOpenState + + + RightOpenState + + + + + + + + + + + + + KeepBothOpen + true + KeepBothOpen + false + Open Both + Close Both + true + + + BothOpenState + + + + + + UiKeepBothOpen + + + + + + true + + + + + + DrainLeftComplete + + + + + + + + DrainLeft + true + Drain Left + false + Drain Left + Cancel Drain Left + true + + + + + + UiDrainLeft + + + + + + DrainRightComplete + + + + + + + + DrainRight + true + Drain Right + false + Drain Right + Cancel Drain Right + true + + + + + + UiDrainRight + + + + + + + + + + + DrainBoth + true + Drain Both + false + Drain Both + Cancel Drain Both + true + + + + + + UiDrainBoth + + + + + + + + + true + + + true + 2 + Borders + + 0 + 0 + + 0 + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + IsRetracted + + + + + + + + + MoveLeft + true + Move Left + true + + 100 + 20 + + + + + + + + UiManipulatorLeft + + + + + + + + ForwardLeft + true + Forward Left + true + + 100 + 20 + + + + + + + + UiManipulatorForwardLeft + + + + + + + + ForwardRight + true + Forward Right + true + + 100 + 20 + + + + + + + + UiManipulatorForwardRight + + + + + + + + Up + true + Up + true + + 100 + 20 + + + + + + + + UiManipulatorUp + + + + + + true + + + IsRetracted + + + + + + + + + MoveRight + true + Move Right + true + + 100 + 20 + + + + + + + + UiManipulatorRight + + + + + + + + BackLeft + true + Back Left + true + + 100 + 20 + + + + + + + + UiManipulatorBackLeft + + + + + + + + BackRight + true + Back Right + true + + 100 + 20 + + + + + + + + UiManipulatorBackRight + + + + + + + + Down + true + Down + true + + 100 + 20 + + + + + + + + UiManipulatorDown + + + + + + + + + true + + + STOP + true + STOP + + + + + + + UiManipulatorStop + + + IsRetracted + + + + + + + + + Home + true + Home + true + + 100 + 20 + + + + + + + + UiManipulatorHome + + + + + + + + Initialize + true + Initialize + true + + 100 + 20 + + + + + + + + UiManipulatorInitialize + + + + + + + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + true + x: + + + true + + + + + + ManipulatorPosition + + + X + + + {0:N2} + it + + + + + + + + true + 14.05 + + + + + + true + + + true + y1: + + + true + + + Y1 + + + {0:N2} + it + + + + + + + + true + 11.80 + + + + + + true + + + true + y2: + + + true + + + Y2 + + + {0:N2} + it + + + + + + + + true + 11.90 + + + + + + true + + + true + z: + + + true + + + Z + + + {0:N2} + it + + + + + + + + true + 17.80 + + + + + + true + + + true + step: + + + true + + + true + None + 0 + 0 + 0 + + + OffsetGain + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Extensions/SaturationShaderPass.cs b/src/Extensions/SaturationShaderPass.cs new file mode 100644 index 00000000..770896c1 --- /dev/null +++ b/src/Extensions/SaturationShaderPass.cs @@ -0,0 +1,207 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Hexa.NET.ImGui; +using OpenTK.Graphics.OpenGL4; +using System.Runtime.Remoting.Contexts; + + +public class SaturationShaderPass : Combinator +{ + public float Alpha { get; set; } + public float MinSaturation {get; set;} + public float MaxSaturation {get; set;} + + const string VertexShaderSource = @" + #version 330 core + layout(location = 0) in vec2 vertexPosition; + layout(location = 1) in vec2 vertexTexCoords; + + out vec2 texCoord; + + void main() + { + texCoord = vec2(.5, .5) * (vertexPosition.xy + 1); + gl_Position = vec4(vertexPosition.xy, 0.0, 1.0); + } + "; + + const string SaturationFragmentShader = @" + #version 330 core + uniform sampler2D tex0; + uniform vec2 iResolution; + uniform float minSaturation = 0.1; + uniform float maxSaturation = 0.9; + uniform float alpha = 0.5; + in vec2 texCoord; + out vec4 fragColor; + + void main() + { + vec4 texColor = texture(tex0, texCoord); + vec4 overlay = vec4(0,0,0,0); + + if (texColor.r >= maxSaturation) overlay = vec4(1,0,0,alpha); + else if (texColor.r <= minSaturation) overlay = vec4(0,0,1,alpha); + + fragColor = mix(texColor, overlay, overlay.a); + } + "; + + static readonly float[] QuadVertices = new float[] { -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f }; + + public override IObservable Process(IObservable source) + { + return Observable.Defer(() => + { + int shaderProgram = 0; + int vertexArray = 0; + int framebuffer = 0; + int targetTexture = 0; + int targetWidth = 0; + int targetHeight = 0; + var targetRef = default(ImTextureRef); + int alphaLocation = 0; + int minSaturation = 0; + int maxSaturation = 0; + return source.Select(texture => + { + var currentContext = ImGui.GetCurrentContext(); + var sourceTexture = (int)(ulong)texture.GetTexID(); + if (sourceTexture == 0) return texture; + + if (shaderProgram == 0) + { + shaderProgram = CreateProgram(VertexShaderSource, SaturationFragmentShader); + vertexArray = CreateQuad(); + framebuffer = GL.GenFramebuffer(); + alphaLocation = GL.GetUniformLocation(shaderProgram, "alpha"); + maxSaturation = GL.GetUniformLocation(shaderProgram, "maxSaturation"); + minSaturation = GL.GetUniformLocation(shaderProgram, "minSaturation"); + } + + // The pass renders into a texture of the same size as the incoming one. + int width, height; + GL.BindTexture(TextureTarget.Texture2D, sourceTexture); + GL.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureWidth, out width); + GL.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureHeight, out height); + if (width != targetWidth || height != targetHeight) + { + targetTexture = CreateTarget(targetTexture, width, height); + targetRef = CreateTextureRef(targetTexture); + targetWidth = width; + targetHeight = height; + GL.BindFramebuffer(FramebufferTarget.Framebuffer, framebuffer); + GL.FramebufferTexture2D( + FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, + TextureTarget.Texture2D, targetTexture, 0); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); + } + + var viewport = new int[4]; + GL.GetInteger(GetPName.Viewport, viewport); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, framebuffer); + GL.Viewport(0, 0, width, height); + GL.UseProgram(shaderProgram); + + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, sourceTexture); + GL.Uniform1(alphaLocation, Alpha); + GL.Uniform1(maxSaturation, MaxSaturation); + GL.Uniform1(minSaturation, MinSaturation); + + GL.BindVertexArray(vertexArray); + GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4); + + GL.BindVertexArray(0); + GL.UseProgram(0); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); + GL.Viewport(viewport[0], viewport[1], viewport[2], viewport[3]); + + return targetRef; + }); + }); + } + + static int CreateQuad() + { + var vertexArray = GL.GenVertexArray(); + GL.BindVertexArray(vertexArray); + GL.BindBuffer(BufferTarget.ArrayBuffer, GL.GenBuffer()); + GL.BufferData( + BufferTarget.ArrayBuffer, QuadVertices.Length * sizeof(float), + QuadVertices, BufferUsageHint.StaticDraw); + GL.EnableVertexAttribArray(0); + GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 0, 0); + GL.BindVertexArray(0); + return vertexArray; + } + + static int CreateTarget(int texture, int width, int height) + { + if (texture == 0) texture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, texture); + GL.TexImage2D( + TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, width, height, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + return texture; + } + + static ImTextureRef CreateTextureRef(int texture) + { + var textureRef = default(ImTextureRef); + textureRef.TexID = new ImTextureID((IntPtr)texture); + return textureRef; + } + + static int CreateProgram(string vertexCode, string fragmentCode) + { + var vertexShader = CompileShader(ShaderType.VertexShader, vertexCode); + var fragmentShader = CompileShader(ShaderType.FragmentShader, fragmentCode); + int status; + + var program = GL.CreateProgram(); + GL.AttachShader(program, vertexShader); + GL.AttachShader(program, fragmentShader); + GL.LinkProgram(program); + GL.DetachShader(program, vertexShader); + GL.DetachShader(program, fragmentShader); + GL.GetProgram(program, GetProgramParameterName.LinkStatus, out status); + + if (status == 0) + { + var infoLog = GL.GetProgramInfoLog(program); + GL.DeleteProgram(program); + throw new InvalidOperationException(string.Format("Failed to link the shader program: {0}", infoLog)); + } + + GL.DeleteShader(vertexShader); + GL.DeleteShader(fragmentShader); + + return program; + } + + static int CompileShader(ShaderType type, string source) + { + int status; + + var shader = GL.CreateShader(type); + GL.ShaderSource(shader, source); + GL.CompileShader(shader); + GL.GetShader(shader, ShaderParameter.CompileStatus, out status); + + if (status == 0) + { + var infoLog = GL.GetShaderInfoLog(shader); + GL.DeleteShader(shader); + throw new InvalidOperationException(string.Format("Failed to compile the {0}.", infoLog)); + } + + return shader; + } +} diff --git a/src/Extensions/SaturationThreshold.cs b/src/Extensions/SaturationThreshold.cs new file mode 100644 index 00000000..4859d73a --- /dev/null +++ b/src/Extensions/SaturationThreshold.cs @@ -0,0 +1,77 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using OpenCV.Net; + +[Combinator] +[Description("Overlays a saturation warning on a grayscale image: pixels at or above MaxSaturation are tinted red (over-exposed), pixels at or below MinSaturation are tinted blue (under-exposed).")] +[WorkflowElementCategory(ElementCategory.Transform)] +public class SaturationThreshold +{ + [Description("Intensity at or below which a pixel is marked blue (under-exposed).")] + public double MinSaturation { get; set; } + + [Description("Intensity at or above which a pixel is marked red (over-exposed).")] + public double MaxSaturation { get; set; } + + [Description("Blend strength of the overlay tint in the range [0, 1]. 1 = solid, 0 = invisible.")] + public double Alpha { get; set; } + + public IObservable Process(IObservable source) + { + return source.Select(input => + { + // The saturation test operates on a single intensity channel, matching the + // fragment shader's use of texColor.r on a grayscale feed. + var gray = input.Channels == 1 ? input : ExtractGray(input); + + // Build the two saturation masks (255 where the condition holds, else 0). + var overMask = new IplImage(gray.Size, IplDepth.U8, 1); // r >= MaxSaturation + var underMask = new IplImage(gray.Size, IplDepth.U8, 1); // r <= MinSaturation + CV.Threshold(gray, overMask, MaxSaturation, 255, ThresholdTypes.Binary); + CV.Threshold(gray, underMask, MinSaturation, 255, ThresholdTypes.BinaryInv); + + // The shader uses "else if", so red takes priority over blue. + // Remove any overlap from the blue mask (a no-op when Min < Max). + CV.Sub(underMask, overMask, underMask); + + // Colorize the grayscale image, then paint the overlays over it. + var output = new IplImage(gray.Size, IplDepth.U8, 3); + CV.CvtColor(gray, output, ColorConversion.Gray2Bgr); + + var alpha = Math.Max(0.0, Math.Min(1.0, Alpha)); + ApplyOverlay(output, overMask, new Scalar(0, 0, 255, 0), alpha); // BGR red + ApplyOverlay(output, underMask, new Scalar(255, 0, 0, 0), alpha); // BGR blue + return output; + }); + } + + static IplImage ExtractGray(IplImage input) + { + var gray = new IplImage(input.Size, IplDepth.U8, 1); + CV.CvtColor(input, gray, ColorConversion.Bgr2Gray); + return gray; + } + + // Reproduces mix(texColor, overlay, overlay.a): result = texColor * (1 - alpha) + + // overlayColor * alpha, applied only on the masked pixels. + static void ApplyOverlay(IplImage image, IplImage mask, Scalar color, double alpha) + { + if (alpha >= 1.0) + { + image.Set(color, mask); // fully opaque: just paint the tint + return; + } + + var solid = new IplImage(image.Size, image.Depth, image.Channels); + solid.SetZero(); + solid.Set(color); + + var blended = new IplImage(image.Size, image.Depth, image.Channels); + CV.AddWeighted(image, 1 - alpha, solid, alpha, 0, blended); + CV.Copy(blended, image, mask); // write the blended result back only where the mask is set + } +} diff --git a/src/Extensions/ScatterStyling.cs b/src/Extensions/ScatterStyling.cs new file mode 100644 index 00000000..0d6235a2 --- /dev/null +++ b/src/Extensions/ScatterStyling.cs @@ -0,0 +1,40 @@ +using Hexa.NET.ImPlot; +using System; +using ImPlot = Hexa.NET.ImPlot.ImPlot; +using Bonsai; +using System.Reactive.Linq; +using System.Reactive; +using System.Linq; +using System.Numerics; + +[Combinator] +public class ScatterStyling +{ + public ImPlotMarker ImPlotMarker {get; set;} + public float Size {get; set;} + public float FillColorR {get; set;} + public float FillColorG {get; set;} + public float FillColorB {get; set;} + public float FillColorA {get; set;} + public float OutlineColorR {get; set;} + public float OutlineColorG {get; set;} + public float OutlineColorB {get; set;} + public float OutlineColorA {get; set;} + public float OutlineWeight {get; set;} + + public unsafe IObservable Process(IObservable source) + { + return Observable.Create(observer => + { + var sourceObserver = Observer.Create( + value => + { + ImPlot.SetNextMarkerStyle(ImPlotMarker, Size, new Vector4(FillColorR, FillColorG, FillColorB, FillColorA), OutlineWeight, new Vector4(OutlineColorR, OutlineColorG, OutlineColorB, OutlineColorA)); + observer.OnNext(value); + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} diff --git a/src/Extensions/SessionInfoVisualizer.cs b/src/Extensions/SessionInfoVisualizer.cs new file mode 100644 index 00000000..df71c2a0 --- /dev/null +++ b/src/Extensions/SessionInfoVisualizer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Reactive; +using System.Reactive.Linq; +using System.Xml.Serialization; +using Bonsai; +using Hexa.NET.ImGui; + +[Combinator] +public class SessionInfoVisualizer +{ + public bool Visible {get; set;} + + private Dictionary _sessionInfo = new Dictionary(); + + [XmlIgnore] + public object SessionInfo + { + get { return _sessionInfo; } + set + { + _sessionInfo = ObjectToStringDict(value); + } + } + + public SessionInfoVisualizer() + { + } + + private static Dictionary ObjectToStringDict(object obj) + { + if (obj == null) return new Dictionary(); + var result = new Dictionary(); + var jo = (Newtonsoft.Json.Linq.JObject)obj; + foreach (var prop in jo.Properties()) + { + result[prop.Name] = prop.Value != null ? prop.Value.ToString() : string.Empty; + } + return result; + } + + public IObservable Process(IObservable source) + { + return Observable.Create(observer => + { + var sourceObserver = Observer.Create( + value => + { + if (Visible) + { + var tableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchSame; + + ImGui.Text("Session Info"); + if (ImGui.BeginTable("Session Info", 2, tableFlags)) + { + ImGui.TableSetupColumn("Field"); + ImGui.TableSetupColumn("Value"); + + KeyValuePair[] fields; + fields = _sessionInfo.ToArray(); + foreach (var field in fields) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + ImGui.Text(field.Key); + + ImGui.TableSetColumnIndex(1); + ImGui.Text(field.Value); + } + + ImGui.EndTable(); + } + observer.OnNext(value); + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} + diff --git a/src/Extensions/SoftwareEventVisualizer.cs b/src/Extensions/SoftwareEventVisualizer.cs index 7076ae56..6788e743 100644 --- a/src/Extensions/SoftwareEventVisualizer.cs +++ b/src/Extensions/SoftwareEventVisualizer.cs @@ -1,98 +1,99 @@ -using Bonsai.Design; -using Bonsai.Expressions; -using AllenNeuralDynamics.AindBehaviorServices.DataTypes; -using AllenNeuralDynamics.Core.Design; +using AllenNeuralDynamics.AindBehaviorServices.DataTypes; +using Bonsai; using Hexa.NET.ImGui; using Hexa.NET.ImPlot; using System; using System.Collections.Generic; -using System.Drawing; +using System.ComponentModel; using System.Numerics; +using System.Reactive; +using Bonsai.Harp; +using System.Reactive.Linq; +using System.Collections.Immutable; using System.Runtime.InteropServices; -using System.Windows.Forms; +using System.Drawing; +using System.Xml.Serialization; -public class SoftwareEventVisualizer : BufferedVisualizer +[Combinator] +public class SoftwareEventVisualizer { - private const float MinPlotHeight = 100.0f; + public bool Visible {get; set;} + public ImmutableList> SoftwareEvents {get; set;} + public string TrialBreakEventName {get; set;} + public float PlotHeight {get; set;} + private const double YAxisMin = 0.0; private const double YAxisMax = 1.0; private const float InputWidth = 80.0f; - private float fontSize = 16.0f; - private float timeWindow = 30.0f; - - private List shadedAreaPlotters = new List(); - private List pointPlotters = new List(); - - private ImGuiControl imGuiCanvas; - private DateTimeOffset startTime; + private float timeWindow = 20.0f; + public List ShadedAreaPlotters {get; set;} + public List PointPlotters {get; set;} + private int maxTrials = 0; private readonly Dictionary> eventHistory = new Dictionary>(); - private double latestTimestamp = 0; - private string trialBreakEventName = ""; - private int maxTrials = 0; private readonly List trialBreaks = new List(); + private double lastTrialBreak = 0; + private DateTimeOffset startTime; + private double latestTimestamp = 0; - private bool HasTrialBreaks { get { return !string.IsNullOrEmpty(trialBreakEventName); } } + private bool HasTrialBreaks { get { return !string.IsNullOrEmpty(TrialBreakEventName); } } private int TrialCount { get { return HasTrialBreaks ? trialBreaks.Count + 1 : 1; } } - private struct EventRecord - { - public double Timestamp; - } - - private struct ShadedSegment + public IObservable Process(IObservable source) { - public double Timestamp; - public ShadedAreaPlotter Config; - } - - /// - public override void Show(object value) - { - } - - /// - protected override void ShowBuffer(IList> values) - { - foreach (var v in values) + return Observable.Create(observer => { - if (!(v.Value is SoftwareEvent)) continue; - var softwareEvent = (SoftwareEvent)v.Value; - - double timestamp = (v.Timestamp - startTime).TotalSeconds; - - string name = softwareEvent.Name; - if (string.IsNullOrEmpty(name)) continue; - - if (HasTrialBreaks && name == trialBreakEventName) - { - trialBreaks.Add(timestamp); - } - - List records; - if (!eventHistory.TryGetValue(name, out records)) - { - records = new List(); - eventHistory[name] = records; - } - records.Add(new EventRecord { Timestamp = timestamp }); - } - - CleanupOldEvents(); - - base.ShowBuffer(values); - if (imGuiCanvas != null) imGuiCanvas.Invalidate(); + startTime = DateTimeOffset.Now; + SoftwareEvents.Clear(); + eventHistory.Clear(); + trialBreaks.Clear(); + lastTrialBreak = 0; + var sourceObserver = Observer.Create( + value => + { + if (Visible) + { + foreach (var v in SoftwareEvents) + { + double timestamp = (v.Timestamp - startTime).TotalSeconds; + + string name = v.Value.Name; + if (HasTrialBreaks && name == TrialBreakEventName) + { + if ((timestamp - lastTrialBreak) > 0.1) + { + trialBreaks.Add(timestamp); + lastTrialBreak = timestamp; + } + } + + List records; + if (!eventHistory.TryGetValue(name, out records)) + { + records = new List(); + eventHistory[name] = records; + } + records.Add(new EventRecord { Timestamp = timestamp }); + } + + CleanupOldEvents(); + + DrawEvents(); + + observer.OnNext(value); + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); } - /// - /// Removes old event records outside the visible window. - /// Keeps the last event before the window for shaded area continuity. - /// private void CleanupOldEvents() - { + { latestTimestamp = (DateTimeOffset.Now - startTime).TotalSeconds; - + double cutoffTime = latestTimestamp - timeWindow; if (HasTrialBreaks && maxTrials > 0 && trialBreaks.Count > 0) @@ -144,42 +145,50 @@ private void CleanupOldEvents() } } - private static Vector4 ToVec4(Color color) + private void DrawEvents() { - return new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); - } + latestTimestamp = (DateTimeOffset.Now - startTime).TotalSeconds; - void StyleColors() - { - ImGui.StyleColorsLight(); - ImPlot.StyleColorsLight(ImPlot.GetStyle()); - } + ImGui.Text("Time Window (s):"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(InputWidth); + ImGui.InputFloat("##timewindow", ref timeWindow); + if (timeWindow < 1.0f) timeWindow = 1.0f; - /// - /// Converts absolute timestamp to plot-relative time where 0 = now. - /// - private double ToPlotTime(double timestamp) - { - return timestamp - latestTimestamp; - } + var availableSize = ImGui.GetContentRegionAvail(); - /// - /// Returns which trial a timestamp belongs to. - /// - private int GetTrialIndex(double timestamp) - { - if (!HasTrialBreaks || trialBreaks.Count == 0) return 0; - for (int i = trialBreaks.Count - 1; i >= 0; i--) + double plotTMin = -(double)timeWindow; + double plotTMax = 0.0; + + int firstVisible, numVisible; + GetVisibleTrialRange(out firstVisible, out numVisible); + + double yMin = HasTrialBreaks ? (double)firstVisible : YAxisMin; + double yMax = HasTrialBreaks ? (double)(firstVisible + numVisible) : YAxisMax; + + ImPlot.SetNextAxesLimits(plotTMin, plotTMax, yMin, yMax, ImPlotCond.Always); + if (ImPlot.BeginPlot("Software Events", new Vector2(-1, PlotHeight), ImPlotFlags.NoTitle)) { - if (timestamp >= trialBreaks[i]) - return i + 1; + ImPlot.SetupAxes("Time (s)", HasTrialBreaks ? "Trial" : "Value"); + ImPlot.SetupAxisLimits(ImAxis.Y1, yMin, yMax, ImPlotCond.Always); + ImPlot.SetupLegend(ImPlotLocation.North, ImPlotLegendFlags.Outside | ImPlotLegendFlags.Horizontal); + + if (HasTrialBreaks && numVisible > 0) + { + SetupTrialAxisTicks(firstVisible, numVisible); + } + + DrawAllShadedAreas(plotTMin, plotTMax); + + foreach (var config in PointPlotters) + { + DrawPointMarkers(config, plotTMin, plotTMax); + } + + ImPlot.EndPlot(); } - return 0; } - /// - /// Computes visible trial range based on MaxTrials rolling window. - /// private void GetVisibleTrialRange(out int firstVisible, out int numVisible) { int total = TrialCount; @@ -200,65 +209,49 @@ private void GetVisibleTrialRange(out int firstVisible, out int numVisible) } } - /// - /// Builds merged timeline of shaded area events, sorted by timestamp. - /// - private List BuildMergedTimeline() + unsafe private void SetupTrialAxisTicks(int firstVisibleTrial, int numVisibleTrials) { - var merged = new List(); - - foreach (var config in shadedAreaPlotters) - { - List records; - if (!eventHistory.TryGetValue(config.EventName, out records)) - continue; + if (numVisibleTrials <= 0) return; - for (int i = 0; i < records.Count; i++) - { - merged.Add(new ShadedSegment - { - Timestamp = records[i].Timestamp, - Config = config - }); - } - } + var positions = new double[numVisibleTrials]; + var labelData = new byte[numVisibleTrials][]; - merged.Sort(delegate(ShadedSegment a, ShadedSegment b) + for (int t = 0; t < numVisibleTrials; t++) { - return a.Timestamp.CompareTo(b.Timestamp); - }); - - return merged; - } + int trialNum = firstVisibleTrial + t; + positions[t] = trialNum + 0.5; + labelData[t] = System.Text.Encoding.UTF8.GetBytes(trialNum.ToString() + '\0'); + } - /// - /// Draws a single shaded rectangle in a given trial row. - /// - unsafe private void DrawShadedRect(ShadedAreaPlotter config, double tStart, double tEnd, int trialIndex) - { - double x0 = ToPlotTime(tStart); - double x1 = ToPlotTime(tEnd); - double yLow = HasTrialBreaks ? (double)trialIndex : 0.0; - double yHigh = HasTrialBreaks ? (double)(trialIndex + 1) : 1.0; + var handles = new GCHandle[numVisibleTrials]; + var ptrs = new IntPtr[numVisibleTrials]; - var color = ToVec4(config.Color); - ImPlot.SetNextLineStyle(color, 0f); - ImPlot.SetNextFillStyle(color, config.Alpha); + try + { + for (int t = 0; t < numVisibleTrials; t++) + { + handles[t] = GCHandle.Alloc(labelData[t], GCHandleType.Pinned); + ptrs[t] = handles[t].AddrOfPinnedObject(); + } - fixed (double* xs = new double[] { x0, x1 }) - fixed (double* ysL = new double[] { yLow, yLow }) - fixed (double* ysH = new double[] { yHigh, yHigh }) + fixed (double* posPtr = positions) + fixed (IntPtr* labelPtrs = ptrs) + { + ImPlot.SetupAxisTicks(ImAxis.Y1, posPtr, numVisibleTrials, (byte**)labelPtrs, false); + } + } + finally { - ImPlot.PlotShaded(config.EventName, xs, ysL, ysH, 2); + for (int t = 0; t < numVisibleTrials; t++) + { + if (handles[t].IsAllocated) handles[t].Free(); + } } } - /// - /// Draws shaded areas as mutually exclusive regions, split at trial boundaries when active. - /// unsafe private void DrawAllShadedAreas(double plotTMin, double plotTMax) { - if (shadedAreaPlotters.Count == 0) return; + if (ShadedAreaPlotters.Count == 0) return; var timeline = BuildMergedTimeline(); if (timeline.Count == 0) return; @@ -323,9 +316,46 @@ unsafe private void DrawAllShadedAreas(double plotTMin, double plotTMax) } } - /// - /// Draws scatter markers for a PointPlotter, offset by trial row when active. - /// + private double ToPlotTime(double timestamp) + { + return timestamp - latestTimestamp; + } + + private static Vector4 ToVec4(Color color) + { + return new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f); + } + + unsafe private void DrawShadedRect(ShadedAreaPlotter config, double tStart, double tEnd, int trialIndex) + { + double x0 = ToPlotTime(tStart); + double x1 = ToPlotTime(tEnd); + double yLow = HasTrialBreaks ? (double)trialIndex : 0.0; + double yHigh = HasTrialBreaks ? (double)(trialIndex + 1) : 1.0; + + var color = ToVec4(config.Color); + ImPlot.SetNextLineStyle(color, 0f); + ImPlot.SetNextFillStyle(color, config.Alpha); + + fixed (double* xs = new double[] { x0, x1 }) + fixed (double* ysL = new double[] { yLow, yLow }) + fixed (double* ysH = new double[] { yHigh, yHigh }) + { + ImPlot.PlotShaded(config.EventName, xs, ysL, ysH, 2); + } + } + + private int GetTrialIndex(double timestamp) + { + if (!HasTrialBreaks || trialBreaks.Count == 0) return 0; + for (int i = trialBreaks.Count - 1; i >= 0; i--) + { + if (timestamp >= trialBreaks[i]) + return i + 1; + } + return 0; + } + unsafe private void DrawPointMarkers(PointPlotter config, double plotTMin, double plotTMax) { List records; @@ -377,159 +407,153 @@ unsafe private void DrawPointMarkers(PointPlotter config, double plotTMin, doubl } } - /// - /// Sets up Y axis ticks with trial labels at row centers. - /// - unsafe private void SetupTrialAxisTicks(int firstVisibleTrial, int numVisibleTrials) + private List BuildMergedTimeline() { - if (numVisibleTrials <= 0) return; - - var positions = new double[numVisibleTrials]; - var labelData = new byte[numVisibleTrials][]; - - for (int t = 0; t < numVisibleTrials; t++) - { - int trialNum = firstVisibleTrial + t; - positions[t] = trialNum + 0.5; - labelData[t] = System.Text.Encoding.UTF8.GetBytes(trialNum.ToString() + '\0'); - } - - var handles = new GCHandle[numVisibleTrials]; - var ptrs = new IntPtr[numVisibleTrials]; + var merged = new List(); - try + foreach (var config in ShadedAreaPlotters) { - for (int t = 0; t < numVisibleTrials; t++) - { - handles[t] = GCHandle.Alloc(labelData[t], GCHandleType.Pinned); - ptrs[t] = handles[t].AddrOfPinnedObject(); - } + List records; + if (!eventHistory.TryGetValue(config.EventName, out records)) + continue; - fixed (double* posPtr = positions) - fixed (IntPtr* labelPtrs = ptrs) + for (int i = 0; i < records.Count; i++) { - ImPlot.SetupAxisTicks(ImAxis.Y1, posPtr, numVisibleTrials, (byte**)labelPtrs, false); + merged.Add(new ShadedSegment + { + Timestamp = records[i].Timestamp, + Config = config + }); } } - finally + + merged.Sort(delegate(ShadedSegment a, ShadedSegment b) { - for (int t = 0; t < numVisibleTrials; t++) - { - if (handles[t].IsAllocated) handles[t].Free(); - } - } + return a.Timestamp.CompareTo(b.Timestamp); + }); + + return merged; } - private void DrawEvents() + private struct EventRecord { - latestTimestamp = (DateTimeOffset.Now - startTime).TotalSeconds; + public double Timestamp; + } - ImGui.Text("Time Window (s):"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(InputWidth); - ImGui.InputFloat("##timewindow", ref timeWindow); - if (timeWindow < 1.0f) timeWindow = 1.0f; + private struct ShadedSegment + { + public double Timestamp; + public ShadedAreaPlotter Config; + } - var availableSize = ImGui.GetContentRegionAvail(); - float plotHeight = Math.Max(availableSize.Y, MinPlotHeight); + public interface IPlotter +{ + string EventName { get; set; } +} - double plotTMin = -(double)timeWindow; - double plotTMax = 0.0; +public class ShadedAreaPlotter : IPlotter +{ + private string _eventName; + private Color _color; + private float _alpha; - int firstVisible, numVisible; - GetVisibleTrialRange(out firstVisible, out numVisible); + public ShadedAreaPlotter() + { + _eventName = ""; + _color = Color.CornflowerBlue; + _alpha = 0.3f; + } - double yMin = HasTrialBreaks ? (double)firstVisible : YAxisMin; - double yMax = HasTrialBreaks ? (double)(firstVisible + numVisible) : YAxisMax; + [Description("The software event name to filter on.")] + public string EventName + { + get { return _eventName; } + set { _eventName = value; } + } - ImPlot.SetNextAxesLimits(plotTMin, plotTMax, yMin, yMax, ImPlotCond.Always); - if (ImPlot.BeginPlot("Software Events", new Vector2(-1, plotHeight), ImPlotFlags.NoTitle)) - { - ImPlot.SetupAxes("Time (s)", HasTrialBreaks ? "Trial" : "Value"); - ImPlot.SetupAxisLimits(ImAxis.Y1, yMin, yMax, ImPlotCond.Always); - ImPlot.SetupLegend(ImPlotLocation.North, ImPlotLegendFlags.Outside | ImPlotLegendFlags.Horizontal); + [XmlIgnore] + [Description("The color of the shaded area.")] + public Color Color + { + get { return _color; } + set { _color = value; } + } - if (HasTrialBreaks && numVisible > 0) - { - SetupTrialAxisTicks(firstVisible, numVisible); - } + [Browsable(false)] + [XmlElement("Color")] + public string ColorHtml + { + get { return ColorTranslator.ToHtml(Color); } + set { try { Color = ColorTranslator.FromHtml(value); } catch { } } + } - DrawAllShadedAreas(plotTMin, plotTMax); + [Description("The transparency of the shaded area (0.0 to 1.0).")] + public float Alpha + { + get { return _alpha; } + set { _alpha = value; } + } +} - foreach (var config in pointPlotters) - { - DrawPointMarkers(config, plotTMin, plotTMax); - } +public class PointPlotter : IPlotter +{ + private string _eventName; + private Color _color; + private float _yPosition; + private float _markerSize; + private ImPlotMarker _marker; - ImPlot.EndPlot(); - } + public PointPlotter() + { + _eventName = ""; + _color = Color.Red; + _yPosition = 0.5f; + _markerSize = 6.0f; + _marker = ImPlotMarker.Circle; } - /// - public override void Load(IServiceProvider provider) + [Description("The software event name to filter on.")] + public string EventName { - var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); - var builder = ExpressionBuilder.GetVisualizerElement(context.Source).Builder as SoftwareEventVisualizerBuilder; - if (builder != null) - { - fontSize = builder.FontSize; - timeWindow = builder.TimeWindow; - shadedAreaPlotters = builder.ShadedAreaPlotters ?? new List(); - pointPlotters = builder.PointPlotters ?? new List(); - trialBreakEventName = builder.TrialBreakEventName ?? ""; - maxTrials = builder.MaxTrials; - } - - if (startTime == default(DateTimeOffset)) - { - startTime = DateTimeOffset.Now; - } - - imGuiCanvas = new ImGuiControl(); - imGuiCanvas.Dock = DockStyle.Fill; - imGuiCanvas.Render += (sender, e) => - { - var dockspaceId = ImGui.DockSpaceOverViewport( - 0, - ImGui.GetMainViewport(), - ImGuiDockNodeFlags.AutoHideTabBar | ImGuiDockNodeFlags.NoUndocking); + get { return _eventName; } + set { _eventName = value; } + } - StyleColors(); - ImGui.PushFont(ImGui.GetFont(), fontSize); + [XmlIgnore] + [Description("The color of the marker.")] + public Color Color + { + get { return _color; } + set { _color = value; } + } - if (ImGui.Begin("SoftwareEventVisualizer")) - { - DrawEvents(); - } + [Browsable(false)] + [XmlElement("Color")] + public string ColorHtml + { + get { return ColorTranslator.ToHtml(Color); } + set { try { Color = ColorTranslator.FromHtml(value); } catch { } } + } - ImGui.End(); - ImGui.PopFont(); - var centralNode = ImGuiP.DockBuilderGetCentralNode(dockspaceId); - if (!ImGui.IsWindowDocked() && !centralNode.IsNull) - { - unsafe - { - var handle = centralNode.Handle; - uint dockId = handle->ID; - ImGuiP.DockBuilderDockWindow("SoftwareEventVisualizer", dockId); - } - } - }; + [Description("The fixed Y position of the marker (0.0 to 1.0).")] + public float YPosition + { + get { return _yPosition; } + set { _yPosition = value; } + } - var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); - if (visualizerService != null) - { - visualizerService.AddControl(imGuiCanvas); - } + [Description("The size of the marker in pixels.")] + public float MarkerSize + { + get { return _markerSize; } + set { _markerSize = value; } } - /// - public override void Unload() + [Description("The marker style.")] + public ImPlotMarker Marker { - if (imGuiCanvas != null) - { - imGuiCanvas.Dispose(); - imGuiCanvas = null; - } + get { return _marker; } + set { _marker = value; } } } +} \ No newline at end of file diff --git a/src/Extensions/SoftwareEventVisualizerBuilder.cs b/src/Extensions/SoftwareEventVisualizerBuilder.cs deleted file mode 100644 index f4bc55f8..00000000 --- a/src/Extensions/SoftwareEventVisualizerBuilder.cs +++ /dev/null @@ -1,177 +0,0 @@ -using Bonsai; -using Bonsai.Expressions; -using AllenNeuralDynamics.AindBehaviorServices.DataTypes; -using Hexa.NET.ImPlot; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Linq.Expressions; -using System.Xml.Serialization; - -public interface IPlotter -{ - string EventName { get; set; } -} - -public class ShadedAreaPlotter : IPlotter -{ - private string _eventName; - private Color _color; - private float _alpha; - - public ShadedAreaPlotter() - { - _eventName = ""; - _color = Color.CornflowerBlue; - _alpha = 0.3f; - } - - [Description("The software event name to filter on.")] - public string EventName - { - get { return _eventName; } - set { _eventName = value; } - } - - [XmlIgnore] - [Description("The color of the shaded area.")] - public Color Color - { - get { return _color; } - set { _color = value; } - } - - [Browsable(false)] - [XmlElement("Color")] - public string ColorHtml - { - get { return ColorTranslator.ToHtml(Color); } - set { try { Color = ColorTranslator.FromHtml(value); } catch { } } - } - - [Description("The transparency of the shaded area (0.0 to 1.0).")] - public float Alpha - { - get { return _alpha; } - set { _alpha = value; } - } -} - -public class PointPlotter : IPlotter -{ - private string _eventName; - private Color _color; - private float _yPosition; - private float _markerSize; - private ImPlotMarker _marker; - - public PointPlotter() - { - _eventName = ""; - _color = Color.Red; - _yPosition = 0.5f; - _markerSize = 6.0f; - _marker = ImPlotMarker.Circle; - } - - [Description("The software event name to filter on.")] - public string EventName - { - get { return _eventName; } - set { _eventName = value; } - } - - [XmlIgnore] - [Description("The color of the marker.")] - public Color Color - { - get { return _color; } - set { _color = value; } - } - - [Browsable(false)] - [XmlElement("Color")] - public string ColorHtml - { - get { return ColorTranslator.ToHtml(Color); } - set { try { Color = ColorTranslator.FromHtml(value); } catch { } } - } - - [Description("The fixed Y position of the marker (0.0 to 1.0).")] - public float YPosition - { - get { return _yPosition; } - set { _yPosition = value; } - } - - [Description("The size of the marker in pixels.")] - public float MarkerSize - { - get { return _markerSize; } - set { _markerSize = value; } - } - - [Description("The marker style.")] - public ImPlotMarker Marker - { - get { return _marker; } - set { _marker = value; } - } -} - -[TypeVisualizer(typeof(SoftwareEventVisualizer))] -[WorkflowElementCategory(ElementCategory.Combinator)] -[Description("Visualizes software events as shaded areas and/or point markers over time.")] -public class SoftwareEventVisualizerBuilder : SingleArgumentExpressionBuilder -{ - private float _fontSize; - private float _timeWindow; - - public SoftwareEventVisualizerBuilder() - { - _fontSize = 16.0f; - _timeWindow = 30.0f; - ShadedAreaPlotters = new List(); - PointPlotters = new List(); - } - - [Description("Font size for text rendering.")] - public float FontSize - { - get { return _fontSize; } - set { _fontSize = value; } - } - - [Description("Number of seconds to display on the X axis.")] - public float TimeWindow - { - get { return _timeWindow; } - set { _timeWindow = value; } - } - - [Description("Shaded area plotter configurations.")] - public List ShadedAreaPlotters { get; set; } - - [Description("Point plotter configurations.")] - public List PointPlotters { get; set; } - - [Description("Software event name that triggers a new trial row. Leave empty to disable trial breaks.")] - public string TrialBreakEventName { get; set; } - - [Description("Maximum number of trial rows to display. When exceeded, only the last N trials are shown. 0 = show all.")] - public int MaxTrials { get; set; } - - /// - public override Expression Build(IEnumerable arguments) - { - var source = arguments.First(); - return Expression.Call(typeof(SoftwareEventVisualizerBuilder), "Process", null, source); - } - - static IObservable Process(IObservable source) - { - return source; - } -} diff --git a/src/Extensions/TableBuilder.cs b/src/Extensions/TableBuilder.cs new file mode 100644 index 00000000..fc69c3cb --- /dev/null +++ b/src/Extensions/TableBuilder.cs @@ -0,0 +1,97 @@ +using Bonsai; +using Bonsai.ImGui; +using Hexa.NET.ImGui; +using Markdig.Extensions.Tables; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; +using System.Reactive; +using System.Reactive.Linq; + +using ImGui = Hexa.NET.ImGui.ImGui; + +/// +/// Represents an operator that begins drawing a table and provides +/// a sequence of notifications for drawing table contents. +/// +[WorkflowElementIcon("Bonsai:ElementIcon.Visualizer")] +[Description("Begins drawing a table and provides a sequence of notifications for drawing table contents.")] +public class TableBuilder : ControlBuilder +{ + // /// + // /// Gets or sets the number of columns in the table. + // /// + // [Description("The number of columns in the table.")] + // public int Columns { get; set; } = 1; + + public List Columns {get; set;} + + /// + /// Gets or sets the table configuration flags. + /// + [Description("The table configuration flags.")] + public ImGuiTableFlags Flags { get; set; } + + /// + /// Gets or sets the explicit outer size of the table. + /// + /// + /// The meaning of this property depends on the table configuration flags. + /// + [TypeConverter(typeof(NumericRecordConverter))] + [Description("The explicit outer size of the table.")] + public Vector2 OuterSize { get; set; } + + /// + /// Gets or sets the optional total width to layout columns for horizontal scrolling. + /// + /// + /// This value is ignored when is disabled. + /// + [Description("The optional total width to layout columns for horizontal scrolling.")] + public float InnerWidth { get; set; } + + public bool ShowHeadersRow {get; set; } + + public TableBuilder() + { + Columns = new List(); + } + + /// + protected override IObservable Generate(IObservable source) + { + return Observable.Create(observer => + { + var label = "##" + Name; + var sourceObserver = Observer.Create( + _ => + { + if (Visible && ImGui.BeginTable(label, Columns.Count, Flags, OuterSize, InnerWidth)) + { + foreach (var col in Columns) + { + ImGui.TableSetupColumn(col.Name, col.Flags, col.InitValue); + } + if (ShowHeadersRow) + ImGui.TableHeadersRow(); + + observer.OnNext(label); + ImGui.EndTable(); + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} + +// [Serializable] +public class ColumnStyling +{ + public string Name {get; set;} + public ImGuiTableColumnFlags Flags {get; set;} + public float InitValue {get; set;} +} \ No newline at end of file diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai index 9e788743..0a70dd97 100644 --- a/src/Extensions/TaskEngine.bonsai +++ b/src/Extensions/TaskEngine.bonsai @@ -19,6 +19,14 @@ ThisTrial + + + 0 + + + + TrialIndex + ThisTrial @@ -433,7 +441,7 @@ - QuiscentPeriod + QuiescentPeriod @@ -475,7 +483,13 @@ - + + + + + + it < 0 ? 0 : it + it < 0 ? 0 : it @@ -487,7 +501,7 @@ - PT0S + PT2S @@ -499,7 +513,7 @@ - + @@ -507,10 +521,11 @@ - - + + + @@ -524,6 +539,22 @@ QuickRetractSettings + + EnableDuringQuiescence + + + + + + Source1 + + + + + + + + @@ -565,10 +596,12 @@ - + - + + + @@ -592,7 +625,7 @@ - PT0S + PT1S @@ -1243,7 +1276,7 @@ - PT0S + PT3S @@ -1378,7 +1411,7 @@ - PT5S + PT8.824S @@ -1629,6 +1662,20 @@ SoftwareEvent + + TrialIndex + + + + 1 + + + + it + 1 + + + TrialIndex + @@ -1667,6 +1714,9 @@ + + + @@ -1713,30 +1763,31 @@ - + - - + - + - + - + - + - + - + + - + + diff --git a/src/Extensions/ToggleButtonBuilder.cs b/src/Extensions/ToggleButtonBuilder.cs new file mode 100644 index 00000000..464f2ca9 --- /dev/null +++ b/src/Extensions/ToggleButtonBuilder.cs @@ -0,0 +1,54 @@ +using System; +using System.ComponentModel; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai.ImGui; +using ImGui = Hexa.NET.ImGui.ImGui; + +/// +/// Represents an operator that draws a button control and generates +/// a sequence of notifications whenever the button is clicked. +/// +[Description("Draws a button control and generates a sequence of notifications whenever the button is clicked.")] +public class ToggleButtonBuilder : TextControlBuilder +{ + public bool Toggled {get; set;} + + public string ToggledOffText {get; set;} + public string ToggledOnText {get; set;} + public bool Enabled {get; set;} + + /// + protected override IObservable Generate(IObservable source) + { + Toggled = false; + return Observable.Create(observer => + { + observer.OnNext(Toggled); + var sourceObserver = Observer.Create( + _ => + { + if (Visible) + { + var label = Toggled ? ToggledOnText : ToggledOffText; + if (Enabled) + { + if (ImGui.Button(label)) + { + observer.OnNext(!Toggled); + Toggled = !Toggled; + } + } else + { + ImGui.BeginDisabled(); + ImGui.Button(label); + ImGui.EndDisabled(); + } + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + }); + } +} \ No newline at end of file diff --git a/src/Extensions/TrialOutcomeVisualizer.cs b/src/Extensions/TrialOutcomeVisualizer.cs index fef88151..4f52cdc2 100644 --- a/src/Extensions/TrialOutcomeVisualizer.cs +++ b/src/Extensions/TrialOutcomeVisualizer.cs @@ -574,31 +574,31 @@ void DrawTrials() // Align 0.5 to 0, 1 to 1 and 0 to -1 ImPlot.SetupAxisLimits(ImAxis.Y2, -y2Max, y2Max, ImPlotCond.Always); ImPlot.SetAxes(ImAxis.X1, ImAxis.Y2); - PlotProbabilityBars(trialIndices, pRewardRight, 1, RightChoiceColor, "P(Reward Right)"); - PlotProbabilityBars(trialIndices, pRewardLeft, -1, LeftChoiceColor, "P(Reward Left)"); + // PlotProbabilityBars(trialIndices, pRewardRight, 1, RightChoiceColor, "P(Reward Right)"); + // PlotProbabilityBars(trialIndices, pRewardLeft, -1, LeftChoiceColor, "P(Reward Left)"); // Switch back to Y1 for everything else ImPlot.SetAxes(ImAxis.X1, ImAxis.Y1); PlotRollingLine(trialIndices, rollingChoiceAvg, "Right Choice Rate", ProbabilityTraceColor); - PlotRollingLine(trialIndices, rollingRewardRate, "Reward Rate", RewardRateLineColor); - PlotRollingLine(trialIndices, rollingFinishedRate, "Finished Trial Rate", FinishedTrialRateColor); + // PlotRollingLine(trialIndices, rollingRewardRate, "Reward Rate", RewardRateLineColor); + // PlotRollingLine(trialIndices, rollingFinishedRate, "Finished Trial Rate", FinishedTrialRateColor); // Right choices: bars extend upward from Y=1 (past 1) PlotRewardedBars(rightRewardedX, RightChoiceY, 1, RightChoiceColor, "Right + Rewarded"); PlotUnrewardedBars(rightUnrewardedX, RightChoiceY, 1, RightChoiceColor, "Right + Unrewarded"); - // Left choices: bars extend downward from Y=0 (past 0 into negative) - PlotRewardedBars(leftRewardedX, LeftChoiceY, -1, LeftChoiceColor, "Left + Rewarded"); - PlotUnrewardedBars(leftUnrewardedX, LeftChoiceY, -1, LeftChoiceColor, "Left + Unrewarded"); + // // Left choices: bars extend downward from Y=0 (past 0 into negative) + // PlotRewardedBars(leftRewardedX, LeftChoiceY, -1, LeftChoiceColor, "Left + Rewarded"); + // PlotUnrewardedBars(leftUnrewardedX, LeftChoiceY, -1, LeftChoiceColor, "Left + Unrewarded"); - // Auto-response trials with purple outline - PlotRewardedBarsWithOutline(rightRewardedAutoX, RightChoiceY, 1, RightChoiceColor, "Right + Rewarded (Auto)"); - PlotUnrewardedBarsWithOutline(rightUnrewardedAutoX, RightChoiceY, 1, RightChoiceColor, "Right + Unrewarded (Auto)"); - PlotRewardedBarsWithOutline(leftRewardedAutoX, LeftChoiceY, -1, LeftChoiceColor, "Left + Rewarded (Auto)"); - PlotUnrewardedBarsWithOutline(leftUnrewardedAutoX, LeftChoiceY, -1, LeftChoiceColor, "Left + Unrewarded (Auto)"); + // // Auto-response trials with purple outline + // PlotRewardedBarsWithOutline(rightRewardedAutoX, RightChoiceY, 1, RightChoiceColor, "Right + Rewarded (Auto)"); + // PlotUnrewardedBarsWithOutline(rightUnrewardedAutoX, RightChoiceY, 1, RightChoiceColor, "Right + Unrewarded (Auto)"); + // PlotRewardedBarsWithOutline(leftRewardedAutoX, LeftChoiceY, -1, LeftChoiceColor, "Left + Rewarded (Auto)"); + // PlotUnrewardedBarsWithOutline(leftUnrewardedAutoX, LeftChoiceY, -1, LeftChoiceColor, "Left + Unrewarded (Auto)"); - // No choice: cross marker at middle + // // No choice: cross marker at middle PlotNoChoiceMarkers(noChoiceX, NoChoiceColor, "No Choice"); ImPlot.EndPlot(); diff --git a/src/Extensions/Visualizers.bonsai b/src/Extensions/Visualizers.bonsai index b912aca2..d6471288 100644 --- a/src/Extensions/Visualizers.bonsai +++ b/src/Extensions/Visualizers.bonsai @@ -1,94 +1,1256 @@  - - - 0 - + + ImGuiContext + + + + + true + 1 + None + + 0 + 0 + + 0 + + + Frame + + + + true + + + + + + + + + + + + + + + DataTransformations + + + + GlobalTrialOutcome + + + HarpTimestampSource + + + TimedTrial + + + GlobalTrialMetrics + + + Bias + + + TrialIndex + + + + + + new (it.Item1 as Bias, it.Item2 as TrialIndex) + + + TrialIndexedBias + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + HarpTimestampSource + + + LickRight + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + + + HarpTimestampSource + + + LickLeft + + + GlobalTrial + + + IsAutoRewardRight + + + + + + + Source1 + + + Value + + + + + + + + + + + + + HarpTimestampSource + + + AutoRewardLeft + + + + + + Source1 + + + Value + + + + + + + + + + + HarpTimestampSource + + + AutoRewardRight + + + GlobalTrial + + + LickspoutOffsetDelta + + + NotZero + + + + Source1 + + + + 0 + + + + + + + + + + + + + + MoveRight + + + + Source1 + + + + 0 + + + + + + + + + + + + HarpTimestampSource + + + LeftBiasCorrection + + + MoveLeft + + + + Source1 + + + + false + + + + + + + + + + + + HarpTimestampSource + + + RightBiasCorrection + + + SoftwareEvent + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + + LickRight + + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + + + + LickLeft + + + + GiveRewardRight + + + + + + Source1 + + + + + + + + + + + WaterRight + + + + GiveRewardRight + + + + + + + Source1 + + + + + + + + + + + WaterLeft + + + + + + + HarpTimestampSource + + + + + + + + + + + + EthogramEvents + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + OutputControl - TrialPlots + Manipulator + + + + UiManipulatorLeft + + + MoveRelative + + + + Source1 + + + + -1 + 0 + 0 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorRight + + + MoveRelative + + + + Source1 + + + + 1 + 0 + 0 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorForwardLeft + + + MoveRelative + + + + Source1 + + + + 0 + 1 + 0 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorForwardRight + + + MoveRelative + + + + Source1 + + + + 0 + 0 + 1 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorBackLeft + + + MoveRelative + + + + Source1 + + + + 0 + -1 + 0 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorBackRight + + + MoveRelative + + + + Source1 + + + + 0 + 0 + -1 + 0 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorUp + + + MoveRelative + + + + Source1 + + + + 0 + 0 + 0 + 1 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorDown + + + MoveRelative + + + + Source1 + + + + 0 + 0 + 0 + -1 + + + + OffsetGain + + + + + + + MoveBy + + + + + + + + + + + + + + + UiManipulatorHome + + + Home + + + + Source1 + + + + HomeMotors + + + + + + + + + + + + UiManipulatorStop + + + StopMotors + + + + Source1 + + + + StopMotors + + + + + + + + + + + + UiManipulatorInitialize + + + GoToInitialPosition + + + + Source1 + + + + GoToInitialPosition + + + + + + + + + + + + OffsetGain + + + + + + + + + + + + + + + + + + + UiHabituationTimeUpdate + + + + 0.05 + + + + + 1 + + + + BumpSize + + + UiMoveLeft + + + BumpLeft + + + + Source1 + + + BumpSize + + + + + + Item2 + + + + -1 + + + + ManualSpoutDelta + + + + + + + + + + + + + + + UiMoveRight + + + BumpRight + + + + Source1 + + + BumpSize + + + + + + Item2 + + + ManualSpoutDelta + + + + + + + + + + + + + + UiGiveRight + + + TriggerRight + + + + Source1 + + + + true + + + + GiveManualWaterRight + + + + + + + + + + + + UiGiveLeft + + + TriggerLeft + + + + Source1 + + + + false + + + + GiveManualWaterRight + + + + + + + + + + + + UiCueGiveLeft + + + UpdateGlobalState + + Source1 + - GlobalTrialOutcome + GlobalAutoWaterState - - 16 + + - - - SoftwareEvent + + Item2 + + + + + + it.SetLeft() + + + GlobalAutoWaterState + + + + + + + + + + + + + + + + UiCueGiveRight + + + UpdateGlobalState + + + + Source1 - IsRightLickEvent + GlobalAutoWaterState - - - - - Source1 - - - Value - - - - - - - - + + + + + Item2 + + + + + + it.SetRight() + + + GlobalAutoWaterState + + + + + + + + + + + + + + + + + true + + + + IsFlushingValveLeft + + + GiveManualWaterRight + + + GiveManualWaterRight + + + GiveRewardRight + + + IsFlushingValveLeft + + + IsFlushingValveRight + + + UiKeepLeftOpen + + + UiKeepBothOpen + + + + + + KeepLeftOpen + + + + + 50 + - - LickRight + + 1 - - IsRightLickEvent + + DefaultRewardAmount + + Source1 + + Source1 - - Value - - - - - - LickLeft + + PT0.1S + + + + + + + + false + DefaultRewardAmount + + + + + + SetIsRightRewardAmount + + + + PT0.02S + + + + Item1 + + GiveRewardRight + + + PT0.5S + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UiKeepRightOpen + + + + + + KeepRightOpen + + + + + 50 + + + + + 1 + + + + DefaultRewardAmount + + + Source1 + + @@ -103,13 +1265,114 @@ - - WaterRight + + PT0.1S + + + + + + + + true + DefaultRewardAmount + + + + + + SetIsRightRewardAmount + + + + PT0.02S + + + + Item1 + + GiveRewardRight + + + PT0.5S + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UiDrainLeft + + + UiDrainBoth + + + + + + FlushLeft + + + + + 50 + + + + + 1 + + + + DefaultVolume + + + Source1 + @@ -125,914 +1388,721 @@ - - WaterLeft + + PT0.1S + + + + + + + + false + + + + DefaultVolume + + + + + + SetIsRightRewardAmount + + + + PT0.02S + + + + Item1 + + + GiveRewardRight + + + DefaultVolume + + + RigSchema + + + Calibration.WaterValveLeft + + + + + + RewardToTime + ((Item1 * 0.001) - Item2.Offset) / Item2.Slope + + + + + + + + + + + + PT1.288S + + + + DefaultVolume + + + DivideByEachOpen + Math.Ceiling(5000 / it) + + + + + + + + + 100 + + + + + + + + false + + IsFlushingValveLeft + - - 16 - 20 - - - QuiscentPeriod - Gold - 0.3 - - - ResponsePeriod - SandyBrown - 0.3 - - - RewardConsumptionPeriod - RosyBrown - 0.3 - - - ItiPeriod - Green - 0.3 - - - - - LickLeft - Red - 0.9 - 6 - Down - - - LickRight - Blue - 0.1 - 6 - Up - - - IsRightTriggerQuickRetract - DarkCyan - 0.5 - 6 - Plus - - - DeliverSecondaryReinforcer - Sienna - 0.5 - 6 - Plus - - - WaterRight - Blue - 0.2 - 6 - Circle - - - WaterLeft - Red - 0.8 - 6 - Circle - - - - 5 - - - - true - true - 1 - 2 - - - + + + 1 + - - + - - - - - + + + + + - + - - - - + + + + - + + + + + + + + + + + + - - - ManualOverrides + + + + + DrainLeftComplete + + + UiDrainRight + + + + + + FlushRight - - OffsetControl - - - - - 0.05 - - - - - 1 - - - - BumpSize - - - ManipulatorBiasTracker - - - {0:F2} - - - - - - - - true - true - Microsoft Sans Serif, 20.25pt - 0.00 - - - - Left - true - true - Microsoft Sans Serif, 15.75pt - ◀️ - - - BumpLeft - - - - Source1 - - - BumpSize - - - - - - Item2 - - - - -1 - - - - ManualSpoutDelta - - - - - - - - - - - - - - - - Right - true - true - Microsoft Sans Serif, 15.75pt - - - - BumpRight - - - - Source1 - - - BumpSize - - - - - - Item2 - - - ManualSpoutDelta - - - - - - - - - - - - - - - true - true - 2 - 1 - - - - - - - ExperimentState - - - - - - - - false - true - Microsoft Sans Serif, 22.125pt - 1 - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + 50 + - - - Spout offset - true - true - Microsoft Sans Serif, 15.75pt - Spout offset - - - - GiveWaterUI - - - - Left - true - true - 💧◀ - - - TriggerLeft - - - - Source1 - - - - false - - - - GiveManualWaterRight - - - - - - - - - - - - - Right - true - true - 💧▶ - - - TriggerRight - - - - Source1 - - - - true - - - - GiveManualWaterRight - - - - - - - - - - - - - ManualWater - true - true - 2 - 1 - - - Percent - 0.5 - - - Percent - 0.5 - - - - - - - - GiveManualWaterRight - - - GiveManualWaterRight - - - GiveRewardRight - - - - - - - - - - - - - - + + + 1 + + + + DefaultVolume + + + Source1 - - - 💧 - true - true - Microsoft Sans Serif, 8.25pt - - - - - ForceAutoWater + + - - GlobalAutoWaterState - - - !it.IsLeft - - - - - - - - SetLeft - true - true - 🎣◀ - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.SetLeft() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - GlobalAutoWaterState - - - !it.IsRight - - - - - - - - SetRight - true - true - 🎣▶ - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.SetRight() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - GlobalAutoWaterState - - - it.HasValue - - - - - - - - Reset - false - true - - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.Reset() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - ForceAutoWater - true - true - 3 - 1 - - - + + Source1 - - - - - - - - - - - - - - - - - - - - - 🎣 - true - true - Microsoft Sans Serif, 8.25pt - - - - - true - true - Microsoft Sans Serif, 15.75pt - - - - Manual Control - true - true - Microsoft Sans Serif, 36pt - 1 - 2 - - - + + + PT0.1S + + + + - - - - - - - - - - - - - - - - - - - - - - - - LauncherControl - - - + + true + - - ExperimentState + DefaultVolume + + + + + + SetIsRightRewardAmount + + + + PT0.02S + + + + Item1 + + + GiveRewardRight + + + DefaultVolume + + + RigSchema + + + Calibration.WaterValveRight + + + + + + RewardToTime + ((Item1 * 0.001) - Item2.Offset) / Item2.Slope + + + - - + - - Start - true - true - Start Experiment - - - StartExperimentToggleButton - - - - End - true - true - End Experiment - - - EndExperimentButton - - - - LauncherControl - true - true - Microsoft Sans Serif, 26pt - 2 - 4 - - - - - - - + + + PT0.888S + - - StartExperimentToggleButton + DefaultVolume + + + DivideByEachOpen + Math.Ceiling(5000 / it) + + + + + - - 1 + + 100 - - - StartExperimentShortcut - - - EndExperimentButton + + - - 1 + + false - - EndExperiment + IsFlushingValveRight - - StartExperiment + + - + + 1 + + - - + - - - - - - + + + + + + + - - - + + + + - + + + + + + + + + + + - - - - - true - true - - - - - true - true - 1 - 2 - - - - - - - - true - true - 3 - 1 - - - - - - - - - TriggeredCamerasStream - - - - - - - - - - - true - true - 2 - 1 - - - - - - - true - true - 1 - 2 - - - Percent - 0.75 - - - Percent - 0.25 - - - - - - + + + + + DrainRightComplete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VisualizerStructure + + + + + - GlobalTrial + Frame - - 2 - 16 + + true - - - true - true + + true + + + Ethograph + WidthStretch + 0 + + + Camera + WidthFixed + 320 + + + BordersInnerH BordersOuterH BordersInnerV BordersOuterV BordersH BordersV BordersInner BordersOuter Borders SizingFixedFit + + 0 + 0 + + 0 + true + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + Behavior + + + true + + + Bias + + + true + + + Ethogram + + + + + + true + + + Camera + + + + + + true + + + true + 1 + Borders + + 0 + 0 + + 0 + + + Metadata + + + true + + + true + None + + + PreSession + true + pre-session + None + + + PreSession + + + IntraSession + true + intra-session + None + + + IntraSession + + + - - + + + - + + + + - + - + - - - - - - + + + + + - + - + + - + - - - + + + + + + Behavior1 + LickLeft + LickRight + TimedTrial + Behavior + AutoRewardRight + AutoRewardLeft + + + Bias + TrialIndexedBias + LeftBiasCorrection + RightBiasCorrection + + + PLACEHOLDER + + + + SessionInfo + + + + TimedTrial + + + Value.Trial.Metadata.Extra + + + SessionMetadata + + + + + + + - - DynamicForaging - true - - - Maximized + + Ethogram + + + EthogramEvents + + + + + + + + + true + + Break + 200 + + + QuiescentPeriod + Gold + 0.3 + + + ResponsePeriod + SandyBrown + 0.3 + + + RewardConsumptionPeriod + RosyBrown + 0.3 + + + ItiPeriod + Green + 0.3 + + + + + LickLeft + Red + 0.9 + 6 + Down + + + LickRight + Blue + 0.1 + 6 + Up + + + IsRightTriggerQuickRetract + DarkCyan + 0.5 + 6 + Plus + + + DeliverSecondaryReinforcer + Sienna + 0.5 + 6 + Plus + + + WaterRight + Red + 0.2 + 6 + Circle + + + WaterLeft + Blue + 0.8 + 6 + Circle + + + + + + + Camera + BodyCamera + 0.1 + 0.9 + 2 + 0 + TriggeredCamerasStream + + + Camera + BodyCamera + 0.1 + 0.9 + 2 + 0 + TriggeredCamerasStream + + + SessionSchema + + + TaskLogicSchema + + + + + + Metadata + StartExperimentShortcut + UiHabituationTimeUpdate + + + PreSession + UiKeepLeftOpen + UiKeepRightOpen + UiKeepBothOpen + UiDrainLeft + UiDrainRight + UiDrainBoth + UiManipulatorLeft + UiManipulatorForwardLeft + UiManipulatorForwardRight + UiManipulatorUp + UiManipulatorRight + UiManipulatorBackLeft + UiManipulatorBackRight + UiManipulatorDown + UiManipulatorStop + UiManipulatorHome + UiManipulatorInitialize + DrainLeftComplete + DrainRightComplete + + + IntraSession + UiGiveLeft + UiGiveRight + UiMoveLeft + UiMoveRight + StartExperiment + UiCueGiveLeft + UiCueGiveRight + SessionMetadata - - + + + + + + + + + \ No newline at end of file diff --git a/src/main.bonsai b/src/main.bonsai index 18d47310..df46f347 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -7,8 +7,8 @@ xmlns:p2="clr-namespace:;assembly=Extensions" xmlns:p3="clr-namespace:System.Reactive;assembly=System.Reactive.Core" xmlns:p4="clr-namespace:AllenNeuralDynamics.AindBehaviorServices.DataTypes;assembly=AllenNeuralDynamics.AindBehaviorServices" - xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:num="clr-namespace:Bonsai.Numerics;assembly=Bonsai.Numerics" + xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions" xmlns="https://bonsai-rx.org/2018/workflow"> @@ -19,9 +19,9 @@ - C:\git\Aind.Behavior.DynamicForaging\local\DynamicForaging_AindDynamicForagingTaskLogic.json - c:\git\Aind.Behavior.DynamicForaging\local\AindDynamicForagingRig.json - c:\git\Aind.Behavior.DynamicForaging\local\Session.json + C:\git\Aind.Behavior.DynamicForaging\local\AindDynamicForagingTaskLogic.json + C:\git\Aind.Behavior.DynamicForaging\local\AindDynamicForagingRig.json + C:\git\Aind.Behavior.DynamicForaging\local\Session.json LoadSchemas @@ -114,6 +114,19 @@ IsWaveformUploadDone + + + false + + + + + 1 + + + + IsRetracted + @@ -152,12 +165,6 @@ GlobalTrial - - SetIsRightRewardAmount - - - GlobalTrialMetrics - TaskLogicParameters @@ -180,6 +187,15 @@ RngObject + + SetIsRightRewardAmount + + + GlobalTrialMetrics + + + TrialIndex + false @@ -301,34 +317,36 @@ - + + - - - - - - + + + - - + + + - - + + - - + + - - - - - - - + + + + + + + + + + diff --git a/src/test_ethogram.bonsai b/src/test_ethogram.bonsai deleted file mode 100644 index 5c71d1eb..00000000 --- a/src/test_ethogram.bonsai +++ /dev/null @@ -1,392 +0,0 @@ - - - - - - - PT1S - - - - - - - - 0 - 3 - - - - - - - - - - - - - - - - PT2.024S - - - - - 0 - - - - - ResponsePeriod - - - - SoftwareEvent - - - - - - - - - - - - - - - - - - - - - 0 - 3 - - - - - - - - - - - - - - - - PT2.085S - - - - - 0 - - - - - RewardPeriod - - - - SoftwareEvent - - - - - - - - - - - - - - - - - - PT2S - - - - - - false - - - - - TrialOutcome - - - - SoftwareEvent - - - - - - - 0 - 3 - - - - - - - - - - - - - - - - PT2.207S - - - - - 0 - - - - - ITI - - - - SoftwareEvent - - - - - - - - - - - - - - - - - - PT0S - - - - - - - SoftwareEvent - - - SoftwareEvent - - - - 0 - 5 - - - - - - Break - - - - - - - - PT0S - PT0.05S - - - - - 0 - 1 - 0 - 0.1 - Linear - - - - Tick - - - - - - - - - - - - 16 - 10 - - - TrialOutcome - CornflowerBlue - 0.3 - - - RewardPeriod - Crimson - 0.3 - - - ITI - Cyan - 0.3 - - - - - TrialOutcome - SaddleBrown - 0.5 - 6 - Circle - - - LickLeft - Red - 0.8 - 6 - Down - - - LickRight - DodgerBlue - 0.2 - 6 - Up - - - Break - 5 - - - - 640 - 480 - On - false - Black - DepthBufferBit ColorBufferBit - true - - Resizable - Normal - Primary - 60 - - - - - 8 - 8 - 8 - 8 - - 16 - 0 - 0 - - 0 - 0 - 0 - 0 - - 2 - false - - - - - - A - - - - - - LickLeft - - - - SoftwareEvent - - - - S - - - - - - LickRight - - - - SoftwareEvent - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test_saturation_visualizer.bonsai b/src/test_saturation_visualizer.bonsai new file mode 100644 index 00000000..5ffcd588 --- /dev/null +++ b/src/test_saturation_visualizer.bonsai @@ -0,0 +1,157 @@ + + + + + + + Frame + + + + PT0S + PT0.01S + + + + + 0.01 + + + + TimeBase + + + + + 25224819 + Default + + + + CameraFeed + + + EndExperiment + + + Camera1 + + + + CameraFeed + + + TimeBase + + + + + + + + + + + + + + + + + + + + + + + Camera1 + + + + + + + Camera2 + + + + CameraFeed + + + TimeBase + + + + + + + + + + + + + + + + + + + + + + + Camera2 + + + + + + + + + + Item2 + Item1 + + + TriggeredCamerasStream + + + + + Frame + Camera1 + 0.1 + 0.3 + 2 + 0 + TriggeredCamerasStream + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test_shader_pass.bonsai b/src/test_shader_pass.bonsai new file mode 100644 index 00000000..43f97e59 --- /dev/null +++ b/src/test_shader_pass.bonsai @@ -0,0 +1,88 @@ + + + + + + + Frame + + + Frame + + + + + Default + + + + Image + + + Frame + + + + Rgba + Repeat + Repeat + Linear + Linear + + + + + 0.5 + 0.2 + 0.8 + + + + + + + + + true + + 640 + 480 + + + 0 + 0 + + + 1 + 1 + + + + + + + Interval.TotalSeconds + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test_visualizer.bonsai b/src/test_visualizer.bonsai index 252f14a5..80d9447b 100644 --- a/src/test_visualizer.bonsai +++ b/src/test_visualizer.bonsai @@ -1,316 +1,3029 @@  - - - 0 - 1 - - - - - 500 - - - - - 0.5 - - - - - 0 - 500 - - - - - 0 - 1 - 0 - 0.05 - Linear - - - - - 3.14 - - - - - - - (it +1) * 0.5 - - - - 0 - 1 - - - - - 500 - - - - - 0.5 - - - - it - - - - - - - - - true - - - - - 0 - 1 - - - - - 500 - - - - - 0.9 - - - - - - - - - - - - Item3 ? Item2 : Item1 - - - - 0 - 500 - - - - - 0 - 1 - 0 - 0.05 - Linear - - - - - - - (it +1) * 0.5 - - - - 0 - 1 - - - - - 500 - - - - - 0.5 - - - - - - - - - - - - - - - - - - - - 0 - 1 - - - - - 500 - - - - - 0.5 - - - - - - - - - - true - - - - - - - - - - 0 - 1 - - - - - 500 - - - - - 0.05 - - - - - - - Item3 ? Item1 : Item2 - - - - - - Item2 - - - - - - - - - 0.59164587077011477 - 0.40913708100521279 - 5 - 0 - 5 - false - 0.5 - 5 - true - 0 - - - - - - - - - - true - false - + + CameraData + + + + + 0 + + + + + CameraFeed + + + + Camera1 + + + + CameraFeed + + + TimeBase + + + + + + + + + + + + + + + + + + + + + + + Camera1 + + + + + + + Camera2 + + + + CameraFeed + + + TimeBase + + + + + + + + + + + + + + + + + + + + + + + Camera2 + + + + + + + + + + Item2 + Item1 + + + TriggeredCamerasStream + + + + + + + + + + + + + + + + + + - - 32 + + TrialData + + + + + A + false + + + + + TimeBase + + + LickLeft + + + + false + + + + + 1 + + + + IsRetracted + + + TimedTrial + + + Value.Trial.IsAutoRewardRight + + + + + + + Source1 + + + Value + + + + + + + + + + + + + Seconds + + + + + + + + + AutoRewardLeft + + + + + + Source1 + + + Value + + + + + + + + + + + + + + + + + AutoRewardRight + + + TimedTrial + + + Value.Trial.LickspoutOffsetDelta + + + NotZero + + + + Source1 + + + + 0 + + + + + + + + + + + + + + MoveRight + + + + Source1 + + + + 0 + + + + + + + + + + + + Seconds + + + + + + + + + LeftBiasCorrection + + + MoveLeft + + + + Source1 + + + + false + + + + + + + + + + + + + + + + + + RightBiasCorrection + + + + 0 + 0 + 0 + 0 + + + + ManipulatorPosition + + + + 0 + + + + OffsetGain + + + + PT1S + + + + + + + + 0 + 3 + + + + + + + + + + + + + + + + PT1.756S + + + + + 0 + + + + + ResponsePeriod + + + + SoftwareEvent + + + + + + + + + + + + + + + + + + + + + 0 + 3 + + + + + + + + + + + + + + + + PT1.826S + + + + + 0 + + + + + RewardPeriod + + + + SoftwareEvent + + + + + + + + + + + + + + + + + + PT2S + + + + + + false + + + + + TrialOutcome + + + + SoftwareEvent + + + + + + + 0 + 3 + + + + + + + + + + + + + + + + PT0.735S + + + + + 0 + + + + + ITI + + + + SoftwareEvent + + + + + + + + + + + + + + + + + + PT0S + + + + + Break + + + + SoftwareEvent + + + + + + SoftwareEvent + + + TimeBase + + + + + + + + + + + + + + + + + + SoftwareEventHistory + + + + PT5S + + + + + Expo + + + + + 1 + + + + + + + + + + + + + 0.13.5 + 0.13.5 + Task6 + 0001-01-01T00:00:00.0000000+00:00 + Stage1 + Plimbo + false + false + + + + + + + + M + false + + + + + Expo + + + + + 1 + + + + + + + + + + + + + 0.13.5 + 0.13.5 + Task7 + 0001-01-01T00:00:00.0000000+00:00 + Stage2 + Plimbo + false + false + + + + + + + + + + SessionInfo + + + + TimeElapsed + + + + + PT0S + PT1S + + + + + 1 + + + + + Convert.ToString(it) + + + + + + + CurrentTrial + + + + TimedTrial + + + + + + Index + + + Convert.ToString(it) + + + + + + + + + + + + Source1 + + + Item1 + + + Item2 + + + + + + + 2 + + + + Item1 + Item2 + + + + 1 + + + + + + + + + + + + + + + + + + + + + SessionMetadata + + + TimedTrial + + + BiasRandom + + + + + + Item2 + + + + + + + it > 1 ? 1 : it + + + it < -1 ? -1 : it + + + + + + new ( +it.Value as Bias, +it.Index as TrialIndex +) + + + TrialIndexedBias + + + + S + false + + + + + TimeBase + + + LickRight + + + + + + + + + 0.1 + 1 + + + + Random + + + + + + + + + -0.1 + 0.1 + + + + BiasRandom + + + + PT0S + PT0.01S + + + + + 0.01 + + + + + TimeBase + + + + 0 + 1 + + + + + 500 + + + + + 0.5 + + + + + 0 + 500 + + + + + 0 + 1 + 0 + 0.05 + Linear + + + + + 3.14 + + + + + + + (it +1) * 0.5 + + + + 0 + 1 + + + + + 500 + + + + + 0.5 + + + + it + + + + + + + + + true + + + + + 0 + 1 + + + + + 500 + + + + + 0.9 + + + + + + + + + + + + Item3 ? Item2 : Item1 + + + + 0 + 500 + + + + + 0 + 1 + 0 + 0.05 + Linear + + + + + + + (it +1) * 0.5 + + + + 0 + 1 + + + + + 500 + + + + + 0.5 + + + + + + + + + + + + + + + + + + + + 0.5 + 0.5 + + + + + + + + + + 0.59164587077011477 + 0.40913708100521279 + 5 + 0 + 5 + 0.5 + 5 + + 0 + + + + + + + + + + true + false + + + + SampleTrial + + + + Source1 + + + Random + + + + + + TimeSpan.FromSeconds(it) + + + + + + + + + PT0.377S + + + + + 1 + + + + + + + + + + + + + + + + + + + + TimeBase + + + + + + + + + TimedTrial + + + + 0 + 1 + + + + + 500 + + + + + 0.5 + + + + + + + + + + true + + + + + + + + + + 0 + 1 + + + + + 500 + + + + + 0.05 + + + + + + + Item3 ? Item1 : Item2 + + + + + + Item2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - 0 - + + Visualizer + + + + + true + 1 + None + + 0 + 0 + + 0 + + + Frame + + + true + + + + + + VisualizerStructure + + + + + + + Frame + + + true + + + true + + + Ethograph + WidthStretch + 0 + + + Camera + WidthFixed + 320 + + + BordersInnerH BordersOuterH BordersInnerV BordersOuterV BordersH BordersV BordersInner BordersOuter Borders SizingFixedFit + + 0 + 0 + + 0 + true + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + Behavior + + + true + + + Bias + + + true + + + Ethogram + + + + + + true + + + Camera + + + + + + true + + + true + 1 + Borders + + 0 + 0 + + 0 + + + Metadata + + + true + + + true + None + + + PreSession + true + pre-session + None + + + PreSession + + + IntraSession + true + intra-session + None + + + IntraSession + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Behavior1 + LickLeft + LickRight + TimedTrial + Behavior + AutoRewardRight + AutoRewardLeft + + + BiasVisualizer + + + + + + + + + + BiasVisualizer + + + + + + + Bias + + + BiasPlot + true + + -1 + 150 + + NoTitle + AutoFit + None + + + BiasPlot + + + + BiasPlot + + + + + -1 + 1 + + + R + L + + East + Outside + 1.1 + -1.1 + + + + + + + LeftBiasCorrection + + + + 0.5 + + + + TrialIndexedBias + + + TrialIndex + + + + + + + new (it.Item1 as StageCorrection, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + StageCorrection + + + BiasPlot + + + + Up + 3 + 1 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 1 + + + + true + None + + + + + + RightBiasCorrection + + + + -0.5 + + + + TrialIndexedBias + + + TrialIndex + + + + + + + new (it.Item1 as StageCorrection, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + StageCorrection + + + BiasPlot + + + + Down + 3 + 0 + 0 + 1 + 1 + 0 + 0 + 1 + 1 + 1 + + + + true + None + + + + + + TrialIndexedBias + + + 1000 + TrialIndex + Bias + + + BiasPlot + + + + 0 + 0 + 0 + 1 + 2 + + + + true + None + + + 1 + TrialIndex + Bias + + + BiasPlot + + + + Circle + 5 + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 + 1 + + + + + true + None + + + + Bias + + + + BiasPlot + + + {0:N2} + it + + + + + + + + TrialIndex + + + + + + + + + + + + + + + true + 0.22 + 196 + 0.21708835946260363 + + 0 + -10 + + None + + + + + 0.5 + + + + + + + new (it.Item1 as LeftBias, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + LeftBias + + + BiasPlot + + + + 1 + 0 + 0 + 0.7 + 1 + + + + true + None + + + + -0.5 + + + + + + + new (it.Item1 as RightBias, it.Item2 as TrialIndex) + + + 1000 + TrialIndex + RightBias + + + BiasPlot + + + + 0 + 0 + 1 + 0.7 + 1 + + + + true + None + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OutputControl + + + + UiHabituationTimeUpdate + + + UiMoveLeft + + + UiGiveRight + + + UiGiveLeft + + + UiMoveRight + + + UiManipulatorLeft + + + UiManipulatorForwardLeft + + + UiManipulatorForwardRight + + + UiManipulatorUp + + + UiDrainLeft + + + UiDrainBoth + + + UiDrainRight + + + UiManipulatorRight + + + UiManipulatorBackRight + + + UiManipulatorBackLeft + + + UiManipulatorDown + + + UiManipulatorStop + + + UiManipulatorHome + + + UiManipulatorInitialize + + + StartExperimentShortcut + + + StartExperiment + + + EndExperiment + + + UiCueGiveRight + + + UiCueGiveLeft + + + UiKeepLeftOpen + + + UiKeepRightOpen + + + UiKeepBothOpen + + + DrainLeftComplete + + + DrainRightComplete + + + + + + + + + Ethogram + + + SoftwareEventHistory + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Break + 200 + + + TrialOutcome + CornflowerBlue + 0.3 + + + RewardPeriod + Crimson + 0.3 + + + ITI + Cyan + 0.3 + + + + + TrialOutcome + SaddleBrown + 0.5 + 2 + Circle + + + LickLeft + Red + 0.8 + 2 + Down + + + LickRight + DodgerBlue + 0.2 + 2 + Up + + + + + + + CameraPreviewVisualizer + + + + Source1 + + + + + + + + + + + + CameraPreview + + + + + + + Camera + + + true + 1 + None + + 0 + 0 + + 0 + + + Table + + + + + + TriggeredCamerasStream + + + + + + + + + Source1 + + + Key + + + + + + + Camera1 + + + + + + + + + + + + + + + + + CameraStream + + + Table + + + true + + + CameraStream + + + EndExperiment + + + + + + Value + + + + + + + 2 + + + + + + + Flipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + Item1 + + + Item2 + + + + + + + + + Vertical + + + + NotFlipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + + + Item1 + + + + + + new ( + it.Size.Width / 2 as X, + it.Size.Height / 2 as Y +) + + + + + + + + + + 320 + 240 + + + + + + + + + + + + + + 320 + 240 + + + 0 + 0 + + 0 + + 1 + 1 + + + + + + + + + + + 1,0,0;0,1,0 + Linear + + 0 + 0 + 0 + 0 + + + + + Table + + + + Rgba + Repeat + Repeat + Linear + Linear + + + + OverlayAlpha + + + + + + + + + + + + + 0.371 + 0.1 + 0.3 + + + + + + + + + PreviewImage + true + + 320 + 320 + + + 0 + 0 + + + 1 + 1 + + + + Table + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + true + Alpha + + + true + + + true + 0 + 1 + None + 0.5 + + + OverlayAlpha + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CameraPreviewVisualizer + + + + Source1 + + + + + + + + + + + + CameraPreview + + + + + + + Camera + + + true + 1 + None + + 0 + 0 + + 0 + + + Table + + + + + + TriggeredCamerasStream + + + + + + + + + Source1 + + + Key + + + + + + + Camera2 + + + + + + + + + + + + + + + + + CameraStream + + + Table + + + true + + + CameraStream + + + EndExperiment + + + + + + Value + + + + + + + 2 + + + + + + + Flipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + Item1 + + + Item2 + + + + + + + + + Vertical + + + + NotFlipped + + + + Source1 + + + Item2 + + + + 2 + + + + + + + + + + + + + + + Item1 + + + + + + new ( + it.Size.Width / 2 as X, + it.Size.Height / 2 as Y +) + + + + + + + + + + 320 + 240 + + + + + + + + + + + + + + 320 + 240 + + + 0 + 0 + + 0 + + 1 + 1 + + + + + + + + + + + 1,0,0;0,1,0 + Linear + + 0 + 0 + 0 + 0 + + + + + Table + + + + Rgba + Repeat + Repeat + Linear + Linear + + + + OverlayAlpha + + + + + + + + + + + + + 0.5 + 0.1 + 0.3 + + + + + + + + + PreviewImage + true + + 320 + 320 + + + 0 + 0 + + + 1 + 1 + + + + Table + + + true + + + true + 1 + None + + 0 + 0 + + 0 + + + true + + + true + 2 + None + + 0 + 0 + + 0 + + + true + + + true + Alpha + + + true + + + true + 0 + 1 + None + 0.5 + + + OverlayAlpha + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SessionInfo + + + + AindDynamicForaging + + 0.0.2-rc37 + Task6 + + + + + + + Metadata + StartExperimentShortcut + UiHabituationTimeUpdate + + + PreSession + UiKeepLeftOpen + UiKeepRightOpen + UiKeepBothOpen + UiDrainLeft + UiDrainRight + UiDrainBoth + UiManipulatorLeft + UiManipulatorForwardLeft + UiManipulatorForwardRight + UiManipulatorUp + UiManipulatorRight + UiManipulatorBackLeft + UiManipulatorBackRight + UiManipulatorDown + UiManipulatorStop + UiManipulatorHome + UiManipulatorInitialize + DrainLeftComplete + DrainRightComplete + + + IntraSession + UiGiveLeft + UiGiveRight + UiMoveLeft + UiMoveRight + StartExperiment + UiCueGiveLeft + UiCueGiveRight + SessionMetadata + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file