From a5b42fe2028aa29b4b6f0d183cda8ef6b1a7d21e Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Thu, 28 Mar 2024 12:36:09 +0800 Subject: [PATCH] feat: Implement AOT publishing for Python project (#125) * try expose api as c lib and use aot * code cleanup and c error code * feat: use aot publishing for python project * fix: add catch all for unmanaged entry point --- .editorconfig | 2 +- Qynit.PulseGen.sln | 7 + exclusion.dic | 2 +- hatch_build.py | 144 ++++++----------- pyproject.toml | 12 +- python/example/schedule.py | 2 +- python/example/schedule_stress.py | 6 +- python/pulsegen_client/dotnet.py | 45 ------ .../__init__.py | 12 +- python/pulsegen_cs/_native.py | 109 +++++++++++++ .../models.py | 0 src/Qynit.PulseGen.Aot/Api.cs | 151 ++++++++++++++++++ .../Models/AbsoluteScheduleDto.cs | 34 ++++ .../Models/BarrierElementDto.cs | 29 ++++ src/Qynit.PulseGen.Aot/Models/BiquadDto.cs | 30 ++++ src/Qynit.PulseGen.Aot/Models/ChannelInfo.cs | 15 ++ .../Models/GridScheduleDto.cs | 41 +++++ .../Models/HannShapeInfo.cs | 13 ++ .../Models/InterpolatedShapeInfo.cs | 15 ++ .../Models/IqCalibration.cs | 20 +++ src/Qynit.PulseGen.Aot/Models/OptionsDto.cs | 29 ++++ .../Models/PlayElementDto.cs | 49 ++++++ .../Models/RepeatElementDto.cs | 35 ++++ .../Models/ScheduleElementDto.cs | 38 +++++ .../Models/ScheduleRequest.cs | 16 ++ .../Models/SetFrequencyElementDto.cs | 28 ++++ .../Models/SetPhaseElementDto.cs | 28 ++++ src/Qynit.PulseGen.Aot/Models/ShapeInfo.cs | 12 ++ .../Models/ShiftFrequencyElementDto.cs | 28 ++++ .../Models/ShiftPhaseElementDto.cs | 28 ++++ .../Models/StackScheduleDto.cs | 37 +++++ .../Models/SwapPhaseElementDto.cs | 28 ++++ .../Models/TriangleShapeInfo.cs | 13 ++ .../Qynit.PulseGen.Aot.csproj | 23 +++ src/Qynit.PulseGen.Aot/ScheduleRunner.cs | 68 ++++++++ src/Qynit.PulseGen.Aot/UnsafeMemoryManager.cs | 21 +++ src/Qynit.PulseGen.Server/Server.cs | 4 +- src/Qynit.PulseGen/Qynit.PulseGen.csproj | 1 + 38 files changed, 1015 insertions(+), 160 deletions(-) delete mode 100644 python/pulsegen_client/dotnet.py rename python/{pulsegen_client => pulsegen_cs}/__init__.py (85%) create mode 100644 python/pulsegen_cs/_native.py rename python/{pulsegen_client => pulsegen_cs}/models.py (100%) create mode 100644 src/Qynit.PulseGen.Aot/Api.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/AbsoluteScheduleDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/BarrierElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/BiquadDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ChannelInfo.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/GridScheduleDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/HannShapeInfo.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/InterpolatedShapeInfo.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/IqCalibration.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/OptionsDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/PlayElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/RepeatElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ScheduleElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ScheduleRequest.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/SetFrequencyElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/SetPhaseElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ShapeInfo.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ShiftFrequencyElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/ShiftPhaseElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/StackScheduleDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/SwapPhaseElementDto.cs create mode 100644 src/Qynit.PulseGen.Aot/Models/TriangleShapeInfo.cs create mode 100644 src/Qynit.PulseGen.Aot/Qynit.PulseGen.Aot.csproj create mode 100644 src/Qynit.PulseGen.Aot/ScheduleRunner.cs create mode 100644 src/Qynit.PulseGen.Aot/UnsafeMemoryManager.cs diff --git a/.editorconfig b/.editorconfig index 49ed54c..8773aa5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ charset = utf-8 spelling_exclusion_path = exclusion.dic # Xml files -[*.xml] +[*.{xml,csproj,props,targets}] indent_size = 2 [*.{js,ts,cjs,mjs,jsx,tsx}] diff --git a/Qynit.PulseGen.sln b/Qynit.PulseGen.sln index 55a26aa..010f489 100644 --- a/Qynit.PulseGen.sln +++ b/Qynit.PulseGen.sln @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WaveGenBenchmarks", "exampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qynit.PulseGen.Server", "src\Qynit.PulseGen.Server\Qynit.PulseGen.Server.csproj", "{3AF1B594-E5D8-43A1-9E74-A8F9570BD0FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qynit.PulseGen.Aot", "src\Qynit.PulseGen.Aot\Qynit.PulseGen.Aot.csproj", "{50443AA3-B148-4D75-87EE-55AFB5A53D69}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,6 +48,10 @@ Global {3AF1B594-E5D8-43A1-9E74-A8F9570BD0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AF1B594-E5D8-43A1-9E74-A8F9570BD0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AF1B594-E5D8-43A1-9E74-A8F9570BD0FA}.Release|Any CPU.Build.0 = Release|Any CPU + {50443AA3-B148-4D75-87EE-55AFB5A53D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50443AA3-B148-4D75-87EE-55AFB5A53D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50443AA3-B148-4D75-87EE-55AFB5A53D69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50443AA3-B148-4D75-87EE-55AFB5A53D69}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +61,7 @@ Global {33659EDC-29F9-45DB-A9FA-E01E648BE4B9} = {3B8B8B3E-9095-4A7E-9309-661439C30D9A} {ECE2579D-700C-4BB3-9009-F35395177B7A} = {6741804E-962A-4762-B289-CD4899A62AD8} {3AF1B594-E5D8-43A1-9E74-A8F9570BD0FA} = {3EBAD22F-230A-4BCB-B16C-A9063E2A4E9A} + {50443AA3-B148-4D75-87EE-55AFB5A53D69} = {3EBAD22F-230A-4BCB-B16C-A9063E2A4E9A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1DC52AEE-D9FC-4B2F-9492-0C92B0479A3C} diff --git a/exclusion.dic b/exclusion.dic index f87fb95..978a045 100644 --- a/exclusion.dic +++ b/exclusion.dic @@ -1,2 +1,2 @@ Qynit -Biquad +Biquad diff --git a/hatch_build.py b/hatch_build.py index b3185b0..045339e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,3 +1,4 @@ +import platform import shutil import subprocess import sys @@ -5,103 +6,58 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface -SERVER_PROJECT_DIR = "src/Qynit.PulseGen.Server" -if sys.platform == "win32": - NPM = "npm.cmd" -else: - NPM = "npm" +SRC_DIR = "src/Qynit.PulseGen.Aot" +DST_DIR = "python/pulsegen_cs/lib" + + +def _check_dotnet() -> None: + try: + subprocess.run( + [ + "dotnet", + "--version", + ], + check=True, + capture_output=True, + ) + except FileNotFoundError as e: + msg = "dotnet is not installed" + raise RuntimeError(msg) from e + + +def _dotnet_publish(version: str) -> None: + if version == "editable": + configuration = "Debug" + ci = "false" + else: + configuration = "Release" + ci = "true" + try: + subprocess.run( + [ + "dotnet", + "publish", + SRC_DIR, + "--output", + DST_DIR, + "--configuration", + configuration, + "--nologo", + "--use-current-runtime", + f"-p:ContinuousIntegrationBuild={ci}", + ], + check=True, + ) + except subprocess.CalledProcessError as e: + msg = "dotnet publish failed" + raise RuntimeError(msg) from e + class CustomBuildHook(BuildHookInterface): def initialize(self, version: str, build_data: Dict[str, Any]) -> None: if self.target_name == "wheel": - self._check_dotnet() - self._check_npm() - self._npm_ci() - self._npm_build() - self._dotnet_publish(version) + _check_dotnet() + _dotnet_publish(version) def clean(self, versions: List[str]) -> None: - shutil.rmtree("artifacts", ignore_errors=True) - - def _check_dotnet(self) -> None: - try: - subprocess.run( - [ - "dotnet", - "--version", - ], - check=True, - capture_output=True, - ) - except FileNotFoundError as e: - msg = "dotnet is not installed" - raise RuntimeError(msg) from e - - def _check_npm(self) -> None: - try: - subprocess.run( - [ - NPM, - "--version", - ], - check=True, - capture_output=True, - ) - except FileNotFoundError as e: - msg = "npm is not installed" - raise RuntimeError(msg) from e - - - def _npm_ci(self) -> None: - try: - subprocess.run( - [ - NPM, - "ci", - ], - check=True, - cwd=SERVER_PROJECT_DIR, - ) - except subprocess.CalledProcessError as e: - msg = "npm ci failed" - raise RuntimeError(msg) from e - - def _npm_build(self) -> None: - try: - subprocess.run( - [ - NPM, - "run", - "build", - ], - check=True, - cwd=SERVER_PROJECT_DIR, - ) - except subprocess.CalledProcessError as e: - msg = "npm build failed" - raise RuntimeError(msg) from e - - def _dotnet_publish(self, version: str) -> None: - if version == "editable": - configuration = "Debug" - ci = "false" - else: - configuration = "Release" - ci = "true" - try: - subprocess.run( - [ - "dotnet", - "publish", - "--configuration", - configuration, - "--nologo", - "/p:UseAppHost=false", - f"/p:ContinuousIntegrationBuild={ci}", - ], - check=True, - ) - except subprocess.CalledProcessError as e: - msg = "dotnet publish failed" - raise RuntimeError(msg) from e - + shutil.rmtree(DST_DIR, ignore_errors=True) diff --git a/pyproject.toml b/pyproject.toml index 9d1a96a..40206af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "pulsegen-client" +name = "pulsegen-cs" dynamic = ["version"] -description = "Client for Qynit.PulseGen.Server" +description = "Bindings for Qynit.PulseGen" readme = "README.md" requires-python = ">=3.8" license = "MIT" @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: Implementation :: CPython", ] -dependencies = ["numpy", "msgpack", "attrs", "pythonnet>=3.0.0"] +dependencies = ["numpy", "msgpack", "attrs"] [project.urls] Documentation = "https://github.com/kahojyun/Qynit.PulseGen#readme" @@ -30,10 +30,8 @@ pattern = "(?P.*)" [tool.hatch.build.hooks.custom] [tool.hatch.build.targets.wheel] -packages = ["python/pulsegen_client"] - -[tool.hatch.build.targets.wheel.force-include] -"artifacts/publish/Qynit.PulseGen.Server/release" = "pulsegen_client/lib" +packages = ["python/pulsegen_cs"] +artifacts = ["*.so", "*.dll"] [tool.hatch.envs.default] dependencies = ["scipy", "matplotlib", "ipython"] diff --git a/python/example/schedule.py b/python/example/schedule.py index 6fefda3..f477ed5 100644 --- a/python/example/schedule.py +++ b/python/example/schedule.py @@ -10,9 +10,9 @@ import numpy as np from matplotlib import pyplot as plt +from pulsegen_cs import * from scipy import signal -from pulsegen_client import * def get_biquad(amp, tau, fs): z = -1 / (tau * (1 + amp)) diff --git a/python/example/schedule_stress.py b/python/example/schedule_stress.py index f5323bb..e9aeb2d 100644 --- a/python/example/schedule_stress.py +++ b/python/example/schedule_stress.py @@ -5,12 +5,11 @@ The server must be running for this example to work. """ -from itertools import cycle import time +from itertools import cycle import numpy as np - -from pulsegen_client import * +from pulsegen_cs import * def gen_n(n: int): @@ -76,5 +75,4 @@ def main(): if __name__ == "__main__": - start_server() main() diff --git a/python/pulsegen_client/dotnet.py b/python/pulsegen_client/dotnet.py deleted file mode 100644 index f717990..0000000 --- a/python/pulsegen_client/dotnet.py +++ /dev/null @@ -1,45 +0,0 @@ -# pylint: disable=all -# pyright: reportMissingImports=false -import sys -import typing as _typing -import numpy as _np -import pulsegen_client.models as _models -from pathlib import Path -from importlib.metadata import files - - -# Find the path to the .NET library -RUNTIME_CONFIG = [f for f in files("pulsegen_client") if str(f).endswith(".runtimeconfig.json")][0] -RUNTIME_CONFIG_PATH = RUNTIME_CONFIG.locate() -LIB_PATH = Path(RUNTIME_CONFIG_PATH).parent -sys.path.append(str(LIB_PATH)) - -from pythonnet import load - -load( - "coreclr", - runtime_config=str(RUNTIME_CONFIG_PATH), -) - -import clr - -clr.AddReference("Qynit.PulseGen.Server") -from Qynit.PulseGen.Server import PythonApi - - -def generate_waveforms( - request: _models.Request, -) -> _typing.Dict[str, _typing.Tuple[_np.ndarray, _typing.Optional[_np.ndarray]]]: - msg = request.packb() - waveforms = {} - for channel in request.channels: - length = channel.length - waveforms[channel.name] = ( - _np.empty(length, dtype=_np.float32), - _np.empty(length, dtype=_np.float32), - ) - PythonApi.Run(msg, waveforms) - return waveforms - -def start_server(): - PythonApi.StartServer() diff --git a/python/pulsegen_client/__init__.py b/python/pulsegen_cs/__init__.py similarity index 85% rename from python/pulsegen_client/__init__.py rename to python/pulsegen_cs/__init__.py index f7191e1..4fdbe43 100644 --- a/python/pulsegen_client/__init__.py +++ b/python/pulsegen_cs/__init__.py @@ -12,13 +12,18 @@ This package is still in development and the API may change in the future. """ +from ._native import generate_waveforms from .models import ( - Biquad, ChannelInfo, IqCalibration, Options, - HannShape, InterpolatedShape, TriangleShape, Absolute, Alignment, Barrier, + Biquad, + ChannelInfo, Grid, + HannShape, + InterpolatedShape, + IqCalibration, + Options, Play, Repeat, Request, @@ -28,8 +33,8 @@ ShiftPhase, Stack, SwapPhase, + TriangleShape, ) -from .dotnet import generate_waveforms, start_server __all__ = [ "ChannelInfo", @@ -53,5 +58,4 @@ "InterpolatedShape", "TriangleShape", "generate_waveforms", - "start_server", ] diff --git a/python/pulsegen_cs/_native.py b/python/pulsegen_cs/_native.py new file mode 100644 index 0000000..fd42a7e --- /dev/null +++ b/python/pulsegen_cs/_native.py @@ -0,0 +1,109 @@ +import ctypes +import sys +import typing +from enum import Enum +from pathlib import Path + +import numpy as np + +from . import models + +if sys.platform == "win32": + lib_path = Path(__file__).parent / "lib" / "Qynit.PulseGen.Aot.dll" +else: + lib_path = Path(__file__).parent / "lib" / "Qynit.PulseGen.Aot.so" + +lib = ctypes.cdll.LoadLibrary(str(lib_path.resolve())) + + +# enum ErrorCode +# { +# Success = 0, +# DeserializeError = 1, +# GenerateWaveformsError = 2, +# KeyNotFound = 3, +# CopyWaveformError = 4, +# InvalidHandle = 5, +# InternalError = 6, +# } +class ErrorCode(Enum): + Success = 0 + DeserializeError = 1 + GenerateWaveformsError = 2 + KeyNotFound = 3 + CopyWaveformError = 4 + InvalidHandle = 5 + InternalError = 6 + + +# int Qynit_PulseGen_Run(char* request, int length, void** out_handle) +Qynit_PulseGen_Run = lib.Qynit_PulseGen_Run +Qynit_PulseGen_Run.argtypes = [ + ctypes.c_char_p, + ctypes.c_int, + ctypes.POINTER(ctypes.c_void_p), +] +Qynit_PulseGen_Run.restype = ctypes.c_int + + +def _run(msg: bytes) -> ctypes.c_void_p: + handle = ctypes.c_void_p() + ret = Qynit_PulseGen_Run(msg, len(msg), ctypes.byref(handle)) + if ret != 0: + err = ErrorCode(ret) + raise Exception(f"Failed to run PulseGen, error code: {err}") + return handle + + +# int Qynit_PulseGen_CopyWaveform(void* handle, char* name, float* i, float* q, int length) +Qynit_PulseGen_CopyWaveform = lib.Qynit_PulseGen_CopyWaveform +Qynit_PulseGen_CopyWaveform.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_float), + ctypes.POINTER(ctypes.c_float), + ctypes.c_int, +] +Qynit_PulseGen_CopyWaveform.restype = ctypes.c_int + + +def _copy_waveform( + handle: ctypes.c_void_p, name: str, length: int +) -> typing.Tuple[np.ndarray, np.ndarray]: + wave_i = np.empty(length, dtype=np.float32) + wave_q = np.empty(length, dtype=np.float32) + pstr = name.encode("utf-8") + ptr_i_float = wave_i.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) + ptr_q_float = wave_q.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) + ret = Qynit_PulseGen_CopyWaveform(handle, pstr, ptr_i_float, ptr_q_float, length) + if ret != 0: + err = ErrorCode(ret) + raise Exception(f"Failed to copy waveform, error code: {err}") + return wave_i, wave_q + + +# int Qynit_PulseGen_FreeWaveform(void* handle) +Qynit_PulseGen_FreeWaveform = lib.Qynit_PulseGen_FreeWaveform +Qynit_PulseGen_FreeWaveform.argtypes = [ctypes.c_void_p] +Qynit_PulseGen_FreeWaveform.restype = ctypes.c_int + + +def _free_waveform(handle: ctypes.c_void_p) -> None: + ret = Qynit_PulseGen_FreeWaveform(handle) + if ret != 0: + err = ErrorCode(ret) + raise Exception(f"Failed to free waveform, error code: {err}") + + +def generate_waveforms( + request: models.Request, +) -> typing.Dict[str, typing.Tuple[np.ndarray, np.ndarray]]: + msg = request.packb() + handle = _run(msg) + try: + waveforms = {} + for ch in request.channels: + waveforms[ch.name] = _copy_waveform(handle, ch.name, ch.length) + return waveforms + finally: + _free_waveform(handle) diff --git a/python/pulsegen_client/models.py b/python/pulsegen_cs/models.py similarity index 100% rename from python/pulsegen_client/models.py rename to python/pulsegen_cs/models.py diff --git a/src/Qynit.PulseGen.Aot/Api.cs b/src/Qynit.PulseGen.Aot/Api.cs new file mode 100644 index 0000000..1e8956e --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Api.cs @@ -0,0 +1,151 @@ +using System.Runtime.InteropServices; + +using MessagePack; + +using Qynit.PulseGen.Aot.Models; + +namespace Qynit.PulseGen.Aot; + +public static class Api +{ + private static MessagePackSerializerOptions MessagePackSerializerOptions { get; } = + new MessagePackSerializerOptions(GeneratedMessagePackResolver.InstanceWithStandardAotResolver); + + enum ErrorCode + { + Success = 0, + DeserializeError = 1, + GenerateWaveformsError = 2, + KeyNotFound = 3, + CopyWaveformError = 4, + InvalidHandle = 5, + InternalError = 6, + } + + [UnmanagedCallersOnly(EntryPoint = "Qynit_PulseGen_Run")] + public static unsafe int Run(byte* requestMsg, int requestMsgLen, void** outWaveformDict) + { + try + { + ScheduleRequest request; + try + { + request = DeserializeRequest(requestMsg, requestMsgLen); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.DeserializeError; + } + List> waveforms; + try + { + waveforms = GenerateWaveforms(request); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.GenerateWaveformsError; + } + var waveformsDict = waveforms.Zip(request.ChannelTable!).ToDictionary(x => x.Second.Name, x => x.First); + var handle = GCHandle.Alloc(waveformsDict); + *outWaveformDict = GCHandle.ToIntPtr(handle).ToPointer(); + return (int)ErrorCode.Success; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.InternalError; + } + } + + [UnmanagedCallersOnly(EntryPoint = "Qynit_PulseGen_CopyWaveform")] + public static unsafe int CopyWaveform(IntPtr handle, IntPtr chName, float* bufferI, float* bufferQ, int bufferLen) + { + try + { + Dictionary> waveformsDict; + try + { + waveformsDict = (Dictionary>)GCHandle.FromIntPtr(handle).Target!; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.InvalidHandle; + } + var chNameStr = Marshal.PtrToStringUTF8(chName); + if (chNameStr is null) + { + return (int)ErrorCode.KeyNotFound; + } + if (!waveformsDict.TryGetValue(chNameStr, out var waveform)) + { + return (int)ErrorCode.KeyNotFound; + } + try + { + var spanI = new Span(bufferI, bufferLen); + waveform.DataI.CopyTo(spanI); + var spanQ = new Span(bufferQ, bufferLen); + waveform.DataQ.CopyTo(spanQ); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.CopyWaveformError; + } + return (int)ErrorCode.Success; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.InternalError; + } + } + + [UnmanagedCallersOnly(EntryPoint = "Qynit_PulseGen_FreeWaveform")] + public static int FreeWaveform(IntPtr handle) + { + try + { + Dictionary> waveformsDict; + try + { + waveformsDict = (Dictionary>)GCHandle.FromIntPtr(handle).Target!; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.InvalidHandle; + } + foreach (var waveform in waveformsDict.Values) + { + waveform.Dispose(); + } + GCHandle.FromIntPtr(handle).Free(); + return (int)ErrorCode.Success; + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return (int)ErrorCode.InternalError; + } + } + + private static List> GenerateWaveforms(ScheduleRequest request) + { + var runner = new ScheduleRunner(request); + return runner.Run(); + } + + private static unsafe ScheduleRequest DeserializeRequest(byte* msg, int len) + { + using var memoryManager = new UnsafeMemoryManager(msg, len); + var memory = memoryManager.Memory; + var options = MessagePackSerializerOptions; + var formatter = options.Resolver.GetFormatterWithVerify(); + var reader = new MessagePackReader(memory); + return formatter.Deserialize(ref reader, options); + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/AbsoluteScheduleDto.cs b/src/Qynit.PulseGen.Aot/Models/AbsoluteScheduleDto.cs new file mode 100644 index 0000000..8f40eba --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/AbsoluteScheduleDto.cs @@ -0,0 +1,34 @@ +using CommunityToolkit.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class AbsoluteScheduleDto : ScheduleElementDto +{ + [Key(6)] + public IList<(double Time, ScheduleElementDto Element)>? Elements { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + Guard.IsNotNull(Elements); + var result = new AbsoluteSchedule() + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + foreach (var (time, element) in Elements) + { + result.Add(element.GetScheduleElement(request), time); + } + return result; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/BarrierElementDto.cs b/src/Qynit.PulseGen.Aot/Models/BarrierElementDto.cs new file mode 100644 index 0000000..b50838d --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/BarrierElementDto.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class BarrierElementDto : ScheduleElementDto +{ + [Key(6)] + public ISet? ChannelIds { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + Guard.IsNotNull(ChannelIds); + return new BarrierElement(ChannelIds) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/BiquadDto.cs b/src/Qynit.PulseGen.Aot/Models/BiquadDto.cs new file mode 100644 index 0000000..7b8b1f9 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/BiquadDto.cs @@ -0,0 +1,30 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class BiquadDto +{ + [Key(0)] + public double B0 { get; set; } + [Key(1)] + public double B1 { get; set; } + [Key(2)] + public double B2 { get; set; } + [Key(3)] + public double A1 { get; set; } + [Key(4)] + public double A2 { get; set; } + + public BiquadCoefficients GetBiquad() + { + return new BiquadCoefficients + { + B0 = B0, + B1 = B1, + B2 = B2, + A1 = A1, + A2 = A2, + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/ChannelInfo.cs b/src/Qynit.PulseGen.Aot/Models/ChannelInfo.cs new file mode 100644 index 0000000..c8b75ed --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ChannelInfo.cs @@ -0,0 +1,15 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed record ChannelInfo( + [property: Key(0)] string Name, + [property: Key(1)] double BaseFrequency, + [property: Key(2)] double SampleRate, + [property: Key(3)] double Delay, + [property: Key(4)] int Length, + [property: Key(5)] int AlignLevel, + [property: Key(6)] IqCalibration? IqCalibration, + [property: Key(7)] IList BiquadChain, + [property: Key(8)] IList FirCoefficients); diff --git a/src/Qynit.PulseGen.Aot/Models/GridScheduleDto.cs b/src/Qynit.PulseGen.Aot/Models/GridScheduleDto.cs new file mode 100644 index 0000000..0b46f2d --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/GridScheduleDto.cs @@ -0,0 +1,41 @@ +using CommunityToolkit.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class GridScheduleDto : ScheduleElementDto +{ + [Key(6)] + public IList<(int Column, int Span, ScheduleElementDto Element)>? Elements { get; set; } + [Key(7)] + public IList<(double Value, GridLengthUnit Unit)>? Columns { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + Guard.IsNotNull(Elements); + Guard.IsNotNull(Columns); + var result = new GridSchedule() + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + foreach (var (column, span, element) in Elements) + { + result.Add(element.GetScheduleElement(request), column, span); + } + foreach (var (value, unit) in Columns) + { + result.AddColumn(new(value, unit)); + } + return result; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/HannShapeInfo.cs b/src/Qynit.PulseGen.Aot/Models/HannShapeInfo.cs new file mode 100644 index 0000000..c3f721c --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/HannShapeInfo.cs @@ -0,0 +1,13 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed record HannShapeInfo : ShapeInfo +{ + private static readonly IPulseShape PulseShape = new HannPulseShape(); + public override IPulseShape GetPulseShape() + { + return PulseShape; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/InterpolatedShapeInfo.cs b/src/Qynit.PulseGen.Aot/Models/InterpolatedShapeInfo.cs new file mode 100644 index 0000000..1630978 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/InterpolatedShapeInfo.cs @@ -0,0 +1,15 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed record InterpolatedShapeInfo( + [property: Key(0)] double[] X, + [property: Key(1)] double[] Y) : ShapeInfo +{ + private InterpolatedPulseShape? _pulseShape; + public override IPulseShape GetPulseShape() + { + return _pulseShape ??= InterpolatedPulseShape.CreateFromXY(X, Y); + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/IqCalibration.cs b/src/Qynit.PulseGen.Aot/Models/IqCalibration.cs new file mode 100644 index 0000000..dfe8393 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/IqCalibration.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class IqCalibration +{ + [Key(0)] + public double A { get; set; } + [Key(1)] + public double B { get; set; } + [Key(2)] + public double C { get; set; } + [Key(3)] + public double D { get; set; } + [Key(4)] + public double IOffset { get; set; } + [Key(5)] + public double QOffset { get; set; } +} diff --git a/src/Qynit.PulseGen.Aot/Models/OptionsDto.cs b/src/Qynit.PulseGen.Aot/Models/OptionsDto.cs new file mode 100644 index 0000000..ace8b15 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/OptionsDto.cs @@ -0,0 +1,29 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class OptionsDto +{ + [Key(0)] + public double TimeTolerance { get; set; } + [Key(1)] + public double AmpTolerance { get; set; } + [Key(2)] + public double PhaseTolerance { get; set; } + [Key(3)] + public bool AllowOversize { get; set; } + + + private PulseGenOptions? _options; + public PulseGenOptions GetOptions() + { + return _options ??= new() + { + TimeTolerance = TimeTolerance, + AmpTolerance = AmpTolerance, + PhaseTolerance = PhaseTolerance, + AllowOversize = AllowOversize, + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/PlayElementDto.cs b/src/Qynit.PulseGen.Aot/Models/PlayElementDto.cs new file mode 100644 index 0000000..2f47dcb --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/PlayElementDto.cs @@ -0,0 +1,49 @@ +using System.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class PlayElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId { get; set; } + [Key(7)] + public double Amplitude { get; set; } + [Key(8)] + public int ShapeId { get; set; } + [Key(9)] + public double Width { get; set; } + [Key(10)] + public double Plateau { get; set; } + [Key(11)] + public double DragCoefficient { get; set; } + [Key(12)] + public double Frequency { get; set; } + [Key(13)] + public double Phase { get; set; } + [Key(14)] + public bool FlexiblePlateau { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + var shapes = request.ShapeTable; + Debug.Assert(shapes is not null); + var pulseShape = ShapeId == -1 ? null : shapes[ShapeId].GetPulseShape(); + var envelope = new Envelope(pulseShape, Width, Plateau); + return new PlayElement(ChannelId, envelope, Frequency, Phase, Amplitude, DragCoefficient) + { + FlexiblePlateau = FlexiblePlateau, + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/RepeatElementDto.cs b/src/Qynit.PulseGen.Aot/Models/RepeatElementDto.cs new file mode 100644 index 0000000..ae62e67 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/RepeatElementDto.cs @@ -0,0 +1,35 @@ +using CommunityToolkit.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class RepeatElementDto : ScheduleElementDto +{ + [Key(6)] + public ScheduleElementDto? Element { get; set; } + [Key(7)] + public int Count { get; set; } + [Key(8)] + public double Spacing { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + Guard.IsNotNull(Element); + var element = Element.GetScheduleElement(request); + return new RepeatElement(element, Count) + { + Spacing = Spacing, + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/ScheduleElementDto.cs b/src/Qynit.PulseGen.Aot/Models/ScheduleElementDto.cs new file mode 100644 index 0000000..3151626 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ScheduleElementDto.cs @@ -0,0 +1,38 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + + +[Union(0, typeof(PlayElementDto))] +[Union(1, typeof(ShiftPhaseElementDto))] +[Union(2, typeof(SetPhaseElementDto))] +[Union(3, typeof(ShiftFrequencyElementDto))] +[Union(4, typeof(SetFrequencyElementDto))] +[Union(5, typeof(SwapPhaseElementDto))] +[Union(6, typeof(BarrierElementDto))] +[Union(7, typeof(RepeatElementDto))] +[Union(8, typeof(StackScheduleDto))] +[Union(9, typeof(AbsoluteScheduleDto))] +[Union(10, typeof(GridScheduleDto))] +[MessagePackObject] +public abstract class ScheduleElementDto +{ + [Key(0)] + public (double, double) MarginData { get; set; } + [Key(1)] + public Alignment Alignment { get; set; } + [Key(2)] + public bool IsVisible { get; set; } + [Key(3)] + public double? Duration { get; set; } + [Key(4)] + public double MaxDuration { get; set; } + [Key(5)] + public double MinDuration { get; set; } + + [IgnoreMember] + public Thickness Margin => new(MarginData.Item1, MarginData.Item2); + public abstract ScheduleElement GetScheduleElement(ScheduleRequest request); +} diff --git a/src/Qynit.PulseGen.Aot/Models/ScheduleRequest.cs b/src/Qynit.PulseGen.Aot/Models/ScheduleRequest.cs new file mode 100644 index 0000000..cf673dc --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ScheduleRequest.cs @@ -0,0 +1,16 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class ScheduleRequest +{ + [Key(0)] + public IList? ChannelTable { get; set; } + [Key(1)] + public IList? ShapeTable { get; set; } + [Key(2)] + public ScheduleElementDto? Schedule { get; set; } + [Key(3)] + public OptionsDto? Options { get; set; } +} diff --git a/src/Qynit.PulseGen.Aot/Models/SetFrequencyElementDto.cs b/src/Qynit.PulseGen.Aot/Models/SetFrequencyElementDto.cs new file mode 100644 index 0000000..bb7bb7b --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/SetFrequencyElementDto.cs @@ -0,0 +1,28 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class SetFrequencyElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId { get; set; } + [Key(7)] + public double Frequency { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + return new SetFrequencyElement(ChannelId, Frequency) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/SetPhaseElementDto.cs b/src/Qynit.PulseGen.Aot/Models/SetPhaseElementDto.cs new file mode 100644 index 0000000..c27c1f7 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/SetPhaseElementDto.cs @@ -0,0 +1,28 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class SetPhaseElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId { get; set; } + [Key(7)] + public double Phase { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + return new SetPhaseElement(ChannelId, Phase) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/ShapeInfo.cs b/src/Qynit.PulseGen.Aot/Models/ShapeInfo.cs new file mode 100644 index 0000000..0370c72 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ShapeInfo.cs @@ -0,0 +1,12 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[Union(0, typeof(HannShapeInfo))] +[Union(1, typeof(TriangleShapeInfo))] +[Union(2, typeof(InterpolatedShapeInfo))] +[MessagePackObject] +public abstract record ShapeInfo +{ + public abstract IPulseShape GetPulseShape(); +} diff --git a/src/Qynit.PulseGen.Aot/Models/ShiftFrequencyElementDto.cs b/src/Qynit.PulseGen.Aot/Models/ShiftFrequencyElementDto.cs new file mode 100644 index 0000000..9d71aae --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ShiftFrequencyElementDto.cs @@ -0,0 +1,28 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class ShiftFrequencyElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId { get; set; } + [Key(7)] + public double DeltaFrequency { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + return new ShiftFrequencyElement(ChannelId, DeltaFrequency) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/ShiftPhaseElementDto.cs b/src/Qynit.PulseGen.Aot/Models/ShiftPhaseElementDto.cs new file mode 100644 index 0000000..734fd8d --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/ShiftPhaseElementDto.cs @@ -0,0 +1,28 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class ShiftPhaseElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId { get; set; } + [Key(7)] + public double DeltaPhase { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + return new ShiftPhaseElement(ChannelId, DeltaPhase) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/StackScheduleDto.cs b/src/Qynit.PulseGen.Aot/Models/StackScheduleDto.cs new file mode 100644 index 0000000..2fad9bd --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/StackScheduleDto.cs @@ -0,0 +1,37 @@ +using CommunityToolkit.Diagnostics; + +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class StackScheduleDto : ScheduleElementDto +{ + [Key(6)] + public IList? Elements { get; set; } + [Key(7)] + public ArrangeOption ArrangeOption { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + Guard.IsNotNull(Elements); + var result = new StackSchedule() + { + ArrangeOption = ArrangeOption, + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + foreach (var element in Elements) + { + result.Add(element.GetScheduleElement(request)); + } + return result; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/SwapPhaseElementDto.cs b/src/Qynit.PulseGen.Aot/Models/SwapPhaseElementDto.cs new file mode 100644 index 0000000..e29c283 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/SwapPhaseElementDto.cs @@ -0,0 +1,28 @@ +using MessagePack; + +using Qynit.PulseGen.Schedules; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed class SwapPhaseElementDto : ScheduleElementDto +{ + [Key(6)] + public int ChannelId1 { get; set; } + [Key(7)] + public int ChannelId2 { get; set; } + + public override ScheduleElement GetScheduleElement(ScheduleRequest request) + { + return new SwapPhaseElement(ChannelId1, ChannelId2) + { + Margin = Margin, + Alignment = Alignment, + IsVisible = IsVisible, + Duration = Duration, + MaxDuration = MaxDuration, + MinDuration = MinDuration, + PulseGenOptions = request.Options?.GetOptions(), + }; + } +} diff --git a/src/Qynit.PulseGen.Aot/Models/TriangleShapeInfo.cs b/src/Qynit.PulseGen.Aot/Models/TriangleShapeInfo.cs new file mode 100644 index 0000000..58b56b7 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Models/TriangleShapeInfo.cs @@ -0,0 +1,13 @@ +using MessagePack; + +namespace Qynit.PulseGen.Aot.Models; + +[MessagePackObject] +public sealed record TriangleShapeInfo : ShapeInfo +{ + private static readonly IPulseShape PulseShape = new TrianglePulseShape(); + public override IPulseShape GetPulseShape() + { + return PulseShape; + } +} diff --git a/src/Qynit.PulseGen.Aot/Qynit.PulseGen.Aot.csproj b/src/Qynit.PulseGen.Aot/Qynit.PulseGen.Aot.csproj new file mode 100644 index 0000000..eb44193 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/Qynit.PulseGen.Aot.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + true + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/src/Qynit.PulseGen.Aot/ScheduleRunner.cs b/src/Qynit.PulseGen.Aot/ScheduleRunner.cs new file mode 100644 index 0000000..c5880b0 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/ScheduleRunner.cs @@ -0,0 +1,68 @@ +using System.Diagnostics; + +using CommunityToolkit.Diagnostics; + +using Qynit.PulseGen.Aot.Models; + +namespace Qynit.PulseGen.Aot; + +public sealed class ScheduleRunner +{ + private readonly ScheduleRequest _scheduleRequest; + private readonly PulseGenOptions _options = new(); + + public ScheduleRunner(ScheduleRequest scheduleRequest) + { + Guard.IsNotNull(scheduleRequest.Schedule); + Guard.IsNotNull(scheduleRequest.ChannelTable); + Guard.IsNotNull(scheduleRequest.ShapeTable); + _scheduleRequest = scheduleRequest; + } + + public List> Run() + { + var scheduleDto = _scheduleRequest.Schedule; + Debug.Assert(scheduleDto is not null); + var schedule = scheduleDto.GetScheduleElement(_scheduleRequest); + schedule.Measure(double.PositiveInfinity); + Debug.Assert(schedule.DesiredDuration is not null); + var duration = schedule.DesiredDuration.Value; + schedule.Arrange(0, duration); + + var phaseTrackingTransform = new PhaseTrackingTransform(); + var channels = _scheduleRequest.ChannelTable; + Debug.Assert(channels is not null); + foreach (var channel in channels) + { + _ = phaseTrackingTransform.AddChannel(channel.BaseFrequency, _options.TimeTolerance); + } + schedule.Render(0, phaseTrackingTransform); + + var pulseLists = phaseTrackingTransform.Finish(); + var postProcessTransform = new PostProcessTransform(_options); + for (var i = 0; i < pulseLists.Count; i++) + { + var channel = channels[i]; + var sourceId = postProcessTransform.AddSourceNode(pulseLists[i]); + var filterId = postProcessTransform.AddFilter(new(channel.BiquadChain.Select(x => x.GetBiquad()), channel.FirCoefficients)); + var delayId = postProcessTransform.AddDelay(channel.Delay); + var terminalId = postProcessTransform.AddTerminalNode(out _); + postProcessTransform.AddEdge(sourceId, filterId); + postProcessTransform.AddEdge(filterId, delayId); + postProcessTransform.AddEdge(delayId, terminalId); + } + var pulseLists2 = postProcessTransform.Finish(); + var result = pulseLists2.Zip(channels).Select(x => + { + using var waveform = WaveformUtils.SampleWaveform(x.First, x.Second.SampleRate, 0, x.Second.Length, x.Second.AlignLevel); + if (x.Second.IqCalibration is { A: var a, B: var b, C: var c, D: var d, IOffset: var iOffset, QOffset: var qOffset }) + { + WaveformUtils.IqTransform(waveform, a, b, c, d, iOffset, qOffset); + } + var floatArray = new PooledComplexArray(waveform.Length, false); + WaveformUtils.ConvertDoubleToFloat(floatArray, waveform); + return floatArray; + }); + return result.ToList(); + } +} diff --git a/src/Qynit.PulseGen.Aot/UnsafeMemoryManager.cs b/src/Qynit.PulseGen.Aot/UnsafeMemoryManager.cs new file mode 100644 index 0000000..f260a12 --- /dev/null +++ b/src/Qynit.PulseGen.Aot/UnsafeMemoryManager.cs @@ -0,0 +1,21 @@ +using System.Buffers; + +namespace Qynit.PulseGen.Aot; + +internal unsafe class UnsafeMemoryManager(T* pointer, int length) : MemoryManager + where T : unmanaged +{ + public override Span GetSpan() + { + return new(pointer, length); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + return new(pointer + elementIndex); + } + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } +} diff --git a/src/Qynit.PulseGen.Server/Server.cs b/src/Qynit.PulseGen.Server/Server.cs index b9c4eec..bf46f0e 100644 --- a/src/Qynit.PulseGen.Server/Server.cs +++ b/src/Qynit.PulseGen.Server/Server.cs @@ -1,5 +1,3 @@ -using System.Reflection; - using MessagePack; using MessagePack.Resolvers; @@ -48,7 +46,7 @@ public static Server CreateApp(string[] args, bool embedded) private static WebApplicationBuilder CreateBuilderForEmbedded(string[] args) { - var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var assemblyPath = AppContext.BaseDirectory; var webRootPath = Path.Combine(assemblyPath, "wwwroot"); var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environments.Production; diff --git a/src/Qynit.PulseGen/Qynit.PulseGen.csproj b/src/Qynit.PulseGen/Qynit.PulseGen.csproj index eb4e401..9aa0ebf 100644 --- a/src/Qynit.PulseGen/Qynit.PulseGen.csproj +++ b/src/Qynit.PulseGen/Qynit.PulseGen.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true