From 10054ca95583566bcd6d62cd214370e1c9ba59b9 Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Wed, 12 Jul 2023 04:16:37 +0800 Subject: [PATCH 1/5] feat: basic schedule element --- src/Qynit.PulseGen/ArrangeOption.cs | 7 ++ src/Qynit.PulseGen/Envelope.cs | 1 + src/Qynit.PulseGen/PlayElement.cs | 35 +++++++++ src/Qynit.PulseGen/ScheduleElement.cs | 54 +++++++++++++ src/Qynit.PulseGen/SetFrequencyElement.cs | 25 ++++++ src/Qynit.PulseGen/SetPhaseElement.cs | 24 ++++++ src/Qynit.PulseGen/ShiftFrequencyElement.cs | 25 ++++++ src/Qynit.PulseGen/ShiftPhaseElement.cs | 25 ++++++ src/Qynit.PulseGen/StackSchedule.cs | 87 +++++++++++++++++++++ src/Qynit.PulseGen/SwapPhaseElement.cs | 25 ++++++ src/Qynit.PulseGen/Thickness.cs | 5 ++ 11 files changed, 313 insertions(+) create mode 100644 src/Qynit.PulseGen/ArrangeOption.cs create mode 100644 src/Qynit.PulseGen/PlayElement.cs create mode 100644 src/Qynit.PulseGen/ScheduleElement.cs create mode 100644 src/Qynit.PulseGen/SetFrequencyElement.cs create mode 100644 src/Qynit.PulseGen/SetPhaseElement.cs create mode 100644 src/Qynit.PulseGen/ShiftFrequencyElement.cs create mode 100644 src/Qynit.PulseGen/ShiftPhaseElement.cs create mode 100644 src/Qynit.PulseGen/StackSchedule.cs create mode 100644 src/Qynit.PulseGen/SwapPhaseElement.cs create mode 100644 src/Qynit.PulseGen/Thickness.cs diff --git a/src/Qynit.PulseGen/ArrangeOption.cs b/src/Qynit.PulseGen/ArrangeOption.cs new file mode 100644 index 0000000..82d7983 --- /dev/null +++ b/src/Qynit.PulseGen/ArrangeOption.cs @@ -0,0 +1,7 @@ +namespace Qynit.PulseGen; + +public enum ArrangeOption +{ + EndToStart, + StartToEnd, +} diff --git a/src/Qynit.PulseGen/Envelope.cs b/src/Qynit.PulseGen/Envelope.cs index bff92b8..020d373 100644 --- a/src/Qynit.PulseGen/Envelope.cs +++ b/src/Qynit.PulseGen/Envelope.cs @@ -6,6 +6,7 @@ public readonly record struct Envelope public IPulseShape? Shape { get; } public double Width { get; } public double Plateau { get; } + public double Duration => Plateau + Width; public bool IsRectangle => Shape is null; public bool IsZero => Width == 0 && Plateau == 0; public Envelope(IPulseShape? shape, double width, double plateau) diff --git a/src/Qynit.PulseGen/PlayElement.cs b/src/Qynit.PulseGen/PlayElement.cs new file mode 100644 index 0000000..d5eb74d --- /dev/null +++ b/src/Qynit.PulseGen/PlayElement.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace Qynit.PulseGen; +internal sealed class PlayElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + public int ChannelId { get; } + public Envelope Envelope { get; } + public double Frequency { get; } + public double Phase { get; } + public double Amplitude { get; } + public double DragCoefficient { get; } + + public PlayElement(int channelId, Envelope envelope, double frequency, double phase, double amplitude, double dragCoefficient) + { + Debug.Assert(envelope.Duration >= 0); + ChannelId = channelId; + Envelope = envelope; + Frequency = frequency; + Phase = phase; + Amplitude = amplitude; + DragCoefficient = dragCoefficient; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return Envelope.Duration; + } + + protected override double MeasureOverride(double maxDuration) + { + return Envelope.Duration; + } +} diff --git a/src/Qynit.PulseGen/ScheduleElement.cs b/src/Qynit.PulseGen/ScheduleElement.cs new file mode 100644 index 0000000..189772d --- /dev/null +++ b/src/Qynit.PulseGen/ScheduleElement.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +public abstract class ScheduleElement +{ + public ScheduleElement? Parent { get; internal set; } + public Thickness Margin { get; set; } + public bool IsVisible { get; set; } + public double? DesiredDuration { get; private set; } + public double? ActualDuration { get; private set; } + public double? ActualTime { get; private set; } + public abstract IReadOnlySet Channels { get; } + internal bool IsMeasuring { get; private set; } + public void Measure(double maxDuration) + { + Debug.Assert(maxDuration >= 0 || double.IsPositiveInfinity(maxDuration)); + if (IsMeasuring) + { + ThrowHelper.ThrowInvalidOperationException("Already measuring"); + } + IsMeasuring = true; + var margin = Margin.Total; + Debug.Assert(double.IsFinite(margin)); + var availableDuration = Math.Max(maxDuration - margin, 0); + var desiredDuration = MeasureOverride(availableDuration) + margin; + Debug.Assert(double.IsFinite(desiredDuration)); + DesiredDuration = Math.Max(desiredDuration, 0); + IsMeasuring = false; + } + protected abstract double MeasureOverride(double maxDuration); + public void Arrange(double time, double finalDuration) + { + Debug.Assert(double.IsFinite(time)); + Debug.Assert(double.IsFinite(finalDuration) && finalDuration >= 0); + if (DesiredDuration is null) + { + ThrowHelper.ThrowInvalidOperationException("Not measured"); + } + if (finalDuration < DesiredDuration) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(finalDuration), finalDuration, "Final duration is less than desired duration"); + } + var innerTime = time + Margin.Start; + Debug.Assert(double.IsFinite(innerTime)); + var innerDuration = Math.Max(finalDuration - Margin.Total, 0); + var actualDuration = ArrangeOverride(innerTime, innerDuration); + Debug.Assert(double.IsFinite(actualDuration)); + ActualDuration = actualDuration; + ActualTime = innerTime; + } + protected abstract double ArrangeOverride(double time, double finalDuration); +} diff --git a/src/Qynit.PulseGen/SetFrequencyElement.cs b/src/Qynit.PulseGen/SetFrequencyElement.cs new file mode 100644 index 0000000..2a103cd --- /dev/null +++ b/src/Qynit.PulseGen/SetFrequencyElement.cs @@ -0,0 +1,25 @@ +namespace Qynit.PulseGen; +internal sealed class SetFrequencyElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + + public int ChannelId { get; } + public double Frequency { get; } + + public SetFrequencyElement(int channelId, double frequency) + { + ChannelId = channelId; + Frequency = frequency; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } +} diff --git a/src/Qynit.PulseGen/SetPhaseElement.cs b/src/Qynit.PulseGen/SetPhaseElement.cs new file mode 100644 index 0000000..eeb7f6d --- /dev/null +++ b/src/Qynit.PulseGen/SetPhaseElement.cs @@ -0,0 +1,24 @@ +namespace Qynit.PulseGen; +internal sealed class SetPhaseElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + public int ChannelId { get; } + public double Phase { get; } + + public SetPhaseElement(int channelId, double phase) + { + ChannelId = channelId; + Phase = phase; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } +} diff --git a/src/Qynit.PulseGen/ShiftFrequencyElement.cs b/src/Qynit.PulseGen/ShiftFrequencyElement.cs new file mode 100644 index 0000000..ae3ff76 --- /dev/null +++ b/src/Qynit.PulseGen/ShiftFrequencyElement.cs @@ -0,0 +1,25 @@ +namespace Qynit.PulseGen; +internal sealed class ShiftFrequencyElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + + public int ChannelId { get; } + public double DeltaFrequency { get; } + + public ShiftFrequencyElement(int channelId, double deltaFrequency) + { + ChannelId = channelId; + DeltaFrequency = deltaFrequency; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } +} diff --git a/src/Qynit.PulseGen/ShiftPhaseElement.cs b/src/Qynit.PulseGen/ShiftPhaseElement.cs new file mode 100644 index 0000000..f1edf7c --- /dev/null +++ b/src/Qynit.PulseGen/ShiftPhaseElement.cs @@ -0,0 +1,25 @@ +namespace Qynit.PulseGen; +internal sealed class ShiftPhaseElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + + public int ChannelId { get; } + public double DeltaPhase { get; } + + public ShiftPhaseElement(int channelId, double deltaPhase) + { + ChannelId = channelId; + DeltaPhase = deltaPhase; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } +} diff --git a/src/Qynit.PulseGen/StackSchedule.cs b/src/Qynit.PulseGen/StackSchedule.cs new file mode 100644 index 0000000..6a9c1c8 --- /dev/null +++ b/src/Qynit.PulseGen/StackSchedule.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +internal class StackSchedule : ScheduleElement +{ + public ArrangeOption ArrangeOption { get; set; } + public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); + private HashSet? _channels; + private readonly List _elements = new(); + + public void Add(ScheduleElement element) + { + if (element.Parent is not null) + { + ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); + } + _elements.Add(element); + element.Parent = this; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + var channels = Channels; + var arrangeOption = ArrangeOption; + var elements = arrangeOption switch + { + ArrangeOption.StartToEnd => _elements.AsEnumerable(), + ArrangeOption.EndToStart => _elements.AsEnumerable().Reverse(), + _ => throw new NotImplementedException(), + }; + var durations = channels.ToDictionary(c => c, _ => 0.0); + Debug.Assert(DesiredDuration is not null); + var totalDuration = DesiredDuration.Value; + Debug.Assert(finalDuration >= totalDuration); + foreach (var element in elements) + { + var elementChannels = element.Channels; + Debug.Assert(element.DesiredDuration is not null); + var innerDuration = element.DesiredDuration.Value; + var usedDuration = elementChannels.Max(c => durations[c]); + var innerTime = arrangeOption switch + { + ArrangeOption.StartToEnd => usedDuration, + ArrangeOption.EndToStart => totalDuration - usedDuration - innerDuration, + _ => throw new NotImplementedException(), + }; + element.Arrange(innerTime, innerDuration); + var newDuration = usedDuration + innerDuration; + Debug.Assert(double.IsFinite(newDuration)); + foreach (var channel in elementChannels) + { + durations[channel] = newDuration; + } + } + return totalDuration; + } + + protected override double MeasureOverride(double maxDuration) + { + var channels = Channels; + var elements = ArrangeOption switch + { + ArrangeOption.StartToEnd => _elements.AsEnumerable(), + ArrangeOption.EndToStart => _elements.AsEnumerable().Reverse(), + _ => throw new NotImplementedException(), + }; + var durations = channels.ToDictionary(c => c, _ => 0.0); + foreach (var element in elements) + { + var elementChannels = element.Channels; + var usedDuration = elementChannels.Max(c => durations[c]); + var leftDuration = maxDuration - usedDuration; + element.Measure(leftDuration); + var desiredDuration = element.DesiredDuration; + Debug.Assert(desiredDuration is not null); + var newDuration = usedDuration + desiredDuration.Value; + Debug.Assert(double.IsFinite(newDuration)); + foreach (var channel in elementChannels) + { + durations[channel] = newDuration; + } + } + return durations.Values.Max(); + } +} diff --git a/src/Qynit.PulseGen/SwapPhaseElement.cs b/src/Qynit.PulseGen/SwapPhaseElement.cs new file mode 100644 index 0000000..2fcb6c4 --- /dev/null +++ b/src/Qynit.PulseGen/SwapPhaseElement.cs @@ -0,0 +1,25 @@ +namespace Qynit.PulseGen; +internal sealed class SwapPhaseElement : ScheduleElement +{ + private HashSet? _channels; + public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId1, ChannelId2 }; + + public int ChannelId1 { get; } + public int ChannelId2 { get; } + + public SwapPhaseElement(int channelId1, int channelId2) + { + ChannelId1 = channelId1; + ChannelId2 = channelId2; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } +} diff --git a/src/Qynit.PulseGen/Thickness.cs b/src/Qynit.PulseGen/Thickness.cs new file mode 100644 index 0000000..5135217 --- /dev/null +++ b/src/Qynit.PulseGen/Thickness.cs @@ -0,0 +1,5 @@ +namespace Qynit.PulseGen; +public record struct Thickness(double Start, double End) +{ + public readonly double Total => Start + End; +} From 8b796716fdb013ce488fcb70fcb54d7ab2150cbd Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Thu, 13 Jul 2023 00:03:23 +0800 Subject: [PATCH 2/5] feat: schedule alignment --- examples/WaveGenDemo/Program.cs | 85 ++++++++++++--------- src/Qynit.PulseGen/Alignment.cs | 9 +++ src/Qynit.PulseGen/BarrierElement.cs | 24 ++++++ src/Qynit.PulseGen/GridSchedule.cs | 64 ++++++++++++++++ src/Qynit.PulseGen/PlayElement.cs | 7 +- src/Qynit.PulseGen/ScheduleElement.cs | 18 ++++- src/Qynit.PulseGen/SetFrequencyElement.cs | 7 +- src/Qynit.PulseGen/SetPhaseElement.cs | 7 +- src/Qynit.PulseGen/ShiftFrequencyElement.cs | 7 +- src/Qynit.PulseGen/ShiftPhaseElement.cs | 7 +- src/Qynit.PulseGen/StackSchedule.cs | 22 ++++-- src/Qynit.PulseGen/SwapPhaseElement.cs | 7 +- src/Qynit.PulseGen/Thickness.cs | 2 + 13 files changed, 218 insertions(+), 48 deletions(-) create mode 100644 src/Qynit.PulseGen/Alignment.cs create mode 100644 src/Qynit.PulseGen/BarrierElement.cs create mode 100644 src/Qynit.PulseGen/GridSchedule.cs diff --git a/examples/WaveGenDemo/Program.cs b/examples/WaveGenDemo/Program.cs index 986eb2e..79a511b 100644 --- a/examples/WaveGenDemo/Program.cs +++ b/examples/WaveGenDemo/Program.cs @@ -3,15 +3,18 @@ using Qynit.PulseGen; +using ScottPlot; + for (var i = 0; i < 5; i++) { RunDouble(); } -for (var i = 0; i < 5; i++) -{ - RunSingle(); -} +//for (var i = 0; i < 5; i++) +//{ +// RunSingle(); +//} +//RunDouble(); static void RunDouble() { @@ -35,29 +38,42 @@ static void Run() where T : unmanaged, IFloatingPointIeee754 var ch1 = phaseTrackingTransform.AddChannel(100e6); var ch2 = phaseTrackingTransform.AddChannel(250e6); var shape = new HannPulseShape(); - phaseTrackingTransform.Play(ch1, new(shape, 30e-9, 100e-9), 0, 0, 0.5, 2e-9, 0); - phaseTrackingTransform.Play(ch2, new(shape, 30e-9, 100e-9), 0, 0, 0.6, 2e-9, 0); - phaseTrackingTransform.ShiftPhase(ch1, 0.25); - phaseTrackingTransform.ShiftPhase(ch2, -0.25); - phaseTrackingTransform.Play(ch1, new(shape, 30e-9, 100e-9), 0, 0, 0.5, 2e-9, 200e-9); - phaseTrackingTransform.Play(ch2, new(shape, 30e-9, 100e-9), 0, 0, 0.6, 2e-9, 200e-9); - phaseTrackingTransform.ShiftFrequency(ch1, -100e6, 400e-9); - phaseTrackingTransform.ShiftFrequency(ch2, -250e6, 400e-9); - phaseTrackingTransform.Play(ch1, new(shape, 200e-9, 0), 0, 0, 0.5, 2e-9, 400e-9); - phaseTrackingTransform.Play(ch2, new(shape, 200e-9, 0), 0, 0, 0.6, 2e-9, 400e-9); - phaseTrackingTransform.SetFrequency(ch1, 0, 600e-9); - phaseTrackingTransform.SetFrequency(ch2, 0, 600e-9); - var tStart = 600e-9; - var count = 0; - while (tStart < 49e-6) - { - phaseTrackingTransform.Play(ch1, new(shape, 30e-9, 0e-9), 0, 0, 0.5, 2e-9, tStart); - phaseTrackingTransform.Play(ch2, new(shape, 30e-9, 0e-9), 0, 0, 0.6, 2e-9, tStart); - phaseTrackingTransform.ShiftPhase(ch1, 0.25); - phaseTrackingTransform.ShiftPhase(ch2, -0.25); - tStart += 0.1e-9; - count++; - } + var stack = new StackSchedule(); + stack.Add(new PlayElement(ch1, new(shape, 30e-9, 100e-9), 0, 0, 0.5, 2e-9)); + stack.Add(new PlayElement(ch2, new(shape, 30e-9, 50e-9), 0, 0, 0.6, 2e-9)); + stack.Add(new ShiftPhaseElement(ch1, 0.25)); + stack.Add(new ShiftPhaseElement(ch2, -0.25)); + stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); + + var stack2 = new StackSchedule(); + stack2.Add(new PlayElement(ch1, new(shape, 30e-9, 0), 0, 0, 0.5, 2e-9) { Margin = new(15e-9) }); + stack2.Add(new PlayElement(ch1, new(shape, 30e-9, 0), 0, 0, 0.5, 2e-9) { Margin = new(15e-9) }); + stack2.Add(new PlayElement(ch1, new(shape, 30e-9, 0), 0, 0, 0.5, 2e-9) { Margin = new(15e-9) }); + stack.Add(stack2); + + stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); + + var grid = new GridSchedule() { Margin = new(0, 90e-9) }; + grid.Add(new PlayElement(ch1, new(shape, 30e-9, 100e-9), 0, 0, 0.5, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Center }); + grid.Add(new PlayElement(ch2, new(shape, 30e-9, 50e-9), 0, 0, 0.6, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Center }); + stack.Add(grid); + + stack.Add(new ShiftFrequencyElement(ch1, -100e6)); + stack.Add(new ShiftFrequencyElement(ch2, -250e6)); + stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); + stack.Add(new PlayElement(ch1, new(null, 200e-9, 0), 0, 0, 0.5, 2e-9)); + stack.Add(new PlayElement(ch2, new(null, 100e-9, 0), 0, 0, 0.6, 2e-9)); + stack.Add(new SetFrequencyElement(ch1, 0)); + stack.Add(new SetFrequencyElement(ch2, 0)); + stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); + stack.Add(new PlayElement(ch1, new(shape, 200e-9, 0), 0, 0, 0.5, 2e-9)); + stack.Add(new PlayElement(ch2, new(shape, 100e-9, 0), 0, 0, 0.6, 2e-9)); + var main = new GridSchedule(); + main.Add(stack); + main.Measure(49.9e-6); + main.Arrange(0, 49.9e-6); + main.Render(0, phaseTrackingTransform); + var t1 = sw.Elapsed; var pulseLists = phaseTrackingTransform.Finish(); @@ -73,14 +89,13 @@ static void Run() where T : unmanaged, IFloatingPointIeee754 Console.WriteLine($"Build time: {(t2 - t1).TotalMilliseconds} ms"); Console.WriteLine($"Sampling time: {(t3 - t2).TotalMilliseconds} ms"); Console.WriteLine($"Total elapsed time: {sw.Elapsed.TotalMilliseconds} ms"); - Console.WriteLine($"Count = {count}"); using var waveform1 = waveforms[ch1]; using var waveform2 = waveforms[ch2]; - //var plot = new Plot(1920, 1080); - //plot.AddSignal(waveform1.DataI[..2000].ToArray(), sampleRate, label: $"wave 1 real"); - //plot.AddSignal(waveform1.DataQ[..2000].ToArray(), sampleRate, label: $"wave 1 imag"); - //plot.AddSignal(waveform2.DataI[..2000].ToArray(), sampleRate, label: $"wave 2 real"); - //plot.AddSignal(waveform2.DataQ[..2000].ToArray(), sampleRate, label: $"wave 2 imag"); - //plot.Legend(); - //plot.SaveFig("demo2.png"); + var plot = new Plot(1920, 1080); + plot.AddSignal(waveform1.DataI[^2000..].ToArray(), sampleRate, label: $"wave 1 real"); + plot.AddSignal(waveform1.DataQ[^2000..].ToArray(), sampleRate, label: $"wave 1 imag"); + plot.AddSignal(waveform2.DataI[^2000..].ToArray(), sampleRate, label: $"wave 2 real"); + plot.AddSignal(waveform2.DataQ[^2000..].ToArray(), sampleRate, label: $"wave 2 imag"); + plot.Legend(); + plot.SaveFig("demo2.png"); } diff --git a/src/Qynit.PulseGen/Alignment.cs b/src/Qynit.PulseGen/Alignment.cs new file mode 100644 index 0000000..42ae763 --- /dev/null +++ b/src/Qynit.PulseGen/Alignment.cs @@ -0,0 +1,9 @@ +namespace Qynit.PulseGen; + +public enum Alignment +{ + End, + Start, + Center, + Stretch, +} diff --git a/src/Qynit.PulseGen/BarrierElement.cs b/src/Qynit.PulseGen/BarrierElement.cs new file mode 100644 index 0000000..54a9819 --- /dev/null +++ b/src/Qynit.PulseGen/BarrierElement.cs @@ -0,0 +1,24 @@ +namespace Qynit.PulseGen; +public class BarrierElement : ScheduleElement +{ + public override IReadOnlySet Channels { get; } + + public BarrierElement(params int[] channelIds) : this((IEnumerable)channelIds) { } + + public BarrierElement(IEnumerable channelIds) + { + Channels = new HashSet(channelIds); + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + return 0; + } + + protected override double MeasureOverride(double maxDuration) + { + return 0; + } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) { } +} diff --git a/src/Qynit.PulseGen/GridSchedule.cs b/src/Qynit.PulseGen/GridSchedule.cs new file mode 100644 index 0000000..b15a039 --- /dev/null +++ b/src/Qynit.PulseGen/GridSchedule.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +public class GridSchedule : ScheduleElement +{ + public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); + private HashSet? _channels; + private readonly List _elements = new(); + + public GridSchedule() + { + Alignment = Alignment.Stretch; + } + + public void Add(ScheduleElement element) + { + if (element.Parent is not null) + { + ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); + } + _elements.Add(element); + element.Parent = this; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + foreach (var element in _elements) + { + Debug.Assert(element.DesiredDuration is not null); + var elementDuration = (element.Alignment == Alignment.Stretch) ? finalDuration : element.DesiredDuration.Value; + Debug.Assert(elementDuration <= finalDuration); + var elementTime = element.Alignment switch + { + Alignment.Start => 0, + Alignment.Center => (finalDuration - elementDuration) / 2, + Alignment.End => finalDuration - elementDuration, + Alignment.Stretch => 0, + _ => throw new NotImplementedException(), + }; + element.Arrange(elementTime, elementDuration); + } + return finalDuration; + } + + protected override double MeasureOverride(double maxDuration) + { + foreach (var element in _elements) + { + element.Measure(maxDuration); + Debug.Assert(element.DesiredDuration is not null); + } + return _elements.Count > 0 ? _elements.Max(e => e.DesiredDuration!.Value) : 0; + } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + foreach (var element in _elements) + { + element.Render(time, phaseTrackingTransform); + } + } +} diff --git a/src/Qynit.PulseGen/PlayElement.cs b/src/Qynit.PulseGen/PlayElement.cs index d5eb74d..c3ba885 100644 --- a/src/Qynit.PulseGen/PlayElement.cs +++ b/src/Qynit.PulseGen/PlayElement.cs @@ -1,7 +1,7 @@ using System.Diagnostics; namespace Qynit.PulseGen; -internal sealed class PlayElement : ScheduleElement +public sealed class PlayElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; @@ -32,4 +32,9 @@ protected override double MeasureOverride(double maxDuration) { return Envelope.Duration; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.Play(ChannelId, Envelope, Frequency, Phase, Amplitude, DragCoefficient, time); + } } diff --git a/src/Qynit.PulseGen/ScheduleElement.cs b/src/Qynit.PulseGen/ScheduleElement.cs index 189772d..7653509 100644 --- a/src/Qynit.PulseGen/ScheduleElement.cs +++ b/src/Qynit.PulseGen/ScheduleElement.cs @@ -7,7 +7,8 @@ public abstract class ScheduleElement { public ScheduleElement? Parent { get; internal set; } public Thickness Margin { get; set; } - public bool IsVisible { get; set; } + public Alignment Alignment { get; set; } + public bool IsVisible { get; set; } = true; public double? DesiredDuration { get; private set; } public double? ActualDuration { get; private set; } public double? ActualTime { get; private set; } @@ -51,4 +52,19 @@ public void Arrange(double time, double finalDuration) ActualTime = innerTime; } protected abstract double ArrangeOverride(double time, double finalDuration); + public void Render(double time, PhaseTrackingTransform phaseTrackingTransform) + { + if (ActualTime is null || ActualDuration is null) + { + ThrowHelper.ThrowInvalidOperationException("Not arranged"); + } + if (!IsVisible) + { + return; + } + var innerTime = time + ActualTime.Value; + Debug.Assert(double.IsFinite(innerTime)); + RenderOverride(innerTime, phaseTrackingTransform); + } + protected abstract void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform); } diff --git a/src/Qynit.PulseGen/SetFrequencyElement.cs b/src/Qynit.PulseGen/SetFrequencyElement.cs index 2a103cd..69197bd 100644 --- a/src/Qynit.PulseGen/SetFrequencyElement.cs +++ b/src/Qynit.PulseGen/SetFrequencyElement.cs @@ -1,5 +1,5 @@ namespace Qynit.PulseGen; -internal sealed class SetFrequencyElement : ScheduleElement +public sealed class SetFrequencyElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; @@ -22,4 +22,9 @@ protected override double MeasureOverride(double maxDuration) { return 0; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.SetFrequency(ChannelId, Frequency, time); + } } diff --git a/src/Qynit.PulseGen/SetPhaseElement.cs b/src/Qynit.PulseGen/SetPhaseElement.cs index eeb7f6d..8d7b3ff 100644 --- a/src/Qynit.PulseGen/SetPhaseElement.cs +++ b/src/Qynit.PulseGen/SetPhaseElement.cs @@ -1,5 +1,5 @@ namespace Qynit.PulseGen; -internal sealed class SetPhaseElement : ScheduleElement +public sealed class SetPhaseElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; @@ -21,4 +21,9 @@ protected override double MeasureOverride(double maxDuration) { return 0; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.SetPhase(ChannelId, Phase, time); + } } diff --git a/src/Qynit.PulseGen/ShiftFrequencyElement.cs b/src/Qynit.PulseGen/ShiftFrequencyElement.cs index ae3ff76..6a567de 100644 --- a/src/Qynit.PulseGen/ShiftFrequencyElement.cs +++ b/src/Qynit.PulseGen/ShiftFrequencyElement.cs @@ -1,5 +1,5 @@ namespace Qynit.PulseGen; -internal sealed class ShiftFrequencyElement : ScheduleElement +public sealed class ShiftFrequencyElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; @@ -22,4 +22,9 @@ protected override double MeasureOverride(double maxDuration) { return 0; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.ShiftFrequency(ChannelId, DeltaFrequency, time); + } } diff --git a/src/Qynit.PulseGen/ShiftPhaseElement.cs b/src/Qynit.PulseGen/ShiftPhaseElement.cs index f1edf7c..a0fee43 100644 --- a/src/Qynit.PulseGen/ShiftPhaseElement.cs +++ b/src/Qynit.PulseGen/ShiftPhaseElement.cs @@ -1,5 +1,5 @@ namespace Qynit.PulseGen; -internal sealed class ShiftPhaseElement : ScheduleElement +public sealed class ShiftPhaseElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; @@ -22,4 +22,9 @@ protected override double MeasureOverride(double maxDuration) { return 0; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.ShiftPhase(ChannelId, DeltaPhase); + } } diff --git a/src/Qynit.PulseGen/StackSchedule.cs b/src/Qynit.PulseGen/StackSchedule.cs index 6a9c1c8..2aa345e 100644 --- a/src/Qynit.PulseGen/StackSchedule.cs +++ b/src/Qynit.PulseGen/StackSchedule.cs @@ -3,13 +3,18 @@ using CommunityToolkit.Diagnostics; namespace Qynit.PulseGen; -internal class StackSchedule : ScheduleElement +public class StackSchedule : ScheduleElement { public ArrangeOption ArrangeOption { get; set; } public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); private HashSet? _channels; private readonly List _elements = new(); + public StackSchedule() + { + Alignment = Alignment.Stretch; + } + public void Add(ScheduleElement element) { if (element.Parent is not null) @@ -31,9 +36,6 @@ protected override double ArrangeOverride(double time, double finalDuration) _ => throw new NotImplementedException(), }; var durations = channels.ToDictionary(c => c, _ => 0.0); - Debug.Assert(DesiredDuration is not null); - var totalDuration = DesiredDuration.Value; - Debug.Assert(finalDuration >= totalDuration); foreach (var element in elements) { var elementChannels = element.Channels; @@ -43,7 +45,7 @@ protected override double ArrangeOverride(double time, double finalDuration) var innerTime = arrangeOption switch { ArrangeOption.StartToEnd => usedDuration, - ArrangeOption.EndToStart => totalDuration - usedDuration - innerDuration, + ArrangeOption.EndToStart => finalDuration - usedDuration - innerDuration, _ => throw new NotImplementedException(), }; element.Arrange(innerTime, innerDuration); @@ -54,7 +56,7 @@ protected override double ArrangeOverride(double time, double finalDuration) durations[channel] = newDuration; } } - return totalDuration; + return finalDuration; } protected override double MeasureOverride(double maxDuration) @@ -84,4 +86,12 @@ protected override double MeasureOverride(double maxDuration) } return durations.Values.Max(); } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + foreach (var element in _elements) + { + element.Render(time, phaseTrackingTransform); + } + } } diff --git a/src/Qynit.PulseGen/SwapPhaseElement.cs b/src/Qynit.PulseGen/SwapPhaseElement.cs index 2fcb6c4..51d8fb5 100644 --- a/src/Qynit.PulseGen/SwapPhaseElement.cs +++ b/src/Qynit.PulseGen/SwapPhaseElement.cs @@ -1,5 +1,5 @@ namespace Qynit.PulseGen; -internal sealed class SwapPhaseElement : ScheduleElement +public sealed class SwapPhaseElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId1, ChannelId2 }; @@ -22,4 +22,9 @@ protected override double MeasureOverride(double maxDuration) { return 0; } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + phaseTrackingTransform.SwapPhase(ChannelId1, ChannelId2, time); + } } diff --git a/src/Qynit.PulseGen/Thickness.cs b/src/Qynit.PulseGen/Thickness.cs index 5135217..c7dd4df 100644 --- a/src/Qynit.PulseGen/Thickness.cs +++ b/src/Qynit.PulseGen/Thickness.cs @@ -1,5 +1,7 @@ namespace Qynit.PulseGen; public record struct Thickness(double Start, double End) { + public Thickness(double value) : this(value, value) { } + public readonly double Total => Start + End; } From 5dd1801e9a2a397485d78bbfa47d80c25579dbc6 Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Thu, 13 Jul 2023 09:08:25 +0800 Subject: [PATCH 3/5] feat: improve scheduling * Grid columns * User specified schedule duration * `PlayElement` with flexible plateau --- examples/WaveGenDemo/Program.cs | 9 +- src/Qynit.PulseGen/GridLength.cs | 24 +++++ src/Qynit.PulseGen/GridLengthUnit.cs | 7 ++ src/Qynit.PulseGen/GridSchedule.cs | 127 ++++++++++++++++++++++++-- src/Qynit.PulseGen/PlayElement.cs | 18 +++- src/Qynit.PulseGen/ScheduleElement.cs | 69 +++++++++++--- src/Qynit.PulseGen/Thickness.cs | 4 +- 7 files changed, 229 insertions(+), 29 deletions(-) create mode 100644 src/Qynit.PulseGen/GridLength.cs create mode 100644 src/Qynit.PulseGen/GridLengthUnit.cs diff --git a/examples/WaveGenDemo/Program.cs b/examples/WaveGenDemo/Program.cs index 79a511b..04e770e 100644 --- a/examples/WaveGenDemo/Program.cs +++ b/examples/WaveGenDemo/Program.cs @@ -53,9 +53,12 @@ static void Run() where T : unmanaged, IFloatingPointIeee754 stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); - var grid = new GridSchedule() { Margin = new(0, 90e-9) }; - grid.Add(new PlayElement(ch1, new(shape, 30e-9, 100e-9), 0, 0, 0.5, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Center }); - grid.Add(new PlayElement(ch2, new(shape, 30e-9, 50e-9), 0, 0, 0.6, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Center }); + var grid = new GridSchedule() { Duration = 500e-9 }; + grid.AddColumn(GridLength.Absolute(90e-9)); + grid.AddColumn(GridLength.Star(1)); + grid.AddColumn(GridLength.Absolute(90e-9)); + grid.Add(new PlayElement(ch1, new(shape, 30e-9, 200e-9), 0, 0, 0.5, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Center }, 1, 1); + grid.Add(new PlayElement(ch2, new(shape, 30e-9, 50e-9), -250e6, 0, 0.6, 2e-9) { Alignment = Qynit.PulseGen.Alignment.Stretch, FlexiblePlateau = true }, 0, 3); stack.Add(grid); stack.Add(new ShiftFrequencyElement(ch1, -100e6)); diff --git a/src/Qynit.PulseGen/GridLength.cs b/src/Qynit.PulseGen/GridLength.cs new file mode 100644 index 0000000..1fc36a7 --- /dev/null +++ b/src/Qynit.PulseGen/GridLength.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +public readonly record struct GridLength(double Value, GridLengthUnit Unit) +{ + public static GridLength Auto => new(double.NaN, GridLengthUnit.Auto); + + public static GridLength Star(double value) + { + Guard.IsGreaterThan(value, 0); + return new(value, GridLengthUnit.Star); + } + + public static GridLength Absolute(double value) + { + Guard.IsGreaterThanOrEqualTo(value, 0); + return new(value, GridLengthUnit.Second); + } + + public bool IsAuto => Unit == GridLengthUnit.Auto; + public bool IsStar => Unit == GridLengthUnit.Star; + public bool IsAbsolute => Unit == GridLengthUnit.Second; + public bool IsValid => IsAuto || (IsStar && Value > 0) || (IsAbsolute && Value >= 0); +} diff --git a/src/Qynit.PulseGen/GridLengthUnit.cs b/src/Qynit.PulseGen/GridLengthUnit.cs new file mode 100644 index 0000000..bba4045 --- /dev/null +++ b/src/Qynit.PulseGen/GridLengthUnit.cs @@ -0,0 +1,7 @@ +namespace Qynit.PulseGen; +public enum GridLengthUnit +{ + Second, + Auto, + Star, +} diff --git a/src/Qynit.PulseGen/GridSchedule.cs b/src/Qynit.PulseGen/GridSchedule.cs index b15a039..d074593 100644 --- a/src/Qynit.PulseGen/GridSchedule.cs +++ b/src/Qynit.PulseGen/GridSchedule.cs @@ -8,50 +8,161 @@ public class GridSchedule : ScheduleElement public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); private HashSet? _channels; private readonly List _elements = new(); + private readonly List<(int Column, int Span)> _elementColumns = new(); + private readonly List _columns = new(); + private List? _minimumColumnSizes; public GridSchedule() { Alignment = Alignment.Stretch; } + public void AddColumn(GridLength length) + { + Guard.IsTrue(length.IsValid); + _columns.Add(length); + } + public void Add(ScheduleElement element) + { + Add(element, 0, 1); + } + + public void Add(ScheduleElement element, int column, int span) { if (element.Parent is not null) { ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); } + Guard.IsGreaterThanOrEqualTo(column, 0); + Guard.IsGreaterThanOrEqualTo(span, 1); _elements.Add(element); element.Parent = this; + _elementColumns.Add((column, span)); } protected override double ArrangeOverride(double time, double finalDuration) { - foreach (var element in _elements) + Debug.Assert(_minimumColumnSizes is not null); + var columnSizes = _minimumColumnSizes.ToList(); + var numColumns = _columns.Count; + var minimumDuration = columnSizes.Sum(); + ExpandColumnByRatio(columnSizes, 0, numColumns, finalDuration - minimumDuration); + var columnStarts = new List(numColumns) { 0 }; + for (var i = 1; i < numColumns; i++) + { + columnStarts.Add(columnStarts[i - 1] + columnSizes[i - 1]); + } + foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) { Debug.Assert(element.DesiredDuration is not null); - var elementDuration = (element.Alignment == Alignment.Stretch) ? finalDuration : element.DesiredDuration.Value; - Debug.Assert(elementDuration <= finalDuration); + var actualColumn = Math.Min(column, numColumns - 1); + var actualSpan = Math.Min(span, numColumns - actualColumn); + var spanDuration = Enumerable.Range(actualColumn, actualSpan).Sum(i => columnSizes[i]); + var elementDuration = (element.Alignment == Alignment.Stretch) ? spanDuration : element.DesiredDuration.Value; + var actualDuration = Math.Min(spanDuration, elementDuration); var elementTime = element.Alignment switch { Alignment.Start => 0, - Alignment.Center => (finalDuration - elementDuration) / 2, - Alignment.End => finalDuration - elementDuration, + Alignment.Center => (spanDuration - actualDuration) / 2, + Alignment.End => spanDuration - actualDuration, Alignment.Stretch => 0, _ => throw new NotImplementedException(), }; - element.Arrange(elementTime, elementDuration); + var actualTime = columnStarts[actualColumn] + elementTime; + element.Arrange(actualTime, actualDuration); } return finalDuration; } protected override double MeasureOverride(double maxDuration) { - foreach (var element in _elements) + if (_columns.Count == 0) { + _columns.Add(GridLength.Star(1)); + } + var numColumns = _columns.Count; + var columnSizes = _columns.Select(l => (l.IsAbsolute) ? l.Value : 0).ToList(); + foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) + { + var actualColumn = Math.Min(column, numColumns - 1); + var actualSpan = Math.Min(span, numColumns - actualColumn); + if (actualSpan > 1) + { + continue; + } element.Measure(maxDuration); Debug.Assert(element.DesiredDuration is not null); + var elementDuration = element.DesiredDuration.Value; + columnSizes[actualColumn] = Math.Max(columnSizes[actualColumn], elementDuration); + } + foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) + { + var actualColumn = Math.Min(column, numColumns - 1); + var actualSpan = Math.Min(span, numColumns - actualColumn); + if (actualSpan == 1) + { + continue; + } + element.Measure(maxDuration); + Debug.Assert(element.DesiredDuration is not null); + var elementDuration = element.DesiredDuration.Value; + var columnSize = Enumerable.Range(actualColumn, actualSpan).Sum(i => columnSizes[i]); + if (columnSize >= elementDuration) + { + continue; + } + var numStar = Enumerable.Range(actualColumn, actualSpan).Count(i => _columns[i].IsStar); + if (numStar == 0) + { + var numAuto = Enumerable.Range(actualColumn, actualSpan).Count(i => _columns[i].IsAuto); + if (numAuto == 0) + { + continue; + } + var autoIncrement = (elementDuration - columnSize) / numAuto; + for (var i = actualColumn; i < actualColumn + actualSpan; i++) + { + if (_columns[i].IsAuto) + { + columnSizes[i] += autoIncrement; + } + } + } + else + { + ExpandColumnByRatio(columnSizes, actualColumn, actualSpan, elementDuration - columnSize); + } + } + _minimumColumnSizes = columnSizes; + return columnSizes.Sum(); + } + + private void ExpandColumnByRatio(List columnSizes, int start, int span, double remainingDuration) + { + var indexOrderByStarRatio = Enumerable.Range(start, span) + .Where(i => _columns[i].IsStar) + .Select(i => (Ratio: columnSizes[i] / _columns[i].Value, Index: i)) + .OrderBy(x => x.Ratio) + .ToList(); + var cumulativeStar = 0.0; + for (var i = 0; i < indexOrderByStarRatio.Count; i++) + { + var nextRatio = (i + 1 < indexOrderByStarRatio.Count) ? indexOrderByStarRatio[i + 1].Ratio : double.PositiveInfinity; + var index = indexOrderByStarRatio[i].Index; + cumulativeStar += _columns[index].Value; + remainingDuration += columnSizes[index]; + var newRatio = remainingDuration / cumulativeStar; + if (newRatio < nextRatio) + { + for (var j = 0; j <= i; j++) + { + var index2 = indexOrderByStarRatio[j].Index; + columnSizes[index2] = newRatio * _columns[index2].Value; + } + break; + } } - return _elements.Count > 0 ? _elements.Max(e => e.DesiredDuration!.Value) : 0; } protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) diff --git a/src/Qynit.PulseGen/PlayElement.cs b/src/Qynit.PulseGen/PlayElement.cs index c3ba885..f0a1347 100644 --- a/src/Qynit.PulseGen/PlayElement.cs +++ b/src/Qynit.PulseGen/PlayElement.cs @@ -5,8 +5,11 @@ public sealed class PlayElement : ScheduleElement { private HashSet? _channels; public override IReadOnlySet Channels => _channels ??= new HashSet { ChannelId }; + public bool FlexiblePlateau { get; set; } public int ChannelId { get; } - public Envelope Envelope { get; } + public IPulseShape? PulseShape { get; } + public double Width { get; } + public double Plateau { get; } public double Frequency { get; } public double Phase { get; } public double Amplitude { get; } @@ -16,7 +19,9 @@ public PlayElement(int channelId, Envelope envelope, double frequency, double ph { Debug.Assert(envelope.Duration >= 0); ChannelId = channelId; - Envelope = envelope; + PulseShape = envelope.Shape; + Width = envelope.Width; + Plateau = envelope.Plateau; Frequency = frequency; Phase = phase; Amplitude = amplitude; @@ -25,16 +30,19 @@ public PlayElement(int channelId, Envelope envelope, double frequency, double ph protected override double ArrangeOverride(double time, double finalDuration) { - return Envelope.Duration; + return FlexiblePlateau ? finalDuration : (Plateau + Width); } protected override double MeasureOverride(double maxDuration) { - return Envelope.Duration; + return FlexiblePlateau ? Width : (Plateau + Width); } protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) { - phaseTrackingTransform.Play(ChannelId, Envelope, Frequency, Phase, Amplitude, DragCoefficient, time); + Debug.Assert(ActualDuration is not null); + var plateau = FlexiblePlateau ? ActualDuration.Value - Width : Plateau; + var envelope = new Envelope(PulseShape, Width, plateau); + phaseTrackingTransform.Play(ChannelId, envelope, Frequency, Phase, Amplitude, DragCoefficient, time); } } diff --git a/src/Qynit.PulseGen/ScheduleElement.cs b/src/Qynit.PulseGen/ScheduleElement.cs index 7653509..8429017 100644 --- a/src/Qynit.PulseGen/ScheduleElement.cs +++ b/src/Qynit.PulseGen/ScheduleElement.cs @@ -5,18 +5,53 @@ namespace Qynit.PulseGen; public abstract class ScheduleElement { + private double _maxDuration = double.PositiveInfinity; + private double _minDuration; + private double? _duration; + public ScheduleElement? Parent { get; internal set; } public Thickness Margin { get; set; } public Alignment Alignment { get; set; } public bool IsVisible { get; set; } = true; + public double? Duration + { + get => _duration; + set + { + if (value is not null) + { + Guard.IsGreaterThanOrEqualTo(value.Value, 0); + } + _duration = value; + } + } + public double MaxDuration + { + get => _maxDuration; + set + { + Guard.IsGreaterThanOrEqualTo(value, 0); + _maxDuration = value; + } + } + public double MinDuration + { + get => _minDuration; + set + { + Guard.IsGreaterThanOrEqualTo(value, 0); + _minDuration = value; + } + } public double? DesiredDuration { get; private set; } public double? ActualDuration { get; private set; } public double? ActualTime { get; private set; } public abstract IReadOnlySet Channels { get; } internal bool IsMeasuring { get; private set; } - public void Measure(double maxDuration) + internal double? UnclippedDesiredDuration { get; private set; } + public void Measure(double availableDuration) { - Debug.Assert(maxDuration >= 0 || double.IsPositiveInfinity(maxDuration)); + Debug.Assert(availableDuration >= 0 || double.IsPositiveInfinity(availableDuration)); if (IsMeasuring) { ThrowHelper.ThrowInvalidOperationException("Already measuring"); @@ -24,10 +59,14 @@ public void Measure(double maxDuration) IsMeasuring = true; var margin = Margin.Total; Debug.Assert(double.IsFinite(margin)); - var availableDuration = Math.Max(maxDuration - margin, 0); - var desiredDuration = MeasureOverride(availableDuration) + margin; - Debug.Assert(double.IsFinite(desiredDuration)); - DesiredDuration = Math.Max(desiredDuration, 0); + var maxDuration = Math.Clamp(Duration ?? double.PositiveInfinity, MinDuration, MaxDuration); + var minDuration = Math.Clamp(Duration ?? 0, MinDuration, MaxDuration); + var innerDuration = Math.Max(availableDuration - margin, 0); + var clampedDuration = Math.Clamp(innerDuration, minDuration, maxDuration); + var measuredDuration = MeasureOverride(clampedDuration); + Debug.Assert(double.IsFinite(measuredDuration)); + UnclippedDesiredDuration = Math.Max(measuredDuration + margin, 0); + DesiredDuration = Math.Min(Math.Clamp(measuredDuration, minDuration, maxDuration) + margin, availableDuration); IsMeasuring = false; } protected abstract double MeasureOverride(double maxDuration); @@ -35,18 +74,26 @@ public void Arrange(double time, double finalDuration) { Debug.Assert(double.IsFinite(time)); Debug.Assert(double.IsFinite(finalDuration) && finalDuration >= 0); - if (DesiredDuration is null) + if (DesiredDuration is null || UnclippedDesiredDuration is null) { ThrowHelper.ThrowInvalidOperationException("Not measured"); } - if (finalDuration < DesiredDuration) + if (finalDuration < UnclippedDesiredDuration) { - ThrowHelper.ThrowArgumentOutOfRangeException(nameof(finalDuration), finalDuration, "Final duration is less than desired duration"); + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(finalDuration), finalDuration, "Final duration is less than unclipped desired duration"); } var innerTime = time + Margin.Start; Debug.Assert(double.IsFinite(innerTime)); - var innerDuration = Math.Max(finalDuration - Margin.Total, 0); - var actualDuration = ArrangeOverride(innerTime, innerDuration); + var maxDuration = Math.Clamp(Duration ?? double.PositiveInfinity, MinDuration, MaxDuration); + var minDuration = Math.Clamp(Duration ?? 0, MinDuration, MaxDuration); + var margin = Margin.Total; + var innerDuration = Math.Max(finalDuration - margin, 0); + var clampedDuration = Math.Clamp(innerDuration, minDuration, maxDuration); + if (clampedDuration + margin < UnclippedDesiredDuration) + { + ThrowHelper.ThrowInvalidOperationException("User specified duration is less than unclipped desired duration"); + } + var actualDuration = ArrangeOverride(innerTime, clampedDuration); Debug.Assert(double.IsFinite(actualDuration)); ActualDuration = actualDuration; ActualTime = innerTime; diff --git a/src/Qynit.PulseGen/Thickness.cs b/src/Qynit.PulseGen/Thickness.cs index c7dd4df..038919b 100644 --- a/src/Qynit.PulseGen/Thickness.cs +++ b/src/Qynit.PulseGen/Thickness.cs @@ -1,7 +1,7 @@ namespace Qynit.PulseGen; -public record struct Thickness(double Start, double End) +public readonly record struct Thickness(double Start, double End) { public Thickness(double value) : this(value, value) { } - public readonly double Total => Start + End; + public double Total => Start + End; } From 87a3a79b84e69a3ba5998a958fb72eeb9cb3124e Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Thu, 13 Jul 2023 18:03:11 +0800 Subject: [PATCH 4/5] style: dotnet format --- tests/Qynit.PulseGen.Tests/PulseListTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Qynit.PulseGen.Tests/PulseListTests.cs b/tests/Qynit.PulseGen.Tests/PulseListTests.cs index 1a45728..6cbe567 100644 --- a/tests/Qynit.PulseGen.Tests/PulseListTests.cs +++ b/tests/Qynit.PulseGen.Tests/PulseListTests.cs @@ -33,7 +33,7 @@ public void Builder_Compressed() Assert.Contains(bin2, pulses.Items); Assert.Contains(bin3, pulses.Items); Assert.Equal(3, pulses.Items[bin1].Count); - Assert.Equal(1, pulses.Items[bin2].Count); + Assert.Single(pulses.Items[bin2]); Assert.Equal(2, pulses.Items[bin3].Count); } @@ -57,7 +57,7 @@ public void Builder_Compressed_Equal() var pulses = builder.Build(); // Assert - Assert.Equal(1, pulses.Items.Count); + Assert.Single(pulses.Items); Assert.Contains(bin, pulses.Items); Assert.Equal(2, pulses.Items[bin].Count); var list = pulses.Items[bin]; @@ -90,7 +90,7 @@ public void Builder_Sorted() var pulses = builder.Build(); // Assert - Assert.Equal(1, pulses.Items.Count); + Assert.Single(pulses.Items); Assert.Contains(bin, pulses.Items); Assert.Equal(3, pulses.Items[bin].Count); var list = pulses.Items[bin]; @@ -121,8 +121,8 @@ public void Builder_SecondBuild_Cleared() var pulses2 = builder.Build(); // Assert - Assert.Equal(1, pulses1.Items.Count); - Assert.Equal(0, pulses2.Items.Count); + Assert.Single(pulses1.Items); + Assert.Empty(pulses2.Items); } [Fact] From 07be6fe55a1efd4ba5ce0ed577ca1a98bff96ac9 Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Fri, 14 Jul 2023 04:18:10 +0800 Subject: [PATCH 5/5] feat: add absolute schedule and repeat element --- examples/WaveGenDemo/Program.cs | 16 +++--- src/Qynit.PulseGen/AbsoluteSchedule.cs | 50 +++++++++++++++++++ src/Qynit.PulseGen/GridSchedule.cs | 21 ++------ src/Qynit.PulseGen/RepeatElement.cs | 69 ++++++++++++++++++++++++++ src/Qynit.PulseGen/Schedule.cs | 16 ++++++ src/Qynit.PulseGen/StackSchedule.cs | 23 +++------ 6 files changed, 156 insertions(+), 39 deletions(-) create mode 100644 src/Qynit.PulseGen/AbsoluteSchedule.cs create mode 100644 src/Qynit.PulseGen/RepeatElement.cs create mode 100644 src/Qynit.PulseGen/Schedule.cs diff --git a/examples/WaveGenDemo/Program.cs b/examples/WaveGenDemo/Program.cs index 04e770e..44874ab 100644 --- a/examples/WaveGenDemo/Program.cs +++ b/examples/WaveGenDemo/Program.cs @@ -64,8 +64,12 @@ static void Run() where T : unmanaged, IFloatingPointIeee754 stack.Add(new ShiftFrequencyElement(ch1, -100e6)); stack.Add(new ShiftFrequencyElement(ch2, -250e6)); stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); - stack.Add(new PlayElement(ch1, new(null, 200e-9, 0), 0, 0, 0.5, 2e-9)); - stack.Add(new PlayElement(ch2, new(null, 100e-9, 0), 0, 0, 0.6, 2e-9)); + + var abs = new AbsoluteSchedule(); + abs.Add(new PlayElement(ch1, new(null, 200e-9, 0), 0, 0, 0.5, 2e-9), 10e-9); + abs.Add(new RepeatElement(new PlayElement(ch2, new(null, 100e-9, 0), 0, 0, 0.6, 2e-9), 2) { Spacing = 10e-9 }, 210e-9); + stack.Add(abs); + stack.Add(new SetFrequencyElement(ch1, 0)); stack.Add(new SetFrequencyElement(ch2, 0)); stack.Add(new BarrierElement(ch1, ch2) { Margin = new(15e-9) }); @@ -95,10 +99,10 @@ static void Run() where T : unmanaged, IFloatingPointIeee754 using var waveform1 = waveforms[ch1]; using var waveform2 = waveforms[ch2]; var plot = new Plot(1920, 1080); - plot.AddSignal(waveform1.DataI[^2000..].ToArray(), sampleRate, label: $"wave 1 real"); - plot.AddSignal(waveform1.DataQ[^2000..].ToArray(), sampleRate, label: $"wave 1 imag"); - plot.AddSignal(waveform2.DataI[^2000..].ToArray(), sampleRate, label: $"wave 2 real"); - plot.AddSignal(waveform2.DataQ[^2000..].ToArray(), sampleRate, label: $"wave 2 imag"); + plot.AddSignal(waveform1.DataI[^4000..].ToArray(), sampleRate, label: $"wave 1 real"); + plot.AddSignal(waveform1.DataQ[^4000..].ToArray(), sampleRate, label: $"wave 1 imag"); + plot.AddSignal(waveform2.DataI[^4000..].ToArray(), sampleRate, label: $"wave 2 real"); + plot.AddSignal(waveform2.DataQ[^4000..].ToArray(), sampleRate, label: $"wave 2 imag"); plot.Legend(); plot.SaveFig("demo2.png"); } diff --git a/src/Qynit.PulseGen/AbsoluteSchedule.cs b/src/Qynit.PulseGen/AbsoluteSchedule.cs new file mode 100644 index 0000000..0b09156 --- /dev/null +++ b/src/Qynit.PulseGen/AbsoluteSchedule.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +public class AbsoluteSchedule : Schedule +{ + private readonly List _elementTimes = new(); + public void Add(ScheduleElement element) + { + Add(element, 0); + } + + public void Add(ScheduleElement element, double time) + { + if (element.Parent is not null) + { + ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); + } + if (!double.IsFinite(time)) + { + ThrowHelper.ThrowArgumentException("The time is not finite."); + } + Children.Add(element); + element.Parent = this; + _elementTimes.Add(time); + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + foreach (var (element, elementTime) in Children.Zip(_elementTimes)) + { + Debug.Assert(element.DesiredDuration is not null); + element.Arrange(elementTime, element.DesiredDuration.Value); + } + return finalDuration; + } + + protected override double MeasureOverride(double maxDuration) + { + var maxTime = 0.0; + foreach (var (element, time) in Children.Zip(_elementTimes)) + { + element.Measure(maxDuration); + Debug.Assert(element.DesiredDuration is not null); + maxTime = Math.Max(maxTime, time + element.DesiredDuration.Value); + } + return maxTime; + } +} diff --git a/src/Qynit.PulseGen/GridSchedule.cs b/src/Qynit.PulseGen/GridSchedule.cs index d074593..dcbd402 100644 --- a/src/Qynit.PulseGen/GridSchedule.cs +++ b/src/Qynit.PulseGen/GridSchedule.cs @@ -3,11 +3,8 @@ using CommunityToolkit.Diagnostics; namespace Qynit.PulseGen; -public class GridSchedule : ScheduleElement +public class GridSchedule : Schedule { - public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); - private HashSet? _channels; - private readonly List _elements = new(); private readonly List<(int Column, int Span)> _elementColumns = new(); private readonly List _columns = new(); private List? _minimumColumnSizes; @@ -36,7 +33,7 @@ public void Add(ScheduleElement element, int column, int span) } Guard.IsGreaterThanOrEqualTo(column, 0); Guard.IsGreaterThanOrEqualTo(span, 1); - _elements.Add(element); + Children.Add(element); element.Parent = this; _elementColumns.Add((column, span)); } @@ -53,7 +50,7 @@ protected override double ArrangeOverride(double time, double finalDuration) { columnStarts.Add(columnStarts[i - 1] + columnSizes[i - 1]); } - foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) + foreach (var (element, (column, span)) in Children.Zip(_elementColumns)) { Debug.Assert(element.DesiredDuration is not null); var actualColumn = Math.Min(column, numColumns - 1); @@ -83,7 +80,7 @@ protected override double MeasureOverride(double maxDuration) } var numColumns = _columns.Count; var columnSizes = _columns.Select(l => (l.IsAbsolute) ? l.Value : 0).ToList(); - foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) + foreach (var (element, (column, span)) in Children.Zip(_elementColumns)) { var actualColumn = Math.Min(column, numColumns - 1); var actualSpan = Math.Min(span, numColumns - actualColumn); @@ -96,7 +93,7 @@ protected override double MeasureOverride(double maxDuration) var elementDuration = element.DesiredDuration.Value; columnSizes[actualColumn] = Math.Max(columnSizes[actualColumn], elementDuration); } - foreach (var (element, (column, span)) in _elements.Zip(_elementColumns)) + foreach (var (element, (column, span)) in Children.Zip(_elementColumns)) { var actualColumn = Math.Min(column, numColumns - 1); var actualSpan = Math.Min(span, numColumns - actualColumn); @@ -164,12 +161,4 @@ private void ExpandColumnByRatio(List columnSizes, int start, int span, } } } - - protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) - { - foreach (var element in _elements) - { - element.Render(time, phaseTrackingTransform); - } - } } diff --git a/src/Qynit.PulseGen/RepeatElement.cs b/src/Qynit.PulseGen/RepeatElement.cs new file mode 100644 index 0000000..7beb79f --- /dev/null +++ b/src/Qynit.PulseGen/RepeatElement.cs @@ -0,0 +1,69 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +namespace Qynit.PulseGen; +public class RepeatElement : ScheduleElement +{ + public override IReadOnlySet Channels => ScheduleElement.Channels; + + public double Spacing { get; set; } + public ScheduleElement ScheduleElement { get; } + public int Count { get; } + + public RepeatElement(ScheduleElement scheduleElement, int count) + { + Guard.IsGreaterThanOrEqualTo(count, 0); + if (scheduleElement.Parent is not null) + { + ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); + } + scheduleElement.Parent = this; + ScheduleElement = scheduleElement; + Count = count; + } + + protected override double ArrangeOverride(double time, double finalDuration) + { + var n = Count; + if (n == 0) + { + return 0; + } + var spacing = Spacing; + var durationPerRepeat = (finalDuration - spacing * (n - 1)) / n; + ScheduleElement.Arrange(0, durationPerRepeat); + return finalDuration; + } + + protected override double MeasureOverride(double maxDuration) + { + var n = Count; + if (n == 0) + { + return 0; + } + var spacing = Spacing; + var durationPerRepeat = (maxDuration - spacing * (n - 1)) / n; + ScheduleElement.Measure(durationPerRepeat); + Debug.Assert(ScheduleElement.DesiredDuration is not null); + return ScheduleElement.DesiredDuration.Value * n + spacing * (n - 1); + } + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + var n = Count; + if (n == 0) + { + return; + } + var spacing = Spacing; + Debug.Assert(ActualDuration is not null); + var durationPerRepeat = (ActualDuration.Value - spacing * (n - 1)) / n; + for (var i = 0; i < n; i++) + { + var innerTime = time + i * (durationPerRepeat + spacing); + ScheduleElement.Render(innerTime, phaseTrackingTransform); + } + } +} diff --git a/src/Qynit.PulseGen/Schedule.cs b/src/Qynit.PulseGen/Schedule.cs new file mode 100644 index 0000000..f506366 --- /dev/null +++ b/src/Qynit.PulseGen/Schedule.cs @@ -0,0 +1,16 @@ +namespace Qynit.PulseGen; +public abstract class Schedule : ScheduleElement +{ + public override IReadOnlySet Channels => _channels ??= Children.SelectMany(e => e.Channels).ToHashSet(); + private HashSet? _channels; + protected List Children { get; } = new(); + + + protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) + { + foreach (var element in Children) + { + element.Render(time, phaseTrackingTransform); + } + } +} diff --git a/src/Qynit.PulseGen/StackSchedule.cs b/src/Qynit.PulseGen/StackSchedule.cs index 2aa345e..a497c13 100644 --- a/src/Qynit.PulseGen/StackSchedule.cs +++ b/src/Qynit.PulseGen/StackSchedule.cs @@ -3,12 +3,9 @@ using CommunityToolkit.Diagnostics; namespace Qynit.PulseGen; -public class StackSchedule : ScheduleElement +public class StackSchedule : Schedule { public ArrangeOption ArrangeOption { get; set; } - public override IReadOnlySet Channels => _channels ??= _elements.SelectMany(e => e.Channels).ToHashSet(); - private HashSet? _channels; - private readonly List _elements = new(); public StackSchedule() { @@ -21,7 +18,7 @@ public void Add(ScheduleElement element) { ThrowHelper.ThrowArgumentException("The element is already added to another schedule."); } - _elements.Add(element); + Children.Add(element); element.Parent = this; } @@ -31,8 +28,8 @@ protected override double ArrangeOverride(double time, double finalDuration) var arrangeOption = ArrangeOption; var elements = arrangeOption switch { - ArrangeOption.StartToEnd => _elements.AsEnumerable(), - ArrangeOption.EndToStart => _elements.AsEnumerable().Reverse(), + ArrangeOption.StartToEnd => Children.AsEnumerable(), + ArrangeOption.EndToStart => Children.AsEnumerable().Reverse(), _ => throw new NotImplementedException(), }; var durations = channels.ToDictionary(c => c, _ => 0.0); @@ -64,8 +61,8 @@ protected override double MeasureOverride(double maxDuration) var channels = Channels; var elements = ArrangeOption switch { - ArrangeOption.StartToEnd => _elements.AsEnumerable(), - ArrangeOption.EndToStart => _elements.AsEnumerable().Reverse(), + ArrangeOption.StartToEnd => Children.AsEnumerable(), + ArrangeOption.EndToStart => Children.AsEnumerable().Reverse(), _ => throw new NotImplementedException(), }; var durations = channels.ToDictionary(c => c, _ => 0.0); @@ -86,12 +83,4 @@ protected override double MeasureOverride(double maxDuration) } return durations.Values.Max(); } - - protected override void RenderOverride(double time, PhaseTrackingTransform phaseTrackingTransform) - { - foreach (var element in _elements) - { - element.Render(time, phaseTrackingTransform); - } - } }