From 5070ce2fc80293e926ddc09f3e5d8d97cdf0a04d Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Sun, 2 Jul 2023 00:59:20 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E2=9A=A1envelope=20caching=20with=20`M?= =?UTF-8?q?emoryCache`=20and=20waveform=20utils=20improvements=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: :zap: more specialized waveform mixing * perf: :zap: envelope caching with `MemoryCache` --- Qynit.Pulsewave.sln | 9 +- ...hmarks.WaveformUtilsBench-report-github.md | 43 +++ examples/WaveGenBenchmarks/Program.cs | 5 + .../WaveGenBenchmarks.csproj | 18 + .../WaveGenBenchmarks/WaveformUtilsBench.cs | 97 +++++ .../ComplexArrayReadOnlySpan.cs | 44 --- src/Qynit.Pulsewave/ComplexArraySpan.cs | 39 -- src/Qynit.Pulsewave/ComplexReadOnlySpan.cs | 44 +++ src/Qynit.Pulsewave/ComplexSpan.cs | 49 +++ src/Qynit.Pulsewave/Envelope.cs | 2 + src/Qynit.Pulsewave/EnvelopeCacheKey.cs | 2 + src/Qynit.Pulsewave/EnvelopeSample.cs | 40 ++ src/Qynit.Pulsewave/HannPulseShape.cs | 2 +- src/Qynit.Pulsewave/IPulseShape.cs | 2 +- src/Qynit.Pulsewave/PooledComplexArray.cs | 14 +- src/Qynit.Pulsewave/Qynit.Pulsewave.csproj | 1 + src/Qynit.Pulsewave/Waveform.cs | 4 +- src/Qynit.Pulsewave/WaveformSampler.cs | 92 +++++ src/Qynit.Pulsewave/WaveformUtils.cs | 362 ++++++++++++++++-- ...anTests.cs => ComplexReadOnlySpanTests.cs} | 24 +- .../WaveformUtilsTests.cs | 255 +++++++++--- 21 files changed, 947 insertions(+), 201 deletions(-) create mode 100644 WaveGenBenchmarks.WaveformUtilsBench-report-github.md create mode 100644 examples/WaveGenBenchmarks/Program.cs create mode 100644 examples/WaveGenBenchmarks/WaveGenBenchmarks.csproj create mode 100644 examples/WaveGenBenchmarks/WaveformUtilsBench.cs delete mode 100644 src/Qynit.Pulsewave/ComplexArrayReadOnlySpan.cs delete mode 100644 src/Qynit.Pulsewave/ComplexArraySpan.cs create mode 100644 src/Qynit.Pulsewave/ComplexReadOnlySpan.cs create mode 100644 src/Qynit.Pulsewave/ComplexSpan.cs create mode 100644 src/Qynit.Pulsewave/EnvelopeCacheKey.cs create mode 100644 src/Qynit.Pulsewave/EnvelopeSample.cs create mode 100644 src/Qynit.Pulsewave/WaveformSampler.cs rename tests/Qynit.Pulsewave.Tests/{ComplexArrayReadOnlySpanTests.cs => ComplexReadOnlySpanTests.cs} (74%) diff --git a/Qynit.Pulsewave.sln b/Qynit.Pulsewave.sln index adcaef0..5647796 100644 --- a/Qynit.Pulsewave.sln +++ b/Qynit.Pulsewave.sln @@ -18,7 +18,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{3B8B8B3E EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{6741804E-962A-4762-B289-CD4899A62AD8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WaveGenDemo", "examples\WaveGenDemo\WaveGenDemo.csproj", "{B83CB224-4C00-4011-BF6A-C76C7D786908}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WaveGenDemo", "examples\WaveGenDemo\WaveGenDemo.csproj", "{B83CB224-4C00-4011-BF6A-C76C7D786908}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WaveGenBenchmarks", "examples\WaveGenBenchmarks\WaveGenBenchmarks.csproj", "{ECE2579D-700C-4BB3-9009-F35395177B7A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +40,10 @@ Global {B83CB224-4C00-4011-BF6A-C76C7D786908}.Debug|Any CPU.Build.0 = Debug|Any CPU {B83CB224-4C00-4011-BF6A-C76C7D786908}.Release|Any CPU.ActiveCfg = Release|Any CPU {B83CB224-4C00-4011-BF6A-C76C7D786908}.Release|Any CPU.Build.0 = Release|Any CPU + {ECE2579D-700C-4BB3-9009-F35395177B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECE2579D-700C-4BB3-9009-F35395177B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECE2579D-700C-4BB3-9009-F35395177B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECE2579D-700C-4BB3-9009-F35395177B7A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +52,7 @@ Global {8A71F7BA-E700-4DA3-BBE7-5E7C866466B8} = {3EBAD22F-230A-4BCB-B16C-A9063E2A4E9A} {33659EDC-29F9-45DB-A9FA-E01E648BE4B9} = {3B8B8B3E-9095-4A7E-9309-661439C30D9A} {B83CB224-4C00-4011-BF6A-C76C7D786908} = {6741804E-962A-4762-B289-CD4899A62AD8} + {ECE2579D-700C-4BB3-9009-F35395177B7A} = {6741804E-962A-4762-B289-CD4899A62AD8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1DC52AEE-D9FC-4B2F-9492-0C92B0479A3C} diff --git a/WaveGenBenchmarks.WaveformUtilsBench-report-github.md b/WaveGenBenchmarks.WaveformUtilsBench-report-github.md new file mode 100644 index 0000000..2b8d4b5 --- /dev/null +++ b/WaveGenBenchmarks.WaveformUtilsBench-report-github.md @@ -0,0 +1,43 @@ +``` ini + +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2) +AMD Ryzen 5 5600, 1 CPU, 12 logical and 6 physical cores +.NET SDK=7.0.304 + [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + + +``` +| Method | Length | Mean | Error | StdDev | Median | Ratio | RatioSD | +|------------------------ |------- |------------:|----------:|----------:|------------:|------:|--------:| +| **MixAddPlateau** | **16** | **12.94 ns** | **0.027 ns** | **0.023 ns** | **12.95 ns** | **0.25** | **0.00** | +| MixAddPlateauFrequency | 16 | 28.13 ns | 0.090 ns | 0.080 ns | 28.13 ns | 0.54 | 0.00 | +| MixAdd | 16 | 20.65 ns | 0.047 ns | 0.042 ns | 20.66 ns | 0.40 | 0.00 | +| MixAddFrequency | 16 | 38.46 ns | 0.113 ns | 0.088 ns | 38.49 ns | 0.74 | 0.00 | +| MixAddWithDrag | 16 | 29.16 ns | 0.143 ns | 0.134 ns | 29.16 ns | 0.56 | 0.00 | +| MixAddFrequencyWithDrag | 16 | 51.62 ns | 0.134 ns | 0.119 ns | 51.61 ns | 1.00 | 0.00 | +| Simple | 16 | 63.72 ns | 0.322 ns | 0.301 ns | 63.59 ns | 1.23 | 0.01 | +| | | | | | | | | +| **MixAddPlateau** | **64** | **42.69 ns** | **0.113 ns** | **0.105 ns** | **42.71 ns** | **0.53** | **0.01** | +| MixAddPlateauFrequency | 64 | 46.73 ns | 0.218 ns | 0.193 ns | 46.74 ns | 0.58 | 0.01 | +| MixAdd | 64 | 37.26 ns | 0.101 ns | 0.095 ns | 37.27 ns | 0.46 | 0.00 | +| MixAddFrequency | 64 | 62.61 ns | 0.192 ns | 0.160 ns | 62.61 ns | 0.78 | 0.01 | +| MixAddWithDrag | 64 | 51.32 ns | 0.466 ns | 0.363 ns | 51.19 ns | 0.64 | 0.01 | +| MixAddFrequencyWithDrag | 64 | 80.17 ns | 0.779 ns | 0.691 ns | 80.01 ns | 1.00 | 0.00 | +| Simple | 64 | 217.39 ns | 4.377 ns | 7.892 ns | 213.14 ns | 2.78 | 0.13 | +| | | | | | | | | +| **MixAddPlateau** | **256** | **72.68 ns** | **0.321 ns** | **0.300 ns** | **72.73 ns** | **0.37** | **0.00** | +| MixAddPlateauFrequency | 256 | 122.84 ns | 0.210 ns | 0.197 ns | 122.80 ns | 0.62 | 0.00 | +| MixAdd | 256 | 74.62 ns | 0.266 ns | 0.249 ns | 74.60 ns | 0.38 | 0.00 | +| MixAddFrequency | 256 | 163.94 ns | 0.327 ns | 0.306 ns | 164.00 ns | 0.83 | 0.01 | +| MixAddWithDrag | 256 | 141.93 ns | 0.305 ns | 0.285 ns | 141.89 ns | 0.72 | 0.00 | +| MixAddFrequencyWithDrag | 256 | 196.62 ns | 1.133 ns | 1.060 ns | 196.16 ns | 1.00 | 0.00 | +| Simple | 256 | 782.64 ns | 4.104 ns | 3.839 ns | 781.45 ns | 3.98 | 0.03 | +| | | | | | | | | +| **MixAddPlateau** | **1024** | **277.91 ns** | **0.331 ns** | **0.294 ns** | **277.91 ns** | **0.40** | **0.00** | +| MixAddPlateauFrequency | 1024 | 424.67 ns | 0.439 ns | 0.389 ns | 424.60 ns | 0.61 | 0.00 | +| MixAdd | 1024 | 274.94 ns | 1.182 ns | 1.048 ns | 275.11 ns | 0.39 | 0.00 | +| MixAddFrequency | 1024 | 566.09 ns | 1.301 ns | 1.086 ns | 566.40 ns | 0.81 | 0.00 | +| MixAddWithDrag | 1024 | 534.09 ns | 2.571 ns | 2.405 ns | 533.32 ns | 0.76 | 0.00 | +| MixAddFrequencyWithDrag | 1024 | 701.37 ns | 2.718 ns | 2.270 ns | 701.43 ns | 1.00 | 0.00 | +| Simple | 1024 | 3,061.59 ns | 12.202 ns | 10.189 ns | 3,060.17 ns | 4.37 | 0.02 | diff --git a/examples/WaveGenBenchmarks/Program.cs b/examples/WaveGenBenchmarks/Program.cs new file mode 100644 index 0000000..69ecc1c --- /dev/null +++ b/examples/WaveGenBenchmarks/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; + +using WaveGenBenchmarks; + +BenchmarkRunner.Run(); diff --git a/examples/WaveGenBenchmarks/WaveGenBenchmarks.csproj b/examples/WaveGenBenchmarks/WaveGenBenchmarks.csproj new file mode 100644 index 0000000..0bb41d1 --- /dev/null +++ b/examples/WaveGenBenchmarks/WaveGenBenchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + diff --git a/examples/WaveGenBenchmarks/WaveformUtilsBench.cs b/examples/WaveGenBenchmarks/WaveformUtilsBench.cs new file mode 100644 index 0000000..839d95d --- /dev/null +++ b/examples/WaveGenBenchmarks/WaveformUtilsBench.cs @@ -0,0 +1,97 @@ +using System.Numerics; + +using BenchmarkDotNet.Attributes; + +using Qynit.Pulsewave; + +namespace WaveGenBenchmarks; +public class WaveformUtilsBench +{ + [Params(16, 64, 256, 1024)] + public int Length { get; set; } + private PooledComplexArray? Source { get; set; } + private PooledComplexArray? Target { get; set; } + private IqPair Amplitude { get; } = new IqPair(1, 1); + private IqPair DragAmplitude { get; } = new IqPair(1, 1); + private double DPhase { get; } = Math.PI / 11; + + [GlobalSetup] + public void Init() + { + Source = new PooledComplexArray(Length, true); + Source.DataI.Fill(0.125); + Source.DataQ.Fill(-0.125); + Target = new PooledComplexArray(Length, true); + } + + [Benchmark] + public void MixAddPlateau() + { + WaveformUtils.MixAddPlateau(Target!, Amplitude); + } + + [Benchmark] + public void MixAddPlateauFrequency() + { + WaveformUtils.MixAddPlateauFrequency(Target!, Amplitude, DPhase); + } + + [Benchmark] + public void MixAdd() + { + WaveformUtils.MixAdd(Target!, Source!, Amplitude); + } + + [Benchmark] + public void MixAddFrequency() + { + WaveformUtils.MixAddFrequency(Target!, Source!, Amplitude, DPhase); + } + + [Benchmark] + public void MixAddWithDrag() + { + WaveformUtils.MixAddWithDrag(Target!, Source!, Amplitude, DragAmplitude); + } + + [Benchmark(Baseline = true)] + public void MixAddFrequencyWithDrag() + { + WaveformUtils.MixAddFrequencyWithDrag(Target!, Source!, Amplitude, DragAmplitude, DPhase); + } + + [Benchmark] + public void Simple() + { + MixAddWithDragSimple(Target!, Source!, Amplitude, DragAmplitude, DPhase); + } + + private static void MixAddWithDragSimple(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + var length = source.Length; + var sourceI = source.DataI; + var sourceQ = source.DataQ; + var targetI = target.DataI; + var targetQ = target.DataQ; + + var carrier = amplitude; + var dragCarrier = dragAmplitude; + var phaser = IqPair.FromPolarCoordinates(T.One, dPhase); + for (var i = 0; i < length; i++) + { + var diff = i switch + { + 0 => new IqPair(sourceI[i + 1], sourceQ[i + 1]) - new IqPair(sourceI[i], sourceQ[i]), + _ when i == length - 1 => new IqPair(sourceI[i], sourceQ[i]) - new IqPair(sourceI[i - 1], sourceQ[i - 1]), + _ => (new IqPair(sourceI[i + 1], sourceQ[i + 1]) - new IqPair(sourceI[i - 1], sourceQ[i - 1])) * T.CreateChecked(0.5), + }; + var sourceIq = new IqPair(sourceI[i], sourceQ[i]); + var totalIq = sourceIq * carrier + diff * dragCarrier; + targetI[i] += totalIq.I; + targetQ[i] += totalIq.Q; + carrier *= phaser; + dragCarrier *= phaser; + } + } +} diff --git a/src/Qynit.Pulsewave/ComplexArrayReadOnlySpan.cs b/src/Qynit.Pulsewave/ComplexArrayReadOnlySpan.cs deleted file mode 100644 index b04ed24..0000000 --- a/src/Qynit.Pulsewave/ComplexArrayReadOnlySpan.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Qynit.Pulsewave; -public readonly ref struct ComplexArrayReadOnlySpan - where T : unmanaged -{ - public ReadOnlySpan DataI { get; } - public ReadOnlySpan DataQ { get; } - public int Length => DataI.Length; - public bool IsEmpty => Length == 0; - internal ComplexArrayReadOnlySpan(ReadOnlySpan dataI, ReadOnlySpan dataQ) - { - Debug.Assert(dataI.Length == dataQ.Length); - DataI = dataI; - DataQ = dataQ; - } - public static implicit operator ComplexArrayReadOnlySpan(PooledComplexArray source) - { - return new ComplexArrayReadOnlySpan(source.DataI, source.DataQ); - } - public static implicit operator ComplexArrayReadOnlySpan(ComplexArraySpan source) - { - return new ComplexArrayReadOnlySpan(source.DataI, source.DataQ); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArrayReadOnlySpan Slice(int start) - { - return new ComplexArrayReadOnlySpan(DataI[start..], DataQ[start..]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArrayReadOnlySpan Slice(int start, int length) - { - return new ComplexArrayReadOnlySpan(DataI.Slice(start, length), DataQ.Slice(start, length)); - } - - public void CopyTo(ComplexArraySpan destination) - { - DataI.CopyTo(destination.DataI); - DataQ.CopyTo(destination.DataQ); - } -} diff --git a/src/Qynit.Pulsewave/ComplexArraySpan.cs b/src/Qynit.Pulsewave/ComplexArraySpan.cs deleted file mode 100644 index f57924b..0000000 --- a/src/Qynit.Pulsewave/ComplexArraySpan.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Qynit.Pulsewave; -public readonly ref struct ComplexArraySpan - where T : unmanaged -{ - public Span DataI { get; } - public Span DataQ { get; } - public int Length => DataI.Length; - public bool IsEmpty => Length == 0; - internal ComplexArraySpan(Span dataI, Span dataQ) - { - Debug.Assert(dataI.Length == dataQ.Length); - DataI = dataI; - DataQ = dataQ; - } - public static implicit operator ComplexArraySpan(PooledComplexArray source) - { - return new ComplexArraySpan(source.DataI, source.DataQ); - } - - public void CopyTo(ComplexArraySpan destination) - { - ((ComplexArrayReadOnlySpan)this).CopyTo(destination); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArraySpan Slice(int start) - { - return new ComplexArraySpan(DataI[start..], DataQ[start..]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArraySpan Slice(int start, int length) - { - return new ComplexArraySpan(DataI.Slice(start, length), DataQ.Slice(start, length)); - } -} diff --git a/src/Qynit.Pulsewave/ComplexReadOnlySpan.cs b/src/Qynit.Pulsewave/ComplexReadOnlySpan.cs new file mode 100644 index 0000000..bdee6a4 --- /dev/null +++ b/src/Qynit.Pulsewave/ComplexReadOnlySpan.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Qynit.Pulsewave; +public readonly ref struct ComplexReadOnlySpan + where T : unmanaged +{ + public ReadOnlySpan DataI { get; } + public ReadOnlySpan DataQ { get; } + public int Length => DataI.Length; + public bool IsEmpty => Length == 0; + internal ComplexReadOnlySpan(ReadOnlySpan dataI, ReadOnlySpan dataQ) + { + Debug.Assert(dataI.Length == dataQ.Length); + DataI = dataI; + DataQ = dataQ; + } + public static implicit operator ComplexReadOnlySpan(PooledComplexArray source) + { + return new ComplexReadOnlySpan(source.DataI, source.DataQ); + } + public static implicit operator ComplexReadOnlySpan(ComplexSpan source) + { + return new ComplexReadOnlySpan(source.DataI, source.DataQ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComplexReadOnlySpan Slice(int start) + { + return new ComplexReadOnlySpan(DataI[start..], DataQ[start..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComplexReadOnlySpan Slice(int start, int length) + { + return new ComplexReadOnlySpan(DataI.Slice(start, length), DataQ.Slice(start, length)); + } + + public void CopyTo(ComplexSpan destination) + { + DataI.CopyTo(destination.DataI); + DataQ.CopyTo(destination.DataQ); + } +} diff --git a/src/Qynit.Pulsewave/ComplexSpan.cs b/src/Qynit.Pulsewave/ComplexSpan.cs new file mode 100644 index 0000000..34667f6 --- /dev/null +++ b/src/Qynit.Pulsewave/ComplexSpan.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Qynit.Pulsewave; +public readonly ref struct ComplexSpan + where T : unmanaged +{ + public Span DataI { get; } + public Span DataQ { get; } + public int Length => DataI.Length; + public bool IsEmpty => Length == 0; + internal ComplexSpan(Span dataI, Span dataQ) + { + Debug.Assert(dataI.Length == dataQ.Length); + DataI = dataI; + DataQ = dataQ; + } + public static implicit operator ComplexSpan(PooledComplexArray source) + { + return new ComplexSpan(source.DataI, source.DataQ); + } + + public void Fill(T i, T q) + { + DataI.Fill(i); + DataQ.Fill(q); + } + public void Clear() + { + DataI.Clear(); + DataQ.Clear(); + } + public void CopyTo(ComplexSpan destination) + { + ((ComplexReadOnlySpan)this).CopyTo(destination); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComplexSpan Slice(int start) + { + return new ComplexSpan(DataI[start..], DataQ[start..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComplexSpan Slice(int start, int length) + { + return new ComplexSpan(DataI.Slice(start, length), DataQ.Slice(start, length)); + } +} diff --git a/src/Qynit.Pulsewave/Envelope.cs b/src/Qynit.Pulsewave/Envelope.cs index 55288f9..0afb5c6 100644 --- a/src/Qynit.Pulsewave/Envelope.cs +++ b/src/Qynit.Pulsewave/Envelope.cs @@ -6,6 +6,8 @@ public readonly record struct Envelope public IPulseShape? Shape { get; } public double Width { get; } public double Plateau { get; } + public bool IsRectangle => Shape is null; + public bool IsZero => Width == 0 && Plateau == 0; public Envelope(IPulseShape? shape, double width, double plateau) { Guard.IsGreaterThanOrEqualTo(width, 0); diff --git a/src/Qynit.Pulsewave/EnvelopeCacheKey.cs b/src/Qynit.Pulsewave/EnvelopeCacheKey.cs new file mode 100644 index 0000000..a46cde2 --- /dev/null +++ b/src/Qynit.Pulsewave/EnvelopeCacheKey.cs @@ -0,0 +1,2 @@ +namespace Qynit.Pulsewave; +internal record EnvelopeCacheKey(EnvelopeInfo EnvelopeInfo, Envelope Envelope); diff --git a/src/Qynit.Pulsewave/EnvelopeSample.cs b/src/Qynit.Pulsewave/EnvelopeSample.cs new file mode 100644 index 0000000..96db280 --- /dev/null +++ b/src/Qynit.Pulsewave/EnvelopeSample.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; + +namespace Qynit.Pulsewave; +internal sealed record EnvelopeSample : IDisposable + where T : unmanaged +{ + public PooledComplexArray? LeftEdge { get; } + public PooledComplexArray? RightEdge { get; } + public int Plateau { get; } + public int Size => (LeftEdge?.Length ?? 0) + (RightEdge?.Length ?? 0); + + private EnvelopeSample(PooledComplexArray? leftEdge, PooledComplexArray? rightEdge, int plateau) + { + LeftEdge = leftEdge; + RightEdge = rightEdge; + Plateau = plateau; + } + + public void Dispose() + { + LeftEdge?.Dispose(); + RightEdge?.Dispose(); + } + + public static EnvelopeSample? Rectangle(int plateau) + { + return (plateau > 0) ? new EnvelopeSample(null, null, plateau) : null; + } + + public static EnvelopeSample Continuous(PooledComplexArray envelope) + { + return new EnvelopeSample(envelope, null, 0); + } + + public static EnvelopeSample WithPlateau(PooledComplexArray leftEdge, PooledComplexArray rightEdge, int plateau) + { + Debug.Assert(plateau > 0); + return new EnvelopeSample(leftEdge, rightEdge, plateau); + } +} diff --git a/src/Qynit.Pulsewave/HannPulseShape.cs b/src/Qynit.Pulsewave/HannPulseShape.cs index 242e7b4..6d1d0be 100644 --- a/src/Qynit.Pulsewave/HannPulseShape.cs +++ b/src/Qynit.Pulsewave/HannPulseShape.cs @@ -13,7 +13,7 @@ public IqPair SampleAt(T x) return i; } - public void SampleIQ(ComplexArraySpan target, T x0, T dx) + public void SampleIQ(ComplexSpan target, T x0, T dx) where T : unmanaged, IFloatingPointIeee754 { var length = target.Length; diff --git a/src/Qynit.Pulsewave/IPulseShape.cs b/src/Qynit.Pulsewave/IPulseShape.cs index 89f1e75..a51fad9 100644 --- a/src/Qynit.Pulsewave/IPulseShape.cs +++ b/src/Qynit.Pulsewave/IPulseShape.cs @@ -17,7 +17,7 @@ public interface IPulseShape IqPair SampleAt(T x) where T : unmanaged, IFloatingPointIeee754; - void SampleIQ(ComplexArraySpan target, T x0, T dx) + void SampleIQ(ComplexSpan target, T x0, T dx) where T : unmanaged, IFloatingPointIeee754 { var length = target.Length; diff --git a/src/Qynit.Pulsewave/PooledComplexArray.cs b/src/Qynit.Pulsewave/PooledComplexArray.cs index 4c664ec..13b8ad0 100644 --- a/src/Qynit.Pulsewave/PooledComplexArray.cs +++ b/src/Qynit.Pulsewave/PooledComplexArray.cs @@ -45,7 +45,7 @@ public PooledComplexArray(int length, bool clear) Clear(); } } - public PooledComplexArray(ComplexArrayReadOnlySpan source) : this(source.Length, false) + public PooledComplexArray(ComplexReadOnlySpan source) : this(source.Length, false) { source.DataI.CopyTo(_dataI); source.DataQ.CopyTo(_dataQ); @@ -55,19 +55,19 @@ public PooledComplexArray Copy() { return new PooledComplexArray(this); } - public void CopyTo(ComplexArraySpan destination) + public void CopyTo(ComplexSpan destination) { - ((ComplexArrayReadOnlySpan)this).CopyTo(destination); + ((ComplexReadOnlySpan)this).CopyTo(destination); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArraySpan Slice(int start) + public ComplexSpan Slice(int start) { - return new ComplexArraySpan(DataI[start..], DataQ[start..]); + return new ComplexSpan(DataI[start..], DataQ[start..]); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComplexArraySpan Slice(int start, int length) + public ComplexSpan Slice(int start, int length) { - return new ComplexArraySpan(DataI.Slice(start, length), DataQ.Slice(start, length)); + return new ComplexSpan(DataI.Slice(start, length), DataQ.Slice(start, length)); } public void Clear() { diff --git a/src/Qynit.Pulsewave/Qynit.Pulsewave.csproj b/src/Qynit.Pulsewave/Qynit.Pulsewave.csproj index 2f6a55e..a56e2c3 100644 --- a/src/Qynit.Pulsewave/Qynit.Pulsewave.csproj +++ b/src/Qynit.Pulsewave/Qynit.Pulsewave.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Qynit.Pulsewave/Waveform.cs b/src/Qynit.Pulsewave/Waveform.cs index db746bb..eddd0d8 100644 --- a/src/Qynit.Pulsewave/Waveform.cs +++ b/src/Qynit.Pulsewave/Waveform.cs @@ -14,7 +14,7 @@ public sealed class Waveform : IDisposable public int Length => _array.Length; public double Dt => 1 / SampleRate; public double TStart => IndexStart * Dt; - public ComplexArraySpan Array => _array; + public ComplexSpan Array => _array; private readonly PooledComplexArray _array; private bool _shouldDispose; @@ -26,7 +26,7 @@ public Waveform(Waveform source) : this(source.IndexStart, source.Length, sou source._array.CopyTo(_array); } - public Waveform(ComplexArrayReadOnlySpan source, int indexStart, double sampleRate) : this(indexStart, source.Length, sampleRate, false) + public Waveform(ComplexReadOnlySpan source, int indexStart, double sampleRate) : this(indexStart, source.Length, sampleRate, false) { source.CopyTo(_array); } diff --git a/src/Qynit.Pulsewave/WaveformSampler.cs b/src/Qynit.Pulsewave/WaveformSampler.cs new file mode 100644 index 0000000..076b920 --- /dev/null +++ b/src/Qynit.Pulsewave/WaveformSampler.cs @@ -0,0 +1,92 @@ +using System.Diagnostics; +using System.Numerics; + +using Microsoft.Extensions.Caching.Memory; + +namespace Qynit.Pulsewave; +public static class WaveformSampler + where T : unmanaged, IFloatingPointIeee754 +{ + private const int PlateauThreshold = 128; + + private static readonly IMemoryCache MemoryCache; + + static WaveformSampler() + { + var options = new MemoryCacheOptions + { + SizeLimit = 16 * 1024 * 1024, + }; + MemoryCache = new MemoryCache(options); + } + + internal static EnvelopeSample? GetEnvelopeSample(EnvelopeInfo envelopeInfo, Envelope envelope) + { + var sampleRate = envelopeInfo.SampleRate; + var shape = envelope.Shape; + if (shape is null) + { + Debug.Assert(envelope.Width == 0); + var rectLength = TimeAxisUtils.NextIndex(envelope.Plateau, sampleRate); + return EnvelopeSample.Rectangle(rectLength); + } + var dt = 1 / sampleRate; + var tOffset = envelopeInfo.IndexOffset * dt; + var width = envelope.Width; + var plateau = envelope.Plateau; + var t1 = width / 2 - tOffset; + var t2 = width / 2 + plateau - tOffset; + var t3 = width + plateau - tOffset; + var length = TimeAxisUtils.NextIndex(t3, sampleRate); + if (length == 0) + { + return null; + } + + var key = new EnvelopeCacheKey(envelopeInfo, envelope); + if (MemoryCache.TryGetValue>(key, out var cachedValue)) + { + return cachedValue; + } + var plateauStartIndex = TimeAxisUtils.NextIndex(t1, sampleRate); + var plateauEndIndex = TimeAxisUtils.NextIndex(t2, sampleRate); + var plateauLength = plateauEndIndex - plateauStartIndex; + var xStep = T.CreateChecked(dt / width); + EnvelopeSample envelopeSample; + if (plateauLength < PlateauThreshold) + { + var array = new PooledComplexArray(length, false); + var x0 = T.CreateChecked(-t1 / width); + if (plateau == 0) + { + shape.SampleIQ(array, x0, xStep); + } + else + { + shape.SampleIQ(array[..plateauStartIndex], x0, xStep); + array[plateauStartIndex..plateauEndIndex].Fill(T.One, T.Zero); + var x2 = T.CreateChecked((plateauEndIndex * dt - t2) / width); + shape.SampleIQ(array[plateauEndIndex..], x2, xStep); + } + envelopeSample = EnvelopeSample.Continuous(array); + } + else + { + var leftLength = plateauStartIndex; + var rightLength = length - plateauEndIndex; + var leftArray = new PooledComplexArray(leftLength, false); + var rightArray = new PooledComplexArray(rightLength, false); + var x0 = T.CreateChecked(-t1 / width); + shape.SampleIQ(leftArray, x0, xStep); + var x2 = T.CreateChecked((plateauEndIndex * dt - t2) / width); + shape.SampleIQ(rightArray, x2, xStep); + envelopeSample = EnvelopeSample.WithPlateau(leftArray, rightArray, plateauLength); + } + var cacheEntryOptions = new MemoryCacheEntryOptions + { + Size = envelopeSample.Size, + }; + MemoryCache.Set(key, envelopeSample, cacheEntryOptions); + return envelopeSample; + } +} diff --git a/src/Qynit.Pulsewave/WaveformUtils.cs b/src/Qynit.Pulsewave/WaveformUtils.cs index f278f46..42900c4 100644 --- a/src/Qynit.Pulsewave/WaveformUtils.cs +++ b/src/Qynit.Pulsewave/WaveformUtils.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using CommunityToolkit.Diagnostics; + namespace Qynit.Pulsewave; public static class WaveformUtils { @@ -50,35 +52,169 @@ internal static PooledComplexArray SampleWaveform(PulseList pulseList, foreach (var pulse in bin) { var tStart = pulse.Time + pulseList.TimeOffset; - var shape = binKey.Envelope.Shape!; - var width = binKey.Envelope.Width; - var plateau = binKey.Envelope.Plateau; - var frequency = binKey.Frequency; var iFracStart = TimeAxisUtils.NextFracIndex(tStart, sampleRate, alignLevel); var iStart = (int)Math.Ceiling(iFracStart); var envelopeInfo = new EnvelopeInfo(iStart - iFracStart, sampleRate); - using var envelope = SampleEnvelope(envelopeInfo, shape, width, plateau); + var envelopeSample = WaveformSampler.GetEnvelopeSample(envelopeInfo, binKey.Envelope); + if (envelopeSample is null) + { + continue; + } + + var frequency = binKey.Frequency; var dt = 1 / sampleRate; - var amplitude = pulse.Amplitude * pulseList.AmplitudeMultiplier * IqPair.FromPolarCoordinates(T.One, T.CreateChecked((iStart * dt - tStart) * frequency * Math.Tau)); + var phaseShift = T.CreateChecked(Math.Tau * frequency * (iStart * dt - tStart)); + var amplitude = pulse.Amplitude * pulseList.AmplitudeMultiplier * IqPair.FromPolarCoordinates(T.One, phaseShift); var complexAmplitude = amplitude.Amplitude; - var dPhase = T.CreateChecked(Math.Tau * frequency * dt); - var arrayIStart = iStart; var dragAmplitude = amplitude.DragAmplitude; - if (dragAmplitude.I == T.Zero && dragAmplitude.Q == T.Zero) - { - MixAddFrequency(waveform[arrayIStart..], envelope, complexAmplitude, dPhase); - } - else - { - var drag = dragAmplitude * T.CreateChecked(sampleRate); - MixAddFrequencyWithDrag(waveform[arrayIStart..], envelope, complexAmplitude, drag, dPhase); - } + var dPhase = T.CreateChecked(Math.Tau * frequency * dt); + MixAddEnvelope(waveform[iStart..], envelopeSample, complexAmplitude, dragAmplitude, dPhase); } } return waveform; } - public static void MixAddFrequency(ComplexArraySpan target, ComplexArrayReadOnlySpan source, IqPair amplitude, T dPhase) + private static void MixAddEnvelope(ComplexSpan target, EnvelopeSample envelopeSample, IqPair complexAmplitude, IqPair dragAmplitude, T dPhase) where T : unmanaged, IFloatingPointIeee754 + { + var currentIndex = 0; + var leftArray = envelopeSample.LeftEdge; + if (leftArray is not null) + { + MixAddGeneral(target[currentIndex..], leftArray, complexAmplitude, dragAmplitude, dPhase); + currentIndex += leftArray.Length; + } + if (envelopeSample.Plateau > 0) + { + MixAddPlateauGeneral(target.Slice(currentIndex, envelopeSample.Plateau), complexAmplitude, dPhase); + currentIndex += envelopeSample.Plateau; + } + var rightArray = envelopeSample.RightEdge; + if (rightArray is not null) + { + MixAddGeneral(target[currentIndex..], rightArray, complexAmplitude, dragAmplitude, dPhase); + } + } + + public static void MixAddPlateauGeneral(ComplexSpan target, IqPair amplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + if (dPhase == T.Zero) + { + MixAddPlateau(target, amplitude); + } + else + { + MixAddPlateauFrequency(target, amplitude, dPhase); + } + } + + public static void MixAddGeneral(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + switch ((dPhase == T.Zero, dragAmplitude == IqPair.Zero)) + { + case (true, true): + MixAdd(target, source, amplitude); + break; + case (true, false): + MixAddWithDrag(target, source, amplitude, dragAmplitude); + break; + case (false, true): + MixAddFrequency(target, source, amplitude, dPhase); + break; + case (false, false): + MixAddFrequencyWithDrag(target, source, amplitude, dragAmplitude, dPhase); + break; + } + } + + public static void MixAddPlateau(ComplexSpan target, IqPair amplitude) + where T : unmanaged, IFloatingPointIeee754 + { + var length = target.Length; + + var i = 0; + ref var targetI = ref MemoryMarshal.GetReference(target.DataI); + ref var targetQ = ref MemoryMarshal.GetReference(target.DataQ); + var vSize = Vector.Count; + + if (Vector.IsHardwareAccelerated && length >= 2 * vSize) + { + var amplitudeVectorI = new Vector(amplitude.I); + var amplitudeVectorQ = new Vector(amplitude.Q); + + for (; i < length - vSize + 1; i += vSize) + { + ref var targetVectorI = ref Unsafe.As>(ref Unsafe.Add(ref targetI, i)); + ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); + targetVectorI += amplitudeVectorI; + targetVectorQ += amplitudeVectorQ; + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref targetI, i) += amplitude.I; + Unsafe.Add(ref targetQ, i) += amplitude.Q; + } + } + + public static void MixAddPlateauFrequency(ComplexSpan target, IqPair amplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + var length = target.Length; + var carrier = amplitude; + var phaser = IqPair.FromPolarCoordinates(T.One, dPhase); + var i = 0; + ref var targetI = ref MemoryMarshal.GetReference(target.DataI); + ref var targetQ = ref MemoryMarshal.GetReference(target.DataQ); + var vSize = Vector.Count; + + if (Vector.IsHardwareAccelerated && length >= 2 * vSize) + { + Span phaserI = stackalloc T[vSize]; + Span phaserQ = stackalloc T[vSize]; + var phaserBulk = IqPair.One; + for (var j = 0; j < vSize; j++) + { + phaserI[j] = phaserBulk.I; + phaserQ[j] = phaserBulk.Q; + phaserBulk *= phaser; + } + var phaserVectorI = new Vector(phaserI); + var phaserVectorQ = new Vector(phaserQ); + + var carrierBroadcastI = new Vector(carrier.I); + var carrierBroadcastQ = new Vector(carrier.Q); + var carrierVectorI = phaserVectorI * carrierBroadcastI - phaserVectorQ * carrierBroadcastQ; + var carrierVectorQ = phaserVectorI * carrierBroadcastQ + phaserVectorQ * carrierBroadcastI; + + var phaserBulkBroadcastI = new Vector(phaserBulk.I); + var phaserBulkBroadcastQ = new Vector(phaserBulk.Q); + for (; i < length - vSize + 1; i += vSize) + { + ref var targetVectorI = ref Unsafe.As>(ref Unsafe.Add(ref targetI, i)); + ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); + targetVectorI += carrierVectorI; + targetVectorQ += carrierVectorQ; + + var newCarrierVectorI = carrierVectorI * phaserBulkBroadcastI - carrierVectorQ * phaserBulkBroadcastQ; + var newCarrierVectorQ = carrierVectorI * phaserBulkBroadcastQ + carrierVectorQ * phaserBulkBroadcastI; + carrierVectorI = newCarrierVectorI; + carrierVectorQ = newCarrierVectorQ; + } + carrier = new IqPair(carrierVectorI[0], carrierVectorQ[0]); + } + + for (; i < length; i++) + { + Unsafe.Add(ref targetI, i) += carrier.I; + Unsafe.Add(ref targetQ, i) += carrier.Q; + carrier *= phaser; + } + } + + public static void MixAdd(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude) where T : unmanaged, IFloatingPointIeee754 { var length = source.Length; @@ -86,7 +222,51 @@ public static void MixAddFrequency(ComplexArraySpan target, ComplexArrayRe { return; } - Debug.Assert(target.Length >= source.Length); + LengthCheck(target, source); + + var i = 0; + ref var targetI = ref MemoryMarshal.GetReference(target.DataI); + ref var targetQ = ref MemoryMarshal.GetReference(target.DataQ); + ref var sourceI = ref MemoryMarshal.GetReference(source.DataI); + ref var sourceQ = ref MemoryMarshal.GetReference(source.DataQ); + var vSize = Vector.Count; + + if (Vector.IsHardwareAccelerated && length >= 2 * vSize) + { + var amplitudeVectorI = new Vector(amplitude.I); + var amplitudeVectorQ = new Vector(amplitude.Q); + + for (; i < length - vSize + 1; i += vSize) + { + var sourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i)); + var sourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i)); + var tempI = sourceVectorI * amplitudeVectorI - sourceVectorQ * amplitudeVectorQ; + var tempQ = sourceVectorI * amplitudeVectorQ + sourceVectorQ * amplitudeVectorI; + ref var targetVectorI = ref Unsafe.As>(ref Unsafe.Add(ref targetI, i)); + ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); + targetVectorI += tempI; + targetVectorQ += tempQ; + } + } + + for (; i < length; i++) + { + var sourceIq = new IqPair(Unsafe.Add(ref sourceI, i), Unsafe.Add(ref sourceQ, i)); + var targetIq = sourceIq * amplitude; + Unsafe.Add(ref targetI, i) += targetIq.I; + Unsafe.Add(ref targetQ, i) += targetIq.Q; + } + } + + public static void MixAddFrequency(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + var length = source.Length; + if (length == 0) + { + return; + } + LengthCheck(target, source); var carrier = amplitude; var phaser = IqPair.FromPolarCoordinates(T.One, dPhase); @@ -110,9 +290,14 @@ public static void MixAddFrequency(ComplexArraySpan target, ComplexArrayRe } var phaserVectorI = new Vector(phaserI); var phaserVectorQ = new Vector(phaserQ); - var carrierVectorI = phaserVectorI * carrier.I - phaserVectorQ * carrier.Q; - var carrierVectorQ = phaserVectorI * carrier.Q + phaserVectorQ * carrier.I; + var carrierBroadcastI = new Vector(carrier.I); + var carrierBroadcastQ = new Vector(carrier.Q); + var carrierVectorI = phaserVectorI * carrierBroadcastI - phaserVectorQ * carrierBroadcastQ; + var carrierVectorQ = phaserVectorI * carrierBroadcastQ + phaserVectorQ * carrierBroadcastI; + + var phaserBulkBroadcastI = new Vector(phaserBulk.I); + var phaserBulkBroadcastQ = new Vector(phaserBulk.Q); for (; i < length - vSize + 1; i += vSize) { var sourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i)); @@ -123,8 +308,8 @@ public static void MixAddFrequency(ComplexArraySpan target, ComplexArrayRe ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); targetVectorI += tempI; targetVectorQ += tempQ; - var newCarrierVectorI = carrierVectorI * phaserBulk.I - carrierVectorQ * phaserBulk.Q; - var newCarrierVectorQ = carrierVectorI * phaserBulk.Q + carrierVectorQ * phaserBulk.I; + var newCarrierVectorI = carrierVectorI * phaserBulkBroadcastI - carrierVectorQ * phaserBulkBroadcastQ; + var newCarrierVectorQ = carrierVectorI * phaserBulkBroadcastQ + carrierVectorQ * phaserBulkBroadcastI; carrierVectorI = newCarrierVectorI; carrierVectorQ = newCarrierVectorQ; } @@ -141,7 +326,7 @@ public static void MixAddFrequency(ComplexArraySpan target, ComplexArrayRe } } - public static void MixAddFrequencyWithDrag(ComplexArraySpan target, ComplexArrayReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude, T dPhase) + public static void MixAddWithDrag(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude) where T : unmanaged, IFloatingPointIeee754 { var length = source.Length; @@ -149,7 +334,98 @@ public static void MixAddFrequencyWithDrag(ComplexArraySpan target, Comple { return; } - Debug.Assert(target.Length >= source.Length); + LengthCheck(target, source); + + ref var targetI = ref MemoryMarshal.GetReference(target.DataI); + ref var targetQ = ref MemoryMarshal.GetReference(target.DataQ); + ref var sourceI = ref MemoryMarshal.GetReference(source.DataI); + ref var sourceQ = ref MemoryMarshal.GetReference(source.DataQ); + + // left boundary + { + var sourceIq = new IqPair(Unsafe.Add(ref sourceI, 0), Unsafe.Add(ref sourceQ, 0)); + var sourceWithAmplitudeIq = sourceIq * amplitude; + if (length == 1) + { + Unsafe.Add(ref targetI, 0) += sourceWithAmplitudeIq.I; + Unsafe.Add(ref targetQ, 0) += sourceWithAmplitudeIq.Q; + return; + } + var nextSourceIq = new IqPair(Unsafe.Add(ref sourceI, 1), Unsafe.Add(ref sourceQ, 1)); + var diff = nextSourceIq - sourceIq; + var dragWithAmplitudeIq = diff * dragAmplitude; + var totalIq = sourceWithAmplitudeIq + dragWithAmplitudeIq; + Unsafe.Add(ref targetI, 0) += totalIq.I; + Unsafe.Add(ref targetQ, 0) += totalIq.Q; + } + var halfDragAmplitude = dragAmplitude * T.Exp2(-T.One); + + var i = 1; + var vSize = Vector.Count; + if (Vector.IsHardwareAccelerated && length >= 2 * vSize + 2) + { + var amplitudeVectorI = new Vector(amplitude.I); + var amplitudeVectorQ = new Vector(amplitude.Q); + var halfDragAmplitudeVectorI = new Vector(halfDragAmplitude.I); + var halfDragAmplitudeVectorQ = new Vector(halfDragAmplitude.Q); + + for (; i < length - vSize; i += vSize) + { + var sourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i)); + var sourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i)); + var sourceWithAmplitudeVectorI = sourceVectorI * amplitudeVectorI - sourceVectorQ * amplitudeVectorQ; + var sourceWithAmplitudeVectorQ = sourceVectorI * amplitudeVectorQ + sourceVectorQ * amplitudeVectorI; + + var nextSourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i + 1)); + var nextSourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i + 1)); + var prevSourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i - 1)); + var prevSourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i - 1)); + var diffVectorI = nextSourceVectorI - prevSourceVectorI; + var diffVectorQ = nextSourceVectorQ - prevSourceVectorQ; + var dragWithAmplitudeVectorI = diffVectorI * halfDragAmplitudeVectorI - diffVectorQ * halfDragAmplitudeVectorQ; + var dragWithAmplitudeVectorQ = diffVectorI * halfDragAmplitudeVectorQ + diffVectorQ * halfDragAmplitudeVectorI; + + var totalVectorI = sourceWithAmplitudeVectorI + dragWithAmplitudeVectorI; + var totalVectorQ = sourceWithAmplitudeVectorQ + dragWithAmplitudeVectorQ; + ref var targetVectorI = ref Unsafe.As>(ref Unsafe.Add(ref targetI, i)); + ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); + targetVectorI += totalVectorI; + targetVectorQ += totalVectorQ; + } + } + + for (; i < length - 1; i++) + { + var sourceIq = new IqPair(Unsafe.Add(ref sourceI, i), Unsafe.Add(ref sourceQ, i)); + var nextSourceIq = new IqPair(Unsafe.Add(ref sourceI, i + 1), Unsafe.Add(ref sourceQ, i + 1)); + var prevSourceIq = new IqPair(Unsafe.Add(ref sourceI, i - 1), Unsafe.Add(ref sourceQ, i - 1)); + var diff = nextSourceIq - prevSourceIq; + var totalIq = sourceIq * amplitude + diff * halfDragAmplitude; + Unsafe.Add(ref targetI, i) += totalIq.I; + Unsafe.Add(ref targetQ, i) += totalIq.Q; + } + + // right boundary + { + var sourceIq = new IqPair(Unsafe.Add(ref sourceI, length - 1), Unsafe.Add(ref sourceQ, length - 1)); + var prevSourceIq = new IqPair(Unsafe.Add(ref sourceI, length - 2), Unsafe.Add(ref sourceQ, length - 2)); + var diff = sourceIq - prevSourceIq; + var totalIq = sourceIq * amplitude + diff * dragAmplitude; + Unsafe.Add(ref targetI, length - 1) += totalIq.I; + Unsafe.Add(ref targetQ, length - 1) += totalIq.Q; + } + } + + public static void MixAddFrequencyWithDrag(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + var length = source.Length; + if (length == 0) + { + return; + } + LengthCheck(target, source); + ref var targetI = ref MemoryMarshal.GetReference(target.DataI); ref var targetQ = ref MemoryMarshal.GetReference(target.DataQ); ref var sourceI = ref MemoryMarshal.GetReference(source.DataI); @@ -191,19 +467,28 @@ public static void MixAddFrequencyWithDrag(ComplexArraySpan target, Comple phaserQ[j] = phaserBulk.Q; phaserBulk *= phaser; } + var phaserVectorI = new Vector(phaserI); var phaserVectorQ = new Vector(phaserQ); - var carrierVectorI = phaserVectorI * carrier.I - phaserVectorQ * carrier.Q; - var carrierVectorQ = phaserVectorI * carrier.Q + phaserVectorQ * carrier.I; - var dragCarrierVectorI = phaserVectorI * dragCarrier.I - phaserVectorQ * dragCarrier.Q; - var dragCarrierVectorQ = phaserVectorI * dragCarrier.Q + phaserVectorQ * dragCarrier.I; + var carrierBroadcastI = new Vector(carrier.I); + var carrierBroadcastQ = new Vector(carrier.Q); + var dragCarrierBroadcastI = new Vector(dragCarrier.I); + var dragCarrierBroadcastQ = new Vector(dragCarrier.Q); + var carrierVectorI = phaserVectorI * carrierBroadcastI - phaserVectorQ * carrierBroadcastQ; + var carrierVectorQ = phaserVectorI * carrierBroadcastQ + phaserVectorQ * carrierBroadcastI; + var dragCarrierVectorI = phaserVectorI * dragCarrierBroadcastI - phaserVectorQ * dragCarrierBroadcastQ; + var dragCarrierVectorQ = phaserVectorI * dragCarrierBroadcastQ + phaserVectorQ * dragCarrierBroadcastI; + + var phaserBulkBroadcastI = new Vector(phaserBulk.I); + var phaserBulkBroadcastQ = new Vector(phaserBulk.Q); for (; i < length - vSize; i += vSize) { var sourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i)); var sourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i)); var sourceWithAmplitudeVectorI = sourceVectorI * carrierVectorI - sourceVectorQ * carrierVectorQ; var sourceWithAmplitudeVectorQ = sourceVectorI * carrierVectorQ + sourceVectorQ * carrierVectorI; + var nextSourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i + 1)); var nextSourceVectorQ = Unsafe.As>(ref Unsafe.Add(ref sourceQ, i + 1)); var prevSourceVectorI = Unsafe.As>(ref Unsafe.Add(ref sourceI, i - 1)); @@ -212,16 +497,18 @@ public static void MixAddFrequencyWithDrag(ComplexArraySpan target, Comple var diffVectorQ = nextSourceVectorQ - prevSourceVectorQ; var dragWithAmplitudeVectorI = diffVectorI * dragCarrierVectorI - diffVectorQ * dragCarrierVectorQ; var dragWithAmplitudeVectorQ = diffVectorI * dragCarrierVectorQ + diffVectorQ * dragCarrierVectorI; + var totalVectorI = sourceWithAmplitudeVectorI + dragWithAmplitudeVectorI; var totalVectorQ = sourceWithAmplitudeVectorQ + dragWithAmplitudeVectorQ; ref var targetVectorI = ref Unsafe.As>(ref Unsafe.Add(ref targetI, i)); ref var targetVectorQ = ref Unsafe.As>(ref Unsafe.Add(ref targetQ, i)); targetVectorI += totalVectorI; targetVectorQ += totalVectorQ; - var newCarrierVectorI = carrierVectorI * phaserBulk.I - carrierVectorQ * phaserBulk.Q; - var newCarrierVectorQ = carrierVectorI * phaserBulk.Q + carrierVectorQ * phaserBulk.I; - var newDragCarrierVectorI = dragCarrierVectorI * phaserBulk.I - dragCarrierVectorQ * phaserBulk.Q; - var newDragCarrierVectorQ = dragCarrierVectorI * phaserBulk.Q + dragCarrierVectorQ * phaserBulk.I; + + var newCarrierVectorI = carrierVectorI * phaserBulkBroadcastI - carrierVectorQ * phaserBulkBroadcastQ; + var newCarrierVectorQ = carrierVectorI * phaserBulkBroadcastQ + carrierVectorQ * phaserBulkBroadcastI; + var newDragCarrierVectorI = dragCarrierVectorI * phaserBulkBroadcastI - dragCarrierVectorQ * phaserBulkBroadcastQ; + var newDragCarrierVectorQ = dragCarrierVectorI * phaserBulkBroadcastQ + dragCarrierVectorQ * phaserBulkBroadcastI; carrierVectorI = newCarrierVectorI; carrierVectorQ = newCarrierVectorQ; dragCarrierVectorI = newDragCarrierVectorI; @@ -255,4 +542,13 @@ public static void MixAddFrequencyWithDrag(ComplexArraySpan target, Comple Unsafe.Add(ref targetQ, length - 1) += totalIq.Q; } } + + private static void LengthCheck(ComplexSpan target, ComplexReadOnlySpan source) where T : unmanaged, IFloatingPointIeee754 + { + Debug.Assert(target.Length >= source.Length); + if (target.Length < source.Length) + { + ThrowHelper.ThrowArgumentException("Target length must be greater than or equal to source length."); + } + } } diff --git a/tests/Qynit.Pulsewave.Tests/ComplexArrayReadOnlySpanTests.cs b/tests/Qynit.Pulsewave.Tests/ComplexReadOnlySpanTests.cs similarity index 74% rename from tests/Qynit.Pulsewave.Tests/ComplexArrayReadOnlySpanTests.cs rename to tests/Qynit.Pulsewave.Tests/ComplexReadOnlySpanTests.cs index d955185..77bd7fa 100644 --- a/tests/Qynit.Pulsewave.Tests/ComplexArrayReadOnlySpanTests.cs +++ b/tests/Qynit.Pulsewave.Tests/ComplexReadOnlySpanTests.cs @@ -1,6 +1,6 @@ namespace Qynit.Pulsewave.Tests; -public class ComplexArrayReadOnlySpanTests +public class ComplexReadOnlySpanTests { private const int Length = 100; @@ -23,19 +23,19 @@ public void Slice_Start_Equal() { // Arrange using var array = GetInitializedPooledComplexArray(); - var complexArrayReadOnlySpan = (ComplexArrayReadOnlySpan)array; + var complexReadOnlySpan = (ComplexReadOnlySpan)array; var start = 15; var remainingLength = array.Length - start; // Act #pragma warning disable IDE0057 // Use range operator - var result = complexArrayReadOnlySpan.Slice( + var result = complexReadOnlySpan.Slice( start); #pragma warning restore IDE0057 // Use range operator // Assert - Assert.Equal(complexArrayReadOnlySpan.DataI[start..].ToArray(), result.DataI.ToArray()); - Assert.Equal(complexArrayReadOnlySpan.DataQ[start..].ToArray(), result.DataQ.ToArray()); + Assert.Equal(complexReadOnlySpan.DataI[start..].ToArray(), result.DataI.ToArray()); + Assert.Equal(complexReadOnlySpan.DataQ[start..].ToArray(), result.DataQ.ToArray()); Assert.Equal(remainingLength, result.Length); } @@ -44,18 +44,18 @@ public void Slice_StartLength_Equal() { // Arrange using var array = GetInitializedPooledComplexArray(); - var complexArrayReadOnlySpan = (ComplexArrayReadOnlySpan)array; + var complexReadOnlySpan = (ComplexReadOnlySpan)array; var start = 15; var length = 40; // Act - var result = complexArrayReadOnlySpan.Slice( + var result = complexReadOnlySpan.Slice( start, length); // Assert - Assert.Equal(complexArrayReadOnlySpan.DataI.Slice(start, length).ToArray(), result.DataI.ToArray()); - Assert.Equal(complexArrayReadOnlySpan.DataQ.Slice(start, length).ToArray(), result.DataQ.ToArray()); + Assert.Equal(complexReadOnlySpan.DataI.Slice(start, length).ToArray(), result.DataI.ToArray()); + Assert.Equal(complexReadOnlySpan.DataQ.Slice(start, length).ToArray(), result.DataQ.ToArray()); Assert.Equal(length, result.Length); } @@ -71,7 +71,7 @@ public void CopyTo_EqualLength_Equal() // Act - ((ComplexArrayReadOnlySpan)source).CopyTo( + ((ComplexReadOnlySpan)source).CopyTo( destination); // Assert @@ -91,7 +91,7 @@ public void CopyTo_DestinationLonger_Equal() // Act - ((ComplexArrayReadOnlySpan)source).CopyTo( + ((ComplexReadOnlySpan)source).CopyTo( destination); // Assert @@ -113,7 +113,7 @@ public void CopyTo_DestinationShorter_Throw() // Act - Assert.Throws(() => ((ComplexArrayReadOnlySpan)source).CopyTo( + Assert.Throws(() => ((ComplexReadOnlySpan)source).CopyTo( destination)); } } diff --git a/tests/Qynit.Pulsewave.Tests/WaveformUtilsTests.cs b/tests/Qynit.Pulsewave.Tests/WaveformUtilsTests.cs index 01a979b..2d98570 100644 --- a/tests/Qynit.Pulsewave.Tests/WaveformUtilsTests.cs +++ b/tests/Qynit.Pulsewave.Tests/WaveformUtilsTests.cs @@ -4,6 +4,8 @@ namespace Qynit.Pulsewave.Tests; public class WaveformUtilsTests { + private const double MixFrequency = 253.1033e6; + [Fact] public void SampleWaveform_Double_Equal() { @@ -37,101 +39,232 @@ public void SampleWaveform_Double_Equal() public void MixAddFrequency_Double_Equal() { // Arrange - var envelopeInfo = new EnvelopeInfo(0.9, 1e9); - var shape = new TrianglePulseShape(); - var width = 30e-9; - var plateau = 40e-9; - using var envelope = WaveformUtils.SampleEnvelope( - envelopeInfo, - shape, - width, - plateau); + var sampleRate = 1e9; + using var envelope = GetEnvelope(sampleRate); var additionalLength = 10; - using var target = new PooledComplexArray(envelope.Length + additionalLength, true); + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); var amplitude = 0.5; - var frequency = 100e6; + var frequency = MixFrequency; var phase = Math.PI / 6; var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); - var dt = 1 / envelopeInfo.SampleRate; - var dPhase = Math.Tau * frequency * dt; + var dragAmplitude = IqPair.Zero; + var dPhase = Math.Tau * frequency / sampleRate; // Act WaveformUtils.MixAddFrequency(target, envelope, cAmplitude, dPhase); - WaveformUtils.MixAddFrequency(target, envelope, cAmplitude, dPhase); - var expectI = new double[target.Length]; - var expectQ = new double[target.Length]; - for (var i = 0; i < envelope.Length; i++) - { - var t = i * dt; - var cPhase = phase + Math.Tau * frequency * t; - var c = Complex.FromPolarCoordinates(amplitude * 2, cPhase); - var p = new Complex(envelope.DataI[i], envelope.DataQ[i]) * c; - expectI[i] = p.Real; - expectQ[i] = p.Imaginary; - } + // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); + var comparer = new ToleranceComparer(1e-9); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); + } + + [Fact] + public void MixAdd_Double_Equal() + { + // Arrange + var sampleRate = 1e9; + using var envelope = GetEnvelope(sampleRate); + var additionalLength = 10; + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); + + var amplitude = 0.5; + var frequency = 0; + var phase = Math.PI / 6; + + var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); + var dragAmplitude = IqPair.Zero; + var dPhase = Math.Tau * frequency / sampleRate; + + // Act + WaveformUtils.MixAdd(target, envelope, cAmplitude); + + // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); + var comparer = new ToleranceComparer(1e-9); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); + } + + [Fact] + public void MixAddPlateauFrequency_Double_Equal() + { + // Arrange + var sampleRate = 1e9; + using var envelope = GetPlateau(); + var additionalLength = 0; + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); + + var amplitude = 0.5; + var frequency = MixFrequency; + var phase = Math.PI / 6; + + var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); + var dragAmplitude = IqPair.Zero; + var dPhase = Math.Tau * frequency / sampleRate; + + // Act + WaveformUtils.MixAddPlateauFrequency(target, cAmplitude, dPhase); + + // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); + var comparer = new ToleranceComparer(1e-9); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); + } + + [Fact] + public void MixAddPlateau_Double_Equal() + { + // Arrange + var sampleRate = 1e9; + using var envelope = GetPlateau(); + var additionalLength = 0; + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); + + var amplitude = 0.5; + var frequency = 0; + var phase = Math.PI / 6; + + var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); + var dragAmplitude = IqPair.Zero; + var dPhase = Math.Tau * frequency / sampleRate; + + // Act + WaveformUtils.MixAddPlateau(target, cAmplitude); // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); var comparer = new ToleranceComparer(1e-9); - Assert.Equal(target.DataI.ToArray(), expectI, comparer); - Assert.Equal(target.DataQ.ToArray(), expectQ, comparer); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); } [Fact] public void MixAddFrequencyWithDrag_Double_Equal() { // Arrange - var envelopeInfo = new EnvelopeInfo(0.9, 1e9); - var shape = new TrianglePulseShape(); - var width = 30e-9; - var plateau = 40e-9; - using var envelope = WaveformUtils.SampleEnvelope( - envelopeInfo, - shape, - width, - plateau); + var sampleRate = 1e9; + using var envelope = GetEnvelope(sampleRate); var additionalLength = 10; - using var target = new PooledComplexArray(envelope.Length + additionalLength, true); + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); var amplitude = 0.5; - var frequency = 100e6; + var frequency = MixFrequency; var phase = Math.PI / 6; var dragCoefficient = 2e-9; var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); - var dragAmplitude = cAmplitude * dragCoefficient * envelopeInfo.SampleRate * IqPair.ImaginaryOne; - var dt = 1 / envelopeInfo.SampleRate; - var dPhase = Math.Tau * frequency * dt; + var dragAmplitude = cAmplitude * dragCoefficient * sampleRate * IqPair.ImaginaryOne; + var dPhase = Math.Tau * frequency / sampleRate; // Act WaveformUtils.MixAddFrequencyWithDrag(target, envelope, cAmplitude, dragAmplitude, dPhase); - WaveformUtils.MixAddFrequencyWithDrag(target, envelope, cAmplitude, dragAmplitude, dPhase); - var expectI = new double[target.Length]; - var expectQ = new double[target.Length]; - for (var i = 0; i < envelope.Length; i++) + // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); + var comparer = new ToleranceComparer(1e-9); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); + } + + [Fact] + public void MixAddWithDrag_Double_Equal() + { + // Arrange + var sampleRate = 1e9; + using var envelope = GetEnvelope(sampleRate); + var additionalLength = 10; + using var expected = GetBuffer(envelope, additionalLength); + using var target = expected.Copy(); + + var amplitude = 0.5; + var frequency = 0; + var phase = Math.PI / 6; + var dragCoefficient = 2e-9; + + var cAmplitude = IqPair.FromPolarCoordinates(amplitude, phase); + var dragAmplitude = cAmplitude * dragCoefficient * sampleRate * IqPair.ImaginaryOne; + var dPhase = Math.Tau * frequency / sampleRate; + + // Act + WaveformUtils.MixAddWithDrag(target, envelope, cAmplitude, dragAmplitude); + + // Assert + MixAddWithDragSimple(expected, envelope, cAmplitude, dragAmplitude, dPhase); + var comparer = new ToleranceComparer(1e-9); + Assert.Equal(target.DataI.ToArray(), expected.DataI.ToArray(), comparer); + Assert.Equal(target.DataQ.ToArray(), expected.DataQ.ToArray(), comparer); + } + + private static PooledComplexArray GetEnvelope(double sampleRate) + { + var envelopeInfo = new EnvelopeInfo(0.9, sampleRate); + var shape = new TrianglePulseShape(); + var width = 30e-9; + var plateau = 45e-9; + var envelope = WaveformUtils.SampleEnvelope( + envelopeInfo, + shape, + width, + plateau); + return envelope; + } + + private static PooledComplexArray GetPlateau() + { + var array = new PooledComplexArray(101, true); + array.DataI.Fill(1); + return array; + } + + private static PooledComplexArray GetBuffer(PooledComplexArray envelope, int additionalLength) + { + var expected = new PooledComplexArray(envelope.Length + additionalLength, true); + var rng = new Random(42); + for (var i = 0; i < expected.Length; i++) + { + expected.DataI[i] = rng.NextDouble(); + expected.DataQ[i] = rng.NextDouble(); + } + + return expected; + } + + private static void MixAddWithDragSimple(ComplexSpan target, ComplexReadOnlySpan source, IqPair amplitude, IqPair dragAmplitude, T dPhase) + where T : unmanaged, IFloatingPointIeee754 + { + var length = source.Length; + var sourceI = source.DataI; + var sourceQ = source.DataQ; + var targetI = target.DataI; + var targetQ = target.DataQ; + + var carrier = amplitude; + var dragCarrier = dragAmplitude; + var phaser = IqPair.FromPolarCoordinates(T.One, dPhase); + for (var i = 0; i < length; i++) { - var t = i * dt; - var cPhase = phase + Math.Tau * frequency * t; - var c = Complex.FromPolarCoordinates(amplitude * 2, cPhase); - var env = new Complex(envelope.DataI[i], envelope.DataQ[i]); var diff = i switch { - 0 => new Complex(envelope.DataI[i + 1], envelope.DataQ[i + 1]) - env, - _ when i == envelope.Length - 1 => env - new Complex(envelope.DataI[i - 1], envelope.DataQ[i - 1]), - _ => (new Complex(envelope.DataI[i + 1], envelope.DataQ[i + 1]) - new Complex(envelope.DataI[i - 1], envelope.DataQ[i - 1])) / 2, + 0 => new IqPair(sourceI[i + 1], sourceQ[i + 1]) - new IqPair(sourceI[i], sourceQ[i]), + _ when i == length - 1 => new IqPair(sourceI[i], sourceQ[i]) - new IqPair(sourceI[i - 1], sourceQ[i - 1]), + _ => (new IqPair(sourceI[i + 1], sourceQ[i + 1]) - new IqPair(sourceI[i - 1], sourceQ[i - 1])) * T.CreateChecked(0.5), }; - var drag = diff * envelopeInfo.SampleRate * dragCoefficient * Complex.ImaginaryOne; - var total = (env + drag) * c; - expectI[i] = total.Real; - expectQ[i] = total.Imaginary; + var sourceIq = new IqPair(sourceI[i], sourceQ[i]); + var totalIq = sourceIq * carrier + diff * dragCarrier; + targetI[i] += totalIq.I; + targetQ[i] += totalIq.Q; + carrier *= phaser; + dragCarrier *= phaser; } - - // Assert - var comparer = new ToleranceComparer(1e-9); - Assert.Equal(target.DataI.ToArray(), expectI, comparer); - Assert.Equal(target.DataQ.ToArray(), expectQ, comparer); } }