diff --git a/Interface/Harp.Olfactometer/ConfigureOdorMix.cs b/Interface/Harp.Olfactometer/ConfigureOdorMix.cs new file mode 100644 index 0000000..a9b20d6 --- /dev/null +++ b/Interface/Harp.Olfactometer/ConfigureOdorMix.cs @@ -0,0 +1,219 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai.Harp; +using System.Collections.Generic; + + +namespace Harp.Olfactometer +{ + /// + /// Represents an operator that generates a sequence of Harp messages to + /// configure the odor mixture in the olfactometer as corresponding odor + /// valves. + /// The configuration will set the target flow for each channel, assuming + /// a constant target full flow, target odor flow and percentage of each odor. + /// + [Description("Generates a sequence of Harp messages to configure an odor mixture.")] + + public class ConfigureOdorMix : Source + { + /// + /// Gets or sets the concentration of Channel0. + /// + [Range(0, 1)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("Odor dilution percentage for channel 0.")] + public float PercentageChannel0 { get; set; } = 0; + + /// + /// Gets or sets the concentration of Channel1. + /// + [Range(0, 1)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("Odor dilution percentage for channel 1.")] + public float PercentageChannel1 { get; set; } = 0; + + /// + /// Gets or sets the concentration of Channel2. + /// + [Range(0, 1)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("Odor dilution percentage for channel 2.")] + public float PercentageChannel2 { get; set; } = 0; + + /// + /// Gets or sets the concentration of Channel3. + /// + [Range(0, 1)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("Odor dilution percentage for channel 3. This Value will be ignored if Channel3AsCarrier is set to True.")] + public float PercentageChannel3 { get; set; } = 0; + + + public bool channel3AsCarrier = false; + /// + /// Gets or sets the operation mode of Channel3. + /// + [Description("Specifies if Channel3 should be used as an odor or carrier channel. If True, the flow value value of Channel3 will be set to TargetTotalFlow.")] + public bool Channel3AsCarrier + { + get { return channel3AsCarrier; } + set + { + channel3AsCarrier = value; + PercentageChannel3 = channel3AsCarrier ? float.NaN : 0; + } + } + + /// + /// Gets or sets the target flow rate for all odor channels. + /// + [Range(0, 100)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("The target odor flow for each channel, assuming PercentageChannelX = 1.")] + public int TargetOdorFlow { get; set; } = 100; + + + /// + /// Gets or sets the target total flow rate. + /// + [Range(0, 100)] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Description("The total target flow rate. This value will be used to calculate the flow rate of the carrier automatically.")] + public int TargetTotalFlow { get; set; } = 1000; + + private List ConstructMessages() + { + var adjustedOdorFlow0 = (int)(TargetOdorFlow * PercentageChannel0); + var adjustedOdorFlow1 = (int)(TargetOdorFlow * PercentageChannel1); + var adjustedOdorFlow2 = (int)(TargetOdorFlow * PercentageChannel2); + var adjustedOdorFlow3 = channel3AsCarrier ? 0 : (int)(TargetOdorFlow * PercentageChannel3); + var carrierFlow = TargetOdorFlow - (adjustedOdorFlow0 + adjustedOdorFlow1 + adjustedOdorFlow2 + adjustedOdorFlow3); + + var channelsTargetFlow = ChannelsTargetFlow.FromPayload(MessageType.Write, new ChannelsTargetFlowPayload( + adjustedOdorFlow0, + adjustedOdorFlow1, + adjustedOdorFlow2, + channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3, + carrierFlow)); + + adjustedOdorFlow3 = channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3; + var odorValveState = OdorValveState.FromPayload(MessageType.Write, + ( + (adjustedOdorFlow0 > 0 ? OdorValves.Valve0 : OdorValves.None) | + (adjustedOdorFlow1 > 0 ? OdorValves.Valve1 : OdorValves.None) | + (adjustedOdorFlow2 > 0 ? OdorValves.Valve2 : OdorValves.None) | + (adjustedOdorFlow3 > 0 ? OdorValves.Valve3 : OdorValves.None) + )); + + return new List { channelsTargetFlow, odorValveState }; + } + + private List ConstructMessages(int odorIndex, double concentration) + { + + var adjustedOdorFlow0 = 0; + var adjustedOdorFlow1 = 0; + var adjustedOdorFlow2 = 0; + var adjustedOdorFlow3 = 0; + + switch (odorIndex) + { + case 0: + adjustedOdorFlow0 = (int)(TargetOdorFlow * concentration); + break; + case 1: + adjustedOdorFlow1 = (int)(TargetOdorFlow * concentration); + break; + case 2: + adjustedOdorFlow2 = (int)(TargetOdorFlow * concentration); + break; + case 3: + if (channel3AsCarrier) + { + throw new Exception("Channel 3 is set as carrier. Cannot set flow for this channel."); + } + adjustedOdorFlow3 = (int)(TargetOdorFlow * concentration); + break; + default: + throw new Exception("Invalid channel number. Must be between 0 and 3."); + } + + var carrierFlow = TargetOdorFlow - (adjustedOdorFlow0 + adjustedOdorFlow1 + adjustedOdorFlow2 + adjustedOdorFlow3); + + var channelsTargetFlow = ChannelsTargetFlow.FromPayload(MessageType.Write, new ChannelsTargetFlowPayload( + adjustedOdorFlow0, + adjustedOdorFlow1, + adjustedOdorFlow2, + channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3, + carrierFlow)); + + adjustedOdorFlow3 = channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3; + var odorValveState = OdorValveState.FromPayload(MessageType.Write, + ( + (adjustedOdorFlow0 > 0 ? OdorValves.Valve0 : OdorValves.None) | + (adjustedOdorFlow1 > 0 ? OdorValves.Valve1 : OdorValves.None) | + (adjustedOdorFlow2 > 0 ? OdorValves.Valve2 : OdorValves.None) | + (adjustedOdorFlow3 > 0 ? OdorValves.Valve3 : OdorValves.None) + )); + + return new List { channelsTargetFlow, odorValveState }; + } + + + /// + /// Generates an observable sequence of Harp messages to configure the + /// odor mixture whenever the input sequence produces an element. + /// + /// + /// The type of the elements in the sequence. + /// + /// + /// The sequence containing the notifications used to emit new configuration + /// messages. + /// + /// + /// A sequence of objects representing the commands + /// needed to fully configure odor mixture. + /// + public IObservable Generate(IObservable source) + { + return source.SelectMany(value => ConstructMessages().ToObservable()); + } + + /// + /// Generates an observable sequence of Harp messages to configure the + /// odor mixture. + /// + /// + /// A sequence of objects representing the commands + /// needed to fully configure the PWM feature. + /// + public override IObservable Generate() + { + return ConstructMessages().ToObservable(); + } + + /// + /// Generates an observable sequence of Harp messages to configure the + /// odor mixture whenever the input sequence produces an element. The + /// tuple values will be used to target a specific channel index (Item1) + /// and set its concentration (Item2). + /// + /// + /// The sequence containing a tuple with the channel index and concentration. + /// + /// + /// A sequence of objects representing the commands + /// needed to fully configure odor mixture. + /// + public IObservable Generate(IObservable> source) + { + return source.SelectMany(value => ConstructMessages(value.Item1, value.Item2)); + } + } + +}